wc-mixin/examples/hero-banner.ts
Buddy 8865b49f4e Add examples page with a context manager
DSD, open/closed template, and light-DOM components that talk through
<main-context>: children register on… callbacks and share updates as events.
Served by Vite via npm run examples; examples/ is linted, formatted and
typechecked.
2026-11-10 09:59:09 -08:00

124 lines
3.0 KiB
TypeScript

import { WebComponent, choice, string, type PropChange } from '../src/index.ts';
import { THEMES } from './themes.ts';
import { register } from './main-context.ts';
const css = `
:host {
--a: #ff5f6d;
--b: #ffc371;
--c: #ff5f6d;
--glow: rgb(255 95 109 / 45%);
}
:host([theme='ocean']) {
--a: #2193b0;
--b: #6dd5ed;
--c: #b2fefa;
--glow: rgb(33 147 176 / 45%);
}
:host([theme='neon']) {
--a: #f72585;
--b: #7209b7;
--c: #4cc9f0;
--glow: rgb(114 9 183 / 55%);
}
.stage {
background:
radial-gradient(60% 80% at 15% 10%, color-mix(in srgb, var(--a) 35%, transparent), transparent),
radial-gradient(50% 70% at 85% 90%, color-mix(in srgb, var(--c) 30%, transparent), transparent),
#0d0d14;
box-shadow: 0 30px 80px -30px var(--glow);
transition: box-shadow 400ms ease;
}
h1 {
background: linear-gradient(100deg, var(--a), var(--b), var(--c), var(--a)) 0 0 / 300% 100%;
-webkit-background-clip: text;
background-clip: text;
color: transparent;
animation: flow 6s linear infinite;
filter: drop-shadow(0 4px 24px var(--glow));
}
.tagline {
animation: rise 700ms cubic-bezier(0.2, 0.8, 0.2, 1) both;
}
@keyframes flow {
to { background-position: 300% 0; }
}
@keyframes rise {
from { opacity: 0; transform: translateY(8px); }
}
@media (prefers-reduced-motion: reduce) {
h1, .tagline { animation: none; }
}
`;
/**
* 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;
}
}