css is now string | CSSStyleSheet. A sheet is adopted as-is: into its own
shadow root, an enclosing shadow root, or document.adoptedStyleSheets for
light DOM at the document level. Strings behave as before.
The examples import their styles with `with { type: 'css' }` and their
static templates with `with { type: 'text' }`. Safari doesn't support those
yet, so vite.config.ts rewrites them into modules Vite builds: a constructed
CSSStyleSheet, or the file's text. npm run examples now just runs vite,
with root set in the config.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
72 lines
1.9 KiB
TypeScript
72 lines
1.9 KiB
TypeScript
import { WebComponent, choice, string, type PropChange } from '../src/index.ts';
|
|
import { THEMES } from './themes.ts';
|
|
import { register } from './main-context.ts';
|
|
import css from './hero-banner.css' with { type: 'css' };
|
|
|
|
/**
|
|
* Declarative shadow DOM: the shadow root and its layout `<style>` are written
|
|
* in index.html inside `<template shadowrootmode="open">`, so the banner paints
|
|
* before any script runs. The mixin finds that root and uses it (no `shadow` or
|
|
* `template` here), and still adopts `css` into it for the animated effects.
|
|
*/
|
|
const SPEC = {
|
|
css,
|
|
element: {
|
|
headlineText: 'h1',
|
|
taglineText: 'p.tagline',
|
|
},
|
|
props: {
|
|
headline: string(),
|
|
tagline: string(),
|
|
theme: choice(THEMES, { default: 'sunset' }),
|
|
},
|
|
} as const;
|
|
|
|
export class HeroBanner extends WebComponent(SPEC) {
|
|
override onConnect(): void {
|
|
register(this, {
|
|
onHeadline: (headline) => {
|
|
this.headline = headline;
|
|
},
|
|
onTagline: (tagline) => {
|
|
this.tagline = tagline;
|
|
},
|
|
onTheme: (theme) => {
|
|
this.theme = theme;
|
|
},
|
|
});
|
|
}
|
|
|
|
override onPropChange(change: PropChange<typeof SPEC>): void {
|
|
switch (change.name) {
|
|
case 'headline': {
|
|
if (this.refs.headlineText) this.refs.headlineText.textContent = change.value ?? '';
|
|
break;
|
|
}
|
|
case 'tagline': {
|
|
const el = this.refs.taglineText;
|
|
if (!el) break;
|
|
el.textContent = change.value ?? '';
|
|
// Restart the entrance animation so each edit visibly lands.
|
|
for (const animation of el.getAnimations()) {
|
|
animation.cancel();
|
|
animation.play();
|
|
}
|
|
break;
|
|
}
|
|
case 'theme': {
|
|
// `:host([theme])` does the work; the attribute is already reflected.
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
customElements.define('hero-banner', HeroBanner);
|
|
|
|
declare global {
|
|
interface HTMLElementTagNameMap {
|
|
'hero-banner': HeroBanner;
|
|
}
|
|
}
|