wc-mixin/examples/secret-note.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

102 lines
2.9 KiB
TypeScript

import { WebComponent, enable, string, type PropChange } from '../src/index.ts';
import { register } from './main-context.ts';
/**
* Closed shadow root built from `template`: `el.shadowRoot` is `null` from the
* outside, but the component still reaches its own DOM through `this.root` and
* `this.refs`. Closed hides the internals, not the data; `message` is still a
* reflected attribute.
*/
const SPEC = {
shadow: 'closed',
template: `
<button type="button" aria-expanded="false">
<span class="lock" aria-hidden="true">🔒</span>
<span class="hint">Click to reveal</span>
</button>
<p class="message"></p>
`,
css: `
:host {
display: grid;
gap: 0.75rem;
padding: 1.25rem;
border-radius: 14px;
background: repeating-linear-gradient(-45deg, #1f1b2e 0 12px, #241f36 12px 24px);
color: #f0eaff;
font: 0.95rem/1.5 ui-monospace, 'SF Mono', Menlo, monospace;
}
button {
all: unset;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 0.5rem;
justify-self: start;
padding: 0.4rem 0.8rem;
border-radius: 999px;
background: #3b3355;
transition: background 150ms ease;
}
button:hover { background: #4a4170; }
button:focus-visible { outline: 2px solid #b69cff; outline-offset: 2px; }
.lock { transition: transform 300ms ease; }
.message {
margin: 0;
filter: blur(6px);
user-select: none;
transition: filter 400ms ease;
}
:host([revealed]) .message { filter: none; user-select: auto; }
:host([revealed]) .lock { transform: rotate(-20deg); }
`,
element: {
toggle: 'button',
hint: '.hint',
lock: '.lock',
message: 'p.message',
},
props: {
message: string({ default: '' }),
revealed: enable(),
},
} as const;
export class SecretNote extends WebComponent(SPEC) {
override onConnect(): void {
// Not `events: { click: true }`: a listener on the host can't see into a closed
// root (`composedPath()` stops at the host), so listen on the button itself.
this.refs.toggle?.addEventListener('click', this, { signal: this.signal });
this.#render();
register(this, {
onSecret: (secret) => {
this.message = secret;
},
});
}
override onPropChange(_change: PropChange<typeof SPEC>): void {
this.#render();
}
override handleEvent(): void {
this.revealed = !this.revealed;
}
#render(): void {
const { toggle, hint, lock, message } = this.refs;
if (message) message.textContent = this.message;
if (hint) hint.textContent = this.revealed ? 'Hide' : 'Click to reveal';
if (lock) lock.textContent = this.revealed ? '🔓' : '🔒';
toggle?.setAttribute('aria-expanded', String(this.revealed));
}
}
customElements.define('secret-note', SecretNote);
declare global {
interface HTMLElementTagNameMap {
'secret-note': SecretNote;
}
}