Skip to content
102 changes: 90 additions & 12 deletions demo/app-root.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,104 @@
import { html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
// unsafeHTML is needed to render dynamic custom-element tag names;
// Lit's html`` tag cannot render variable tag names directly.
import { unsafeHTML } from 'lit/directives/unsafe-html.js';

import '@src/elements/ia-button/ia-button-story';
import '@src/labs/ia-snow/ia-snow-story';
import '@src/elements/ia-combo-box/ia-combo-box-story';
import '@src/elements/ia-status-indicator/ia-status-indicator-story';
const storyModules = import.meta.glob(
['../src/elements/**/*-story.ts', '../src/labs/**/*-story.ts'],
{ eager: true }
);

const storyEntries = Object.keys(storyModules)
.map(path => {
const labs = path.includes('/src/labs/');
const parts = path.split('/');
const filename = parts[parts.length - 1]; // e.g. "ia-button-story.ts"
const tag = filename.replace(/-story\.ts$/, '');
return { tag, storyTag: `${tag}-story`, id: `elem-${tag}`, labs };
})
.sort((a, b) => a.tag.localeCompare(b.tag));

const productionEntries = storyEntries.filter(e => !e.labs);
const labsEntries = storyEntries.filter(e => e.labs);
const ALL_ENTRIES = [...productionEntries, ...labsEntries];

@customElement('app-root')
export class AppRoot extends LitElement {
createRenderRoot() { return this; }

private _observer?: IntersectionObserver;
private _abortController = new AbortController();

render() {
return html`
<h1>🏛️ Internet Archive Elements ⚛️</h1>
<nav id="ia-sidebar">
<h2>Production-Ready</h2>
${productionEntries.map(e => html`<a href="#${e.id}">&lt;${e.tag}&gt;</a>`)}
<h2>Labs 🧪</h2>
${labsEntries.map(e => html`<a href="#${e.id}">&lt;${e.tag}&gt;</a>`)}
</nav>
<div id="ia-content">
<h1>Internet Archive Elements</h1>
<h2>Production-Ready Elements</h2>
${productionEntries.map(e => html`
<div id="${e.id}" class="ia-anchor">
${unsafeHTML(`<${e.storyTag}></${e.storyTag}>`)}
</div>
`)}
<h2>Labs Elements</h2>
${labsEntries.map(e => html`
<div id="${e.id}" class="ia-anchor">
${unsafeHTML(`<${e.storyTag}></${e.storyTag}>`)}
</div>
`)}
</div>
`;
}

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

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

<h2>🧪 Labs Elements</h2>
const visible = new Set<string>();

<ia-snow-story></ia-snow-story>
<ia-button-story></ia-button-story>
`;
// Only anchors in the top 30% of the viewport count as "active".
// The first (topmost) visible anchor wins.
this._observer = new IntersectionObserver(
entries => {
for (const entry of entries) {
if (entry.isIntersecting) visible.add(entry.target.id);
else visible.delete(entry.target.id);
}
const activeId = allIds.find(id => visible.has(id)) ?? allIds[0];
allIds.forEach(id => links[id]?.classList.toggle('active', id === activeId));
},
{ rootMargin: '0px 0px -70% 0px' },
);

allIds.forEach(id => {
const el = document.getElementById(id);
if (el) this._observer!.observe(el);
});

allIds.forEach(id => {
links[id]?.addEventListener('click', (e: Event) => {
e.preventDefault();
const el = document.getElementById(id);
if (el) {
const top = el.getBoundingClientRect().top + window.scrollY;
window.scrollTo({ top: Math.max(0, top - 16), behavior: 'smooth' });
}
}, { signal: this._abortController.signal });
});
}

disconnectedCallback() {
super.disconnectedCallback();
this._observer?.disconnect();
this._abortController.abort();
}
}
86 changes: 84 additions & 2 deletions demo/index.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;

Expand All @@ -24,7 +24,89 @@ a:hover {
}

body {
margin: 1rem;
margin: 0;
min-width: 320px;
min-height: 100vh;
display: flex;
}

app-root {
display: contents;
}

#ia-sidebar {
width: 200px;
flex-shrink: 0;
position: sticky;
top: 0;
height: 100vh;
border-right: 1px solid #ddd;
padding: 1rem 0;
box-sizing: border-box;
overflow-y: auto;
}

#ia-sidebar h2 {
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #767676;
padding: 0 1rem;
margin: 0 0 0.5rem;
}

#ia-sidebar a {
display: block;
font-size: 0.82rem;
font-weight: 400;
color: #444;
text-decoration: none;
padding: 0.3rem 1rem;
border-left: 3px solid transparent;
transition: background 0.1s, color 0.1s, border-color 0.1s;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

#ia-sidebar a:hover {
background: #f0f0f0;
color: #213547;
}

#ia-sidebar a.active {
color: #194880;
border-left-color: #194880;
background: #f0f4f9;
font-weight: 600;
}

#ia-content {
flex: 1;
padding: 0.75rem 1rem;
padding-bottom: 100vh; /* lets scroll-spy activate on the last element */
min-width: 0;
}

.ia-anchor {
scroll-margin-top: 16px;
}

.ia-anchor > * {
display: block;
margin-bottom: 0.5rem;
}

#ia-content h1 {
font-size: 1.4rem;
margin: 0 0 0.25rem;
display: flex;
align-items: center;
}

#ia-content h2 {
font-size: 1.1rem;
font-weight: 600;
margin: 0.75rem 0 0.25rem;
}
1 change: 0 additions & 1 deletion demo/story-components/story-prop-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ export class StoryPropsSettings extends LitElement {
if (!this.propInputData) return nothing;

return html`
<h3>Properties</h3>
<div class="settings-options">
<table>
${this.propInputData.settings.map(
Expand Down
1 change: 0 additions & 1 deletion demo/story-components/story-styles-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ export class StoryStylesSettings extends LitElement {
if (!this.styleInputData) return nothing;

return html`
<h3>Styles</h3>
<div class="settings-options">
<table>
${this.styleInputData.settings.map(
Expand Down
Loading
Loading