wc-mixin/examples/hero-banner.ts
Buddy 76d78e1925 Accept a CSSStyleSheet as css; move example CSS and templates into files
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>
2026-11-10 11:44:27 -08:00

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;
}
}