Skip to content

Commit 915c2e9

Browse files
authored
Demo page enhancements
* Discover, register elements automatically based on path/filename * New layout and styling * Adds copy to clipboard buttons
1 parent 547c42c commit 915c2e9

7 files changed

Lines changed: 681 additions & 81 deletions

File tree

demo/app-root.ts

Lines changed: 90 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,104 @@
11
import { html, LitElement } from 'lit';
22
import { customElement } from 'lit/decorators.js';
3+
// unsafeHTML is needed to render dynamic custom-element tag names;
4+
// Lit's html`` tag cannot render variable tag names directly.
5+
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
36

4-
import '@src/elements/ia-button/ia-button-story';
5-
import '@src/labs/ia-snow/ia-snow-story';
6-
import '@src/elements/ia-combo-box/ia-combo-box-story';
7-
import '@src/elements/ia-status-indicator/ia-status-indicator-story';
7+
const storyModules = import.meta.glob(
8+
['../src/elements/**/*-story.ts', '../src/labs/**/*-story.ts'],
9+
{ eager: true }
10+
);
11+
12+
const storyEntries = Object.keys(storyModules)
13+
.map(path => {
14+
const labs = path.includes('/src/labs/');
15+
const parts = path.split('/');
16+
const filename = parts[parts.length - 1]; // e.g. "ia-button-story.ts"
17+
const tag = filename.replace(/-story\.ts$/, '');
18+
return { tag, storyTag: `${tag}-story`, id: `elem-${tag}`, labs };
19+
})
20+
.sort((a, b) => a.tag.localeCompare(b.tag));
21+
22+
const productionEntries = storyEntries.filter(e => !e.labs);
23+
const labsEntries = storyEntries.filter(e => e.labs);
24+
const ALL_ENTRIES = [...productionEntries, ...labsEntries];
825

926
@customElement('app-root')
1027
export class AppRoot extends LitElement {
28+
createRenderRoot() { return this; }
29+
30+
private _observer?: IntersectionObserver;
31+
private _abortController = new AbortController();
32+
1133
render() {
1234
return html`
13-
<h1>🏛️ Internet Archive Elements ⚛️</h1>
35+
<nav id="ia-sidebar">
36+
<h2>Production-Ready</h2>
37+
${productionEntries.map(e => html`<a href="#${e.id}">&lt;${e.tag}&gt;</a>`)}
38+
<h2>Labs 🧪</h2>
39+
${labsEntries.map(e => html`<a href="#${e.id}">&lt;${e.tag}&gt;</a>`)}
40+
</nav>
41+
<div id="ia-content">
42+
<h1>Internet Archive Elements</h1>
43+
<h2>Production-Ready Elements</h2>
44+
${productionEntries.map(e => html`
45+
<div id="${e.id}" class="ia-anchor">
46+
${unsafeHTML(`<${e.storyTag}></${e.storyTag}>`)}
47+
</div>
48+
`)}
49+
<h2>Labs Elements</h2>
50+
${labsEntries.map(e => html`
51+
<div id="${e.id}" class="ia-anchor">
52+
${unsafeHTML(`<${e.storyTag}></${e.storyTag}>`)}
53+
</div>
54+
`)}
55+
</div>
56+
`;
57+
}
1458

15-
<h2>🚀 Production-Ready Elements</h2>
59+
firstUpdated() {
60+
const allIds = ALL_ENTRIES.map(e => e.id);
1661

17-
<ia-status-indicator-story></ia-status-indicator-story>
18-
<ia-combo-box-story></ia-combo-box-story>
62+
const links = Object.fromEntries(
63+
allIds.map(id => [id, this.querySelector(`#ia-sidebar a[href="#${id}"]`)])
64+
);
1965

20-
<h2>🧪 Labs Elements</h2>
66+
const visible = new Set<string>();
2167

22-
<ia-snow-story></ia-snow-story>
23-
<ia-button-story></ia-button-story>
24-
`;
68+
// Only anchors in the top 30% of the viewport count as "active".
69+
// The first (topmost) visible anchor wins.
70+
this._observer = new IntersectionObserver(
71+
entries => {
72+
for (const entry of entries) {
73+
if (entry.isIntersecting) visible.add(entry.target.id);
74+
else visible.delete(entry.target.id);
75+
}
76+
const activeId = allIds.find(id => visible.has(id)) ?? allIds[0];
77+
allIds.forEach(id => links[id]?.classList.toggle('active', id === activeId));
78+
},
79+
{ rootMargin: '0px 0px -70% 0px' },
80+
);
81+
82+
allIds.forEach(id => {
83+
const el = document.getElementById(id);
84+
if (el) this._observer!.observe(el);
85+
});
86+
87+
allIds.forEach(id => {
88+
links[id]?.addEventListener('click', (e: Event) => {
89+
e.preventDefault();
90+
const el = document.getElementById(id);
91+
if (el) {
92+
const top = el.getBoundingClientRect().top + window.scrollY;
93+
window.scrollTo({ top: Math.max(0, top - 16), behavior: 'smooth' });
94+
}
95+
}, { signal: this._abortController.signal });
96+
});
97+
}
98+
99+
disconnectedCallback() {
100+
super.disconnectedCallback();
101+
this._observer?.disconnect();
102+
this._abortController.abort();
25103
}
26104
}

demo/index.css

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
:root {
2-
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
2+
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
33
line-height: 1.5;
44
font-weight: 400;
55

@@ -24,7 +24,89 @@ a:hover {
2424
}
2525

2626
body {
27-
margin: 1rem;
27+
margin: 0;
2828
min-width: 320px;
2929
min-height: 100vh;
30+
display: flex;
31+
}
32+
33+
app-root {
34+
display: contents;
35+
}
36+
37+
#ia-sidebar {
38+
width: 200px;
39+
flex-shrink: 0;
40+
position: sticky;
41+
top: 0;
42+
height: 100vh;
43+
border-right: 1px solid #ddd;
44+
padding: 1rem 0;
45+
box-sizing: border-box;
46+
overflow-y: auto;
47+
}
48+
49+
#ia-sidebar h2 {
50+
font-size: 0.7rem;
51+
font-weight: 600;
52+
text-transform: uppercase;
53+
letter-spacing: 0.08em;
54+
color: #767676;
55+
padding: 0 1rem;
56+
margin: 0 0 0.5rem;
57+
}
58+
59+
#ia-sidebar a {
60+
display: block;
61+
font-size: 0.82rem;
62+
font-weight: 400;
63+
color: #444;
64+
text-decoration: none;
65+
padding: 0.3rem 1rem;
66+
border-left: 3px solid transparent;
67+
transition: background 0.1s, color 0.1s, border-color 0.1s;
68+
white-space: nowrap;
69+
overflow: hidden;
70+
text-overflow: ellipsis;
71+
}
72+
73+
#ia-sidebar a:hover {
74+
background: #f0f0f0;
75+
color: #213547;
76+
}
77+
78+
#ia-sidebar a.active {
79+
color: #194880;
80+
border-left-color: #194880;
81+
background: #f0f4f9;
82+
font-weight: 600;
83+
}
84+
85+
#ia-content {
86+
flex: 1;
87+
padding: 0.75rem 1rem;
88+
padding-bottom: 100vh; /* lets scroll-spy activate on the last element */
89+
min-width: 0;
90+
}
91+
92+
.ia-anchor {
93+
scroll-margin-top: 16px;
94+
}
95+
96+
.ia-anchor > * {
97+
display: block;
98+
margin-bottom: 0.5rem;
99+
}
100+
101+
#ia-content h1 {
102+
font-size: 1.4rem;
103+
margin: 0 0 0.25rem;
104+
display: flex;
105+
align-items: center;
106+
}
107+
108+
#ia-content h2 {
109+
font-size: 1.1rem;
110+
font-weight: 600;
111+
margin: 0.75rem 0 0.25rem;
30112
}

demo/story-components/story-prop-settings.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ export class StoryPropsSettings extends LitElement {
4545
if (!this.propInputData) return nothing;
4646

4747
return html`
48-
<h3>Properties</h3>
4948
<div class="settings-options">
5049
<table>
5150
${this.propInputData.settings.map(

demo/story-components/story-styles-settings.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ export class StoryStylesSettings extends LitElement {
3030
if (!this.styleInputData) return nothing;
3131

3232
return html`
33-
<h3>Styles</h3>
3433
<div class="settings-options">
3534
<table>
3635
${this.styleInputData.settings.map(

0 commit comments

Comments
 (0)