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.
This commit is contained in:
parent
be8ee0198f
commit
8865b49f4e
103
examples/color-swatch-picker.ts
Normal file
103
examples/color-swatch-picker.ts
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import { WebComponent, choice, string } from '../src/index.ts';
|
||||||
|
import { share } from './main-context.ts';
|
||||||
|
import { SWATCHES, THEMES } from './themes.ts';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Light DOM radio group. The radios are real light-DOM `<input>`s, so arrow
|
||||||
|
* keys, focus and form behaviour are native. A pick is shared with the nearest
|
||||||
|
* `<main-context>` as a `theme` update.
|
||||||
|
*/
|
||||||
|
const SPEC = {
|
||||||
|
template: `
|
||||||
|
<fieldset>
|
||||||
|
<legend></legend>
|
||||||
|
<div class="swatches">
|
||||||
|
${THEMES.map(
|
||||||
|
(theme) => `
|
||||||
|
<label class="swatch" style="--swatch: ${SWATCHES[theme]}">
|
||||||
|
<input type="radio" value="${theme}">
|
||||||
|
<span>${theme}</span>
|
||||||
|
</label>`,
|
||||||
|
).join('')}
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
`,
|
||||||
|
css: `
|
||||||
|
color-swatch-picker { display: block; font: 0.9rem/1.3 system-ui, sans-serif; }
|
||||||
|
color-swatch-picker fieldset { border: 0; margin: 0; padding: 0; }
|
||||||
|
color-swatch-picker legend { font-weight: 600; padding: 0; margin-bottom: 0.35rem; }
|
||||||
|
color-swatch-picker .swatches { display: flex; flex-wrap: wrap; gap: 0.5rem; }
|
||||||
|
color-swatch-picker .swatch {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
place-items: end center;
|
||||||
|
width: 5.5rem;
|
||||||
|
height: 3.5rem;
|
||||||
|
padding-bottom: 0.3rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--swatch);
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: capitalize;
|
||||||
|
text-shadow: 0 1px 2px rgb(0 0 0 / 35%);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 150ms ease, box-shadow 150ms ease;
|
||||||
|
}
|
||||||
|
color-swatch-picker .swatch:hover { transform: translateY(-2px); }
|
||||||
|
color-swatch-picker .swatch input { position: absolute; opacity: 0; pointer-events: none; }
|
||||||
|
color-swatch-picker .swatch:has(:checked) {
|
||||||
|
box-shadow: 0 0 0 3px var(--page, #fff), 0 0 0 5px var(--ink, #1d1d29);
|
||||||
|
}
|
||||||
|
color-swatch-picker .swatch:has(:focus-visible) {
|
||||||
|
outline: 2px solid #7c5cff;
|
||||||
|
outline-offset: 6px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
events: {
|
||||||
|
change: true,
|
||||||
|
},
|
||||||
|
element: { legend: 'legend' },
|
||||||
|
elements: { radios: 'input[type=radio]' },
|
||||||
|
props: {
|
||||||
|
label: string({ default: 'Theme' }),
|
||||||
|
value: choice(THEMES, { default: 'sunset' }),
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// Each picker needs its own radio group name.
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
export class ColorSwatchPicker extends WebComponent(SPEC) {
|
||||||
|
readonly #name = `color-swatch-picker-${(count += 1)}`;
|
||||||
|
|
||||||
|
override onConnect(): void {
|
||||||
|
for (const radio of this.refs.radios) radio.name = this.#name;
|
||||||
|
this.#render();
|
||||||
|
}
|
||||||
|
|
||||||
|
override onPropChange(): void {
|
||||||
|
this.#render();
|
||||||
|
}
|
||||||
|
|
||||||
|
override handleEvent(event: Event): void {
|
||||||
|
const radio = this.refs.radios.find((input) => input === event.target);
|
||||||
|
const theme = THEMES.find((name) => name === radio?.value);
|
||||||
|
if (theme && this.value !== theme) {
|
||||||
|
this.value = theme;
|
||||||
|
share(this, 'theme', theme);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#render(): void {
|
||||||
|
if (this.refs.legend) this.refs.legend.textContent = this.label;
|
||||||
|
for (const radio of this.refs.radios) radio.checked = radio.value === this.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('color-swatch-picker', ColorSwatchPicker);
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'color-swatch-picker': ColorSwatchPicker;
|
||||||
|
}
|
||||||
|
}
|
||||||
9
examples/dispatch.ts
Normal file
9
examples/dispatch.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
/** Dispatch a `CustomEvent` from `el`. It bubbles by default so an ancestor can catch it. */
|
||||||
|
export function dispatch<T = unknown>(
|
||||||
|
el: HTMLElement,
|
||||||
|
name: string,
|
||||||
|
detail: T,
|
||||||
|
init: Omit<CustomEventInit<T>, 'detail'> = {},
|
||||||
|
): void {
|
||||||
|
el.dispatchEvent(new CustomEvent(name, { bubbles: true, ...init, detail }));
|
||||||
|
}
|
||||||
123
examples/hero-banner.ts
Normal file
123
examples/hero-banner.ts
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
137
examples/index.html
Normal file
137
examples/index.html
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>wc-mixin examples</title>
|
||||||
|
<link rel="stylesheet" href="./style.css" />
|
||||||
|
<script type="module" src="./main.ts"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<!--
|
||||||
|
The context manager: siblings below register callbacks with it and
|
||||||
|
share updates through it, instead of reaching for each other.
|
||||||
|
-->
|
||||||
|
<main-context>
|
||||||
|
<!--
|
||||||
|
Declarative shadow DOM: the parser attaches this shadow root, so the
|
||||||
|
banner renders (with its layout styles) before any JS loads. The
|
||||||
|
component's `css` adds the gradient animation once it upgrades, and its
|
||||||
|
props take over the text from here.
|
||||||
|
-->
|
||||||
|
<hero-banner id="banner" theme="sunset">
|
||||||
|
<template shadowrootmode="open">
|
||||||
|
<style>
|
||||||
|
:host { display: block; }
|
||||||
|
.stage {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: clamp(2rem, 6vw, 4.5rem) clamp(1.25rem, 5vw, 3.5rem);
|
||||||
|
border-radius: 24px;
|
||||||
|
background: #0d0d14;
|
||||||
|
color: #f4f4fb;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.eyebrow {
|
||||||
|
margin: 0;
|
||||||
|
font: 600 0.75rem/1 system-ui, sans-serif;
|
||||||
|
letter-spacing: 0.2em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
margin: 0;
|
||||||
|
font: 800 clamp(2.25rem, 8vw, 5rem)/1.02 system-ui, sans-serif;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
.tagline { margin: 0; max-width: 36rem; font-size: 1.1rem; opacity: 0.8; }
|
||||||
|
</style>
|
||||||
|
<div class="stage">
|
||||||
|
<p class="eyebrow">Declarative shadow DOM</p>
|
||||||
|
<h1>Hello, wc-mixin</h1>
|
||||||
|
<p class="tagline">Typed props, refs and styles without the boilerplate.</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</hero-banner>
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
|
<section class="card">
|
||||||
|
<h2>Controls <span class="kind">light DOM</span></h2>
|
||||||
|
<p>
|
||||||
|
<code><labeled-field></code> and <code><color-swatch-picker></code> render
|
||||||
|
into the light DOM; their CSS lives in a <code><style></code> in
|
||||||
|
<code><head></code>. They never touch the other components: each edit is
|
||||||
|
shared with <code><main-context></code>, which calls the
|
||||||
|
<code>on…</code> callbacks the other components registered.
|
||||||
|
</p>
|
||||||
|
<div class="controls">
|
||||||
|
<labeled-field
|
||||||
|
id="headline"
|
||||||
|
name="headline"
|
||||||
|
label="Headline"
|
||||||
|
value="Hello, wc-mixin"
|
||||||
|
required
|
||||||
|
hint="Shared as headline → hero-banner"
|
||||||
|
></labeled-field>
|
||||||
|
<labeled-field
|
||||||
|
id="tagline"
|
||||||
|
name="tagline"
|
||||||
|
label="Tagline"
|
||||||
|
value="Typed props, refs and styles without the boilerplate."
|
||||||
|
hint="Shared as tagline → hero-banner"
|
||||||
|
></labeled-field>
|
||||||
|
<color-swatch-picker id="theme" label="Banner theme"></color-swatch-picker>
|
||||||
|
<labeled-field
|
||||||
|
id="stars"
|
||||||
|
name="stars"
|
||||||
|
label="Number of stars"
|
||||||
|
type="number"
|
||||||
|
value="5"
|
||||||
|
required
|
||||||
|
hint="Shared as stars → star-rating (1–10)"
|
||||||
|
></labeled-field>
|
||||||
|
<labeled-field
|
||||||
|
id="secret"
|
||||||
|
name="secret"
|
||||||
|
label="Secret message"
|
||||||
|
value="The cake is in the closed shadow root."
|
||||||
|
placeholder="Something hidden…"
|
||||||
|
hint="Shared as secret → secret-note"
|
||||||
|
></labeled-field>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="controls">
|
||||||
|
<section class="card">
|
||||||
|
<h2><star-rating> <span class="kind">template · open shadow</span></h2>
|
||||||
|
<p>Click or hover a star. Clicking the current value clears it.</p>
|
||||||
|
<star-rating id="rating" value="3">Rate this mixin</star-rating>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<h2><secret-note> <span class="kind">template · closed shadow</span></h2>
|
||||||
|
<p>The page can't reach inside, but the component can, through <code>this.root</code>.</p>
|
||||||
|
<secret-note id="note" message="The cake is in the closed shadow root."></secret-note>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<h2>From the outside</h2>
|
||||||
|
<output id="probe"></output>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<div class="log-head">
|
||||||
|
<h2>Attribute log</h2>
|
||||||
|
<button id="clear-log" type="button">Clear</button>
|
||||||
|
</div>
|
||||||
|
<p>Each prop change reflects to an attribute, and this list records them as mutations.</p>
|
||||||
|
<ol id="log"></ol>
|
||||||
|
</section>
|
||||||
|
</main-context>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
137
examples/labeled-field.ts
Normal file
137
examples/labeled-field.ts
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
import { WebComponent, choice, enable, string, type PropChange } from '../src/index.ts';
|
||||||
|
import { dispatch } from './dispatch.ts';
|
||||||
|
import type { FieldInput } from './main-context.ts';
|
||||||
|
|
||||||
|
const css = `
|
||||||
|
labeled-field {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.35rem;
|
||||||
|
font: 0.9rem/1.3 system-ui, sans-serif;
|
||||||
|
}
|
||||||
|
labeled-field label { display: grid; gap: 0.35rem; }
|
||||||
|
labeled-field .field-label { font-weight: 600; color: var(--ink, #1d1d29); }
|
||||||
|
labeled-field[required] .field-label::after { content: ' *'; color: #e5484d; }
|
||||||
|
labeled-field input {
|
||||||
|
font: inherit;
|
||||||
|
padding: 0.6rem 0.75rem;
|
||||||
|
border: 1.5px solid var(--line, #d9d9e3);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--field, #fff);
|
||||||
|
color: inherit;
|
||||||
|
transition: border-color 150ms ease, box-shadow 150ms ease;
|
||||||
|
}
|
||||||
|
labeled-field input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #7c5cff;
|
||||||
|
box-shadow: 0 0 0 4px rgb(124 92 255 / 18%);
|
||||||
|
}
|
||||||
|
labeled-field .field-hint { color: var(--muted, #6b6b7b); min-height: 1.3em; }
|
||||||
|
labeled-field:state(invalid) input { border-color: #e5484d; }
|
||||||
|
labeled-field:state(invalid) input:focus { box-shadow: 0 0 0 4px rgb(229 72 77 / 18%); }
|
||||||
|
labeled-field:state(invalid) .field-hint { color: #e5484d; }
|
||||||
|
`;
|
||||||
|
|
||||||
|
const template = `
|
||||||
|
<label>
|
||||||
|
<span class="field-label"></span>
|
||||||
|
<input>
|
||||||
|
</label>
|
||||||
|
<small class="field-hint"></small>
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Light DOM: `template` is appended into the element on first connect, so the
|
||||||
|
* `<input>` is a real child of the page and works with page CSS, autofill and
|
||||||
|
* `document.querySelector`. `css` goes into `<head>` once for every instance.
|
||||||
|
* The native `input` event bubbles out of the inner `<input>` to the host, which
|
||||||
|
* reports it to `<main-context>` as a `wc:field-input` keyed by `name`.
|
||||||
|
*/
|
||||||
|
const SPEC = {
|
||||||
|
template,
|
||||||
|
css,
|
||||||
|
events: {
|
||||||
|
input: true,
|
||||||
|
},
|
||||||
|
element: { label: '.field-label', input: 'input', hint: '.field-hint' },
|
||||||
|
props: {
|
||||||
|
name: string({ default: '' }),
|
||||||
|
label: string({ default: '' }),
|
||||||
|
value: string({ default: '' }),
|
||||||
|
placeholder: string(),
|
||||||
|
hint: string(),
|
||||||
|
type: choice(['text', 'number', 'email'], { default: 'text' }),
|
||||||
|
required: enable(),
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export class LabeledField extends WebComponent(SPEC) {
|
||||||
|
override onConnect(): void {
|
||||||
|
const { label, input, hint } = this.refs;
|
||||||
|
if (!label || !input || !hint) return;
|
||||||
|
label.textContent = this.label;
|
||||||
|
input.type = this.type;
|
||||||
|
input.value = this.value;
|
||||||
|
input.required = this.required;
|
||||||
|
if (this.placeholder !== undefined) input.placeholder = this.placeholder;
|
||||||
|
this.#validate();
|
||||||
|
}
|
||||||
|
|
||||||
|
override onPropChange(change: PropChange<typeof SPEC>): void {
|
||||||
|
// Attributes are decoded at upgrade, before the template is appended.
|
||||||
|
const { label, input } = this.refs;
|
||||||
|
if (!label || !input) return;
|
||||||
|
switch (change.name) {
|
||||||
|
case 'label': {
|
||||||
|
label.textContent = change.value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'value': {
|
||||||
|
if (input.value !== change.value) input.value = change.value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'placeholder': {
|
||||||
|
input.placeholder = change.value ?? '';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'type': {
|
||||||
|
input.type = change.value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'required': {
|
||||||
|
input.required = change.value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'name':
|
||||||
|
case 'hint': {
|
||||||
|
// `name` is read on input; `hint` is shown by #validate below.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.#validate();
|
||||||
|
}
|
||||||
|
|
||||||
|
override handleEvent(): void {
|
||||||
|
if (this.refs.input) {
|
||||||
|
this.value = this.refs.input.value;
|
||||||
|
}
|
||||||
|
dispatch<FieldInput>(this, 'wc:field-input', { name: this.name, value: this.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
// `internals.states` drives the `:state(invalid)` selectors above.
|
||||||
|
#validate(): void {
|
||||||
|
const { input, hint } = this.refs;
|
||||||
|
if (!input || !hint) return;
|
||||||
|
const valid = input.checkValidity();
|
||||||
|
if (valid) this.internals.states.delete('invalid');
|
||||||
|
else this.internals.states.add('invalid');
|
||||||
|
hint.textContent = valid ? (this.hint ?? '') : input.validationMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('labeled-field', LabeledField);
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'labeled-field': LabeledField;
|
||||||
|
}
|
||||||
|
}
|
||||||
142
examples/main-context.ts
Normal file
142
examples/main-context.ts
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
import { WebComponent, custom, type HandledEvent } from '../src/index.ts';
|
||||||
|
import { dispatch } from './dispatch.ts';
|
||||||
|
import type { Theme } from './themes.ts';
|
||||||
|
|
||||||
|
/** Every update components can share through `<main-context>`: name → value. */
|
||||||
|
export interface Updates {
|
||||||
|
headline: string;
|
||||||
|
tagline: string;
|
||||||
|
theme: Theme;
|
||||||
|
secret: string;
|
||||||
|
stars: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** `onHeadline`, `onTheme`, …: each is called with that update's value. */
|
||||||
|
export type Callbacks = {
|
||||||
|
readonly [K in keyof Updates as `on${Capitalize<K>}`]?: (value: Updates[K]) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** The `wc:register` detail. The context drops the callbacks when `signal` aborts. */
|
||||||
|
export type Registration = Callbacks & { readonly signal: AbortSignal };
|
||||||
|
|
||||||
|
/** A `<labeled-field>`'s raw input; the context turns it into an update by `name`. */
|
||||||
|
export interface FieldInput {
|
||||||
|
readonly name: string;
|
||||||
|
readonly value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ask the nearest `<main-context>` to call `callbacks` on updates. Call it from
|
||||||
|
* `onConnect`: the element's `signal` unregisters it on disconnect.
|
||||||
|
*/
|
||||||
|
export function register(
|
||||||
|
el: HTMLElement & { readonly signal: AbortSignal },
|
||||||
|
callbacks: Callbacks,
|
||||||
|
): void {
|
||||||
|
dispatch<Registration>(el, 'wc:register', { ...callbacks, signal: el.signal });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Share an update; the nearest `<main-context>` calls every matching `on…` callback. */
|
||||||
|
export function share<K extends keyof Updates>(el: HTMLElement, name: K, value: Updates[K]): void {
|
||||||
|
dispatch(el, `wc:${name}`, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The shared ancestor siblings talk through. Children register callbacks with
|
||||||
|
* a `wc:register` event and share updates with `wc:<name>` events; both bubble
|
||||||
|
* up to here, and an update is handed to every registered `on<Name>`. Neither
|
||||||
|
* side knows about the other, only about this element.
|
||||||
|
*
|
||||||
|
* It must be defined before the elements that register, or their
|
||||||
|
* `wc:register` fires before anything is listening.
|
||||||
|
*/
|
||||||
|
const SPEC = {
|
||||||
|
// Light DOM with no template: a pure wrapper that doesn't affect layout.
|
||||||
|
css: 'main-context { display: contents; }',
|
||||||
|
events: {
|
||||||
|
'wc:register': custom<Registration>(),
|
||||||
|
'wc:field-input': custom<FieldInput>(),
|
||||||
|
'wc:headline': custom<Updates['headline']>(),
|
||||||
|
'wc:tagline': custom<Updates['tagline']>(),
|
||||||
|
'wc:theme': custom<Updates['theme']>(),
|
||||||
|
'wc:secret': custom<Updates['secret']>(),
|
||||||
|
'wc:stars': custom<Updates['stars']>(),
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export class MainContext extends WebComponent(SPEC) {
|
||||||
|
readonly #registered = new Set<Registration>();
|
||||||
|
|
||||||
|
override handleEvent(event: HandledEvent<typeof SPEC>): void {
|
||||||
|
// The nearest context owns the event; an outer one never sees it.
|
||||||
|
event.stopPropagation();
|
||||||
|
switch (event.type) {
|
||||||
|
case 'wc:register': {
|
||||||
|
this.#register(event.detail);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'wc:field-input': {
|
||||||
|
this.#fieldInput(event.detail);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'wc:headline': {
|
||||||
|
for (const r of this.#registered) r.onHeadline?.(event.detail);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'wc:tagline': {
|
||||||
|
for (const r of this.#registered) r.onTagline?.(event.detail);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'wc:theme': {
|
||||||
|
for (const r of this.#registered) r.onTheme?.(event.detail);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'wc:secret': {
|
||||||
|
for (const r of this.#registered) r.onSecret?.(event.detail);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'wc:stars': {
|
||||||
|
for (const r of this.#registered) r.onStars?.(event.detail);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#register(registration: Registration): void {
|
||||||
|
this.#registered.add(registration);
|
||||||
|
registration.signal.addEventListener(
|
||||||
|
'abort',
|
||||||
|
() => {
|
||||||
|
this.#registered.delete(registration);
|
||||||
|
},
|
||||||
|
{ once: true, signal: this.signal },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fields are generic inputs, so the context decides what each one means:
|
||||||
|
// it re-shares them as typed updates from itself, which comes back through
|
||||||
|
// `handleEvent` like any other update.
|
||||||
|
#fieldInput({ name, value }: FieldInput): void {
|
||||||
|
switch (name) {
|
||||||
|
case 'headline':
|
||||||
|
case 'tagline':
|
||||||
|
case 'secret': {
|
||||||
|
share(this, name, value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'stars': {
|
||||||
|
const stars = Number(value);
|
||||||
|
if (Number.isInteger(stars) && stars >= 1 && stars <= 10) share(this, 'stars', stars);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('main-context', MainContext);
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'main-context': MainContext;
|
||||||
|
}
|
||||||
|
}
|
||||||
68
examples/main.ts
Normal file
68
examples/main.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
// The context first: elements that register with it on connect need it to be
|
||||||
|
// listening already when they upgrade.
|
||||||
|
import './main-context.ts';
|
||||||
|
import './color-swatch-picker.ts';
|
||||||
|
import './hero-banner.ts';
|
||||||
|
import './labeled-field.ts';
|
||||||
|
import './secret-note.ts';
|
||||||
|
import './star-rating.ts';
|
||||||
|
|
||||||
|
// Components talk only through <main-context>; this file just reports on them.
|
||||||
|
|
||||||
|
function $<K extends keyof HTMLElementTagNameMap>(tag: K, id: string): HTMLElementTagNameMap[K] {
|
||||||
|
const el = document.querySelector(`${tag}#${id}`);
|
||||||
|
if (!el) throw new Error(`missing <${tag} id="${id}">`);
|
||||||
|
// oxlint-disable-next-line no-unsafe-type-assertion -- the tag in the selector guarantees it
|
||||||
|
return el as HTMLElementTagNameMap[K];
|
||||||
|
}
|
||||||
|
|
||||||
|
const rating = $('star-rating', 'rating');
|
||||||
|
const note = $('secret-note', 'note');
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Closed vs open: what page code can see from outside.
|
||||||
|
|
||||||
|
const probe = $('output', 'probe');
|
||||||
|
function updateProbe(): void {
|
||||||
|
probe.textContent = [
|
||||||
|
`rating.shadowRoot → ${rating.shadowRoot ? 'ShadowRoot (open)' : 'null'}`,
|
||||||
|
`note.shadowRoot → ${note.shadowRoot ? 'ShadowRoot' : 'null (closed)'}`,
|
||||||
|
`note.revealed → ${note.revealed}`,
|
||||||
|
`note.querySelector('p') → ${note.querySelector('p') ? '<p>' : 'null'}`,
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
updateProbe();
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Attribute log: every reflected prop shows up here as an attribute mutation.
|
||||||
|
|
||||||
|
const log = $('ol', 'log');
|
||||||
|
|
||||||
|
function show(value: string | null): string {
|
||||||
|
return value === null ? '∅' : JSON.stringify(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new MutationObserver((records) => {
|
||||||
|
for (const { target, attributeName, oldValue } of records) {
|
||||||
|
if (!(target instanceof Element) || attributeName === null) continue;
|
||||||
|
const element = document.createElement('code');
|
||||||
|
element.textContent = `<${target.localName}#${target.id}>`;
|
||||||
|
const name = document.createElement('b');
|
||||||
|
name.textContent = attributeName;
|
||||||
|
const change = `${show(oldValue)} → ${show(target.getAttribute(attributeName))}`;
|
||||||
|
const item = document.createElement('li');
|
||||||
|
item.append(element, ' ', name, ' ', change);
|
||||||
|
log.prepend(item);
|
||||||
|
while (log.children.length > 40) log.lastElementChild?.remove();
|
||||||
|
}
|
||||||
|
updateProbe();
|
||||||
|
});
|
||||||
|
for (const el of document.querySelectorAll('main-context [id]')) {
|
||||||
|
if (el.localName.includes('-')) {
|
||||||
|
observer.observe(el, { attributes: true, attributeOldValue: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$('button', 'clear-log').addEventListener('click', () => {
|
||||||
|
log.replaceChildren();
|
||||||
|
});
|
||||||
101
examples/secret-note.ts
Normal file
101
examples/secret-note.ts
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
127
examples/star-rating.ts
Normal file
127
examples/star-rating.ts
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import { WebComponent, enable, number } from '../src/index.ts';
|
||||||
|
import { register } from './main-context.ts';
|
||||||
|
|
||||||
|
const css = `
|
||||||
|
:host {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
font: 500 0.95rem/1 system-ui, sans-serif;
|
||||||
|
color: var(--ink, #1d1d29);
|
||||||
|
}
|
||||||
|
.stars { display: inline-flex; gap: 0.15rem; }
|
||||||
|
button {
|
||||||
|
all: unset;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.75rem;
|
||||||
|
line-height: 1;
|
||||||
|
color: #d4d4dc;
|
||||||
|
transition: color 120ms ease, transform 120ms ease;
|
||||||
|
}
|
||||||
|
button:focus-visible { outline: 2px solid #f5a524; outline-offset: 2px; border-radius: 4px; }
|
||||||
|
button.on { color: #f5a524; }
|
||||||
|
button.preview { color: #f8c96b; }
|
||||||
|
:host(:not([readonly])) button:hover { transform: scale(1.2); }
|
||||||
|
:host([readonly]) button { cursor: default; }
|
||||||
|
output {
|
||||||
|
min-width: 3.5em;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
color: var(--muted, #6b6b7b);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open shadow root built from `template`. The stars are rebuilt whenever `max`
|
||||||
|
* changes; `value` just toggles classes. Clicks and hovers inside the shadow
|
||||||
|
* root bubble to the host, where `events` routes them to `handleEvent`.
|
||||||
|
*/
|
||||||
|
const SPEC = {
|
||||||
|
shadow: 'open',
|
||||||
|
css,
|
||||||
|
template: `
|
||||||
|
<span class="label"><slot>Rating</slot></span>
|
||||||
|
<div class="stars" role="radiogroup"></div>
|
||||||
|
<output>0 / 5</output>
|
||||||
|
`,
|
||||||
|
events: { click: true, pointerover: true, pointerleave: true },
|
||||||
|
element: { group: ['.stars', HTMLDivElement], output: 'output' },
|
||||||
|
elements: { stars: 'button.star' },
|
||||||
|
props: {
|
||||||
|
value: number({ default: 0 }),
|
||||||
|
max: number({ default: 5 }),
|
||||||
|
readonly: enable(),
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export class StarRating extends WebComponent(SPEC) {
|
||||||
|
override onConnect(): void {
|
||||||
|
register(this, {
|
||||||
|
onStars: (stars) => {
|
||||||
|
this.max = stars;
|
||||||
|
this.value = Math.min(this.value, stars);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.#build();
|
||||||
|
}
|
||||||
|
|
||||||
|
override onPropChange(): void {
|
||||||
|
this.#build();
|
||||||
|
}
|
||||||
|
|
||||||
|
override handleEvent(event: PointerEvent): void {
|
||||||
|
if (event.type === 'pointerleave') {
|
||||||
|
this.#paint(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// A host listener sees into an open root; a closed one would hide the button.
|
||||||
|
const star = this.refs.stars.find((button) => event.composedPath().includes(button));
|
||||||
|
if (!star || this.readonly) return;
|
||||||
|
const n = Number(star.dataset['n']);
|
||||||
|
if (event.type === 'pointerover') {
|
||||||
|
this.#paint(n);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.value = n === this.value ? 0 : n;
|
||||||
|
this.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
|
}
|
||||||
|
|
||||||
|
#build(): void {
|
||||||
|
const { group } = this.refs;
|
||||||
|
if (!group) return;
|
||||||
|
const max = Math.max(1, Math.trunc(this.max) || 1);
|
||||||
|
if (this.refs.stars.length !== max) {
|
||||||
|
group.replaceChildren(
|
||||||
|
...Array.from({ length: max }, (_, i) => {
|
||||||
|
const button = document.createElement('button');
|
||||||
|
button.className = 'star';
|
||||||
|
button.dataset['n'] = String(i + 1);
|
||||||
|
button.textContent = '★';
|
||||||
|
button.setAttribute('role', 'radio');
|
||||||
|
button.setAttribute('aria-label', `${i + 1} star${i === 0 ? '' : 's'}`);
|
||||||
|
return button;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.#paint(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// `preview` is the hovered star (0 for none); `value` is the committed one.
|
||||||
|
#paint(preview: number): void {
|
||||||
|
for (const star of this.refs.stars) {
|
||||||
|
const n = Number(star.dataset['n']);
|
||||||
|
star.classList.toggle('on', n <= this.value);
|
||||||
|
star.classList.toggle('preview', n > this.value && n <= preview);
|
||||||
|
star.setAttribute('aria-checked', String(n === this.value));
|
||||||
|
star.disabled = this.readonly;
|
||||||
|
}
|
||||||
|
if (this.refs.output) this.refs.output.value = `${this.value} / ${this.refs.stars.length}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('star-rating', StarRating);
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'star-rating': StarRating;
|
||||||
|
}
|
||||||
|
}
|
||||||
82
examples/style.css
Normal file
82
examples/style.css
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
:root {
|
||||||
|
--page: #f6f5fb;
|
||||||
|
--card: #ffffff;
|
||||||
|
--ink: #1d1d29;
|
||||||
|
--muted: #6b6b7b;
|
||||||
|
--line: #e2e1ec;
|
||||||
|
--field: #ffffff;
|
||||||
|
color-scheme: light;
|
||||||
|
}
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--page: #111118;
|
||||||
|
--card: #1a1a24;
|
||||||
|
--ink: #ececf4;
|
||||||
|
--muted: #9c9cb0;
|
||||||
|
--line: #30303f;
|
||||||
|
--field: #13131b;
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 2rem 1rem 4rem;
|
||||||
|
background: var(--page);
|
||||||
|
color: var(--ink);
|
||||||
|
font: 1rem/1.55 system-ui, sans-serif;
|
||||||
|
}
|
||||||
|
main { max-width: 64rem; margin: 0 auto; display: grid; gap: 1.5rem; }
|
||||||
|
h2 { margin: 0 0 0.25rem; font-size: 1.1rem; }
|
||||||
|
.kind {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 0.4rem;
|
||||||
|
padding: 0.1rem 0.5rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: color-mix(in srgb, #7c5cff 14%, transparent);
|
||||||
|
color: #7c5cff;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 600;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
padding: 1.5rem;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 18px;
|
||||||
|
background: var(--card);
|
||||||
|
}
|
||||||
|
.card > p { margin: 0 0 1rem; color: var(--muted); font-size: 0.9rem; }
|
||||||
|
.grid { display: grid; gap: 1.5rem; grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr)); }
|
||||||
|
.controls { display: grid; gap: 1rem; }
|
||||||
|
pre, output#probe {
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.9rem 1rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--page);
|
||||||
|
font: 0.8rem/1.6 ui-monospace, 'SF Mono', Menlo, monospace;
|
||||||
|
white-space: pre;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
#log {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
max-height: 18rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
font: 0.8rem/1.7 ui-monospace, 'SF Mono', Menlo, monospace;
|
||||||
|
}
|
||||||
|
#log li { padding: 0.15rem 0; border-bottom: 1px dashed var(--line); }
|
||||||
|
#log code { color: var(--muted); }
|
||||||
|
#log b { color: #7c5cff; }
|
||||||
|
.log-head { display: flex; justify-content: space-between; align-items: baseline; }
|
||||||
|
.log-head button {
|
||||||
|
font: inherit;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
padding: 0.2rem 0.7rem;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: none;
|
||||||
|
color: var(--muted);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
11
examples/themes.ts
Normal file
11
examples/themes.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
/** Shared by `<hero-banner theme>` and `<color-swatch-picker>` so the two stay in step. */
|
||||||
|
export const THEMES = ['sunset', 'ocean', 'neon'] as const;
|
||||||
|
|
||||||
|
export type Theme = (typeof THEMES)[number];
|
||||||
|
|
||||||
|
/** Swatch colours for the picker; the banner defines its own gradients in CSS. */
|
||||||
|
export const SWATCHES: Readonly<Record<Theme, string>> = {
|
||||||
|
sunset: 'linear-gradient(135deg, #ff5f6d, #ffc371)',
|
||||||
|
ocean: 'linear-gradient(135deg, #2193b0, #6dd5ed)',
|
||||||
|
neon: 'linear-gradient(135deg, #f72585, #7209b7, #4cc9f0)',
|
||||||
|
};
|
||||||
@ -49,6 +49,18 @@ export default defineConfig({
|
|||||||
'eslint/max-lines': 'off',
|
'eslint/max-lines': 'off',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
files: ['examples/**/*.ts'],
|
||||||
|
rules: {
|
||||||
|
// Examples import the library from `../src`, like the tests.
|
||||||
|
'import/no-relative-parent-imports': 'off',
|
||||||
|
// Importing a component module is how it gets registered.
|
||||||
|
'import/no-unassigned-import': 'off',
|
||||||
|
// `onPropChange` switches are exhaustive over prop names, which
|
||||||
|
// `switch-exhaustiveness-check` enforces; a `default` would hide a missed one.
|
||||||
|
'eslint/default-case': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
env: {
|
env: {
|
||||||
builtin: true,
|
builtin: true,
|
||||||
|
|||||||
42
package-lock.json
generated
42
package-lock.json
generated
@ -15,6 +15,7 @@
|
|||||||
"oxlint-tsgolint": "^7.0.2006",
|
"oxlint-tsgolint": "^7.0.2006",
|
||||||
"playwright": "^1.65.0",
|
"playwright": "^1.65.0",
|
||||||
"typescript": "^7.0.2",
|
"typescript": "^7.0.2",
|
||||||
|
"vite": "^8.3.6",
|
||||||
"vitest": "^5.1.0"
|
"vitest": "^5.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -59,7 +60,6 @@
|
|||||||
"integrity": "sha512-1Duyg48TdJNdyhEB8Xn1L36bT9Pv6GUtlKjHxOys8xkaF/rQq3XJE+bAurSK/AgIz5mGFsb77cn9Pb20+WCScw==",
|
"integrity": "sha512-1Duyg48TdJNdyhEB8Xn1L36bT9Pv6GUtlKjHxOys8xkaF/rQq3XJE+bAurSK/AgIz5mGFsb77cn9Pb20+WCScw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/oxc-project"
|
"url": "https://github.com/sponsors/oxc-project"
|
||||||
}
|
}
|
||||||
@ -862,7 +862,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"android"
|
"android"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
}
|
}
|
||||||
@ -880,7 +879,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"android"
|
"android"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
}
|
}
|
||||||
@ -898,7 +896,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
}
|
}
|
||||||
@ -916,7 +913,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
}
|
}
|
||||||
@ -934,7 +930,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"freebsd"
|
"freebsd"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
}
|
}
|
||||||
@ -952,7 +947,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
}
|
}
|
||||||
@ -973,7 +967,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
}
|
}
|
||||||
@ -994,7 +987,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
}
|
}
|
||||||
@ -1015,7 +1007,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
}
|
}
|
||||||
@ -1036,7 +1027,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
}
|
}
|
||||||
@ -1057,7 +1047,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
}
|
}
|
||||||
@ -1078,7 +1067,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
}
|
}
|
||||||
@ -1096,7 +1084,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"openharmony"
|
"openharmony"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
}
|
}
|
||||||
@ -1114,7 +1101,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
}
|
}
|
||||||
@ -1132,7 +1118,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
}
|
}
|
||||||
@ -1142,8 +1127,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
|
||||||
"integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
|
"integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/@types/chai": {
|
"node_modules/@types/chai": {
|
||||||
"version": "5.2.3",
|
"version": "5.2.3",
|
||||||
@ -1678,7 +1662,6 @@
|
|||||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
@ -1753,7 +1736,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
}
|
}
|
||||||
@ -1764,7 +1746,6 @@
|
|||||||
"integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==",
|
"integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"detect-libc": "^2.0.3"
|
"detect-libc": "^2.0.3"
|
||||||
},
|
},
|
||||||
@ -1802,7 +1783,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"android"
|
"android"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12.0.0"
|
"node": ">= 12.0.0"
|
||||||
},
|
},
|
||||||
@ -1824,7 +1804,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12.0.0"
|
"node": ">= 12.0.0"
|
||||||
},
|
},
|
||||||
@ -1846,7 +1825,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12.0.0"
|
"node": ">= 12.0.0"
|
||||||
},
|
},
|
||||||
@ -1868,7 +1846,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"freebsd"
|
"freebsd"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12.0.0"
|
"node": ">= 12.0.0"
|
||||||
},
|
},
|
||||||
@ -1890,7 +1867,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12.0.0"
|
"node": ">= 12.0.0"
|
||||||
},
|
},
|
||||||
@ -1915,7 +1891,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12.0.0"
|
"node": ">= 12.0.0"
|
||||||
},
|
},
|
||||||
@ -1940,7 +1915,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12.0.0"
|
"node": ">= 12.0.0"
|
||||||
},
|
},
|
||||||
@ -1965,7 +1939,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12.0.0"
|
"node": ">= 12.0.0"
|
||||||
},
|
},
|
||||||
@ -1990,7 +1963,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12.0.0"
|
"node": ">= 12.0.0"
|
||||||
},
|
},
|
||||||
@ -2012,7 +1984,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12.0.0"
|
"node": ">= 12.0.0"
|
||||||
},
|
},
|
||||||
@ -2034,7 +2005,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12.0.0"
|
"node": ">= 12.0.0"
|
||||||
},
|
},
|
||||||
@ -2075,7 +2045,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"nanoid": "bin/nanoid.cjs"
|
"nanoid": "bin/nanoid.cjs"
|
||||||
},
|
},
|
||||||
@ -2232,8 +2201,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/picomatch": {
|
"node_modules/picomatch": {
|
||||||
"version": "4.0.7",
|
"version": "4.0.7",
|
||||||
@ -2307,7 +2275,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.20",
|
"nanoid": "^3.3.20",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
@ -2323,7 +2290,6 @@
|
|||||||
"integrity": "sha512-bWvlMpFKiTfDKfnK7FC2MPgPeWCf/Kpwsq49ApdW+FQaBZb6O6dZ4MAPbPUjp7hYQrw/RobqCzn64BRrQecGHw==",
|
"integrity": "sha512-bWvlMpFKiTfDKfnK7FC2MPgPeWCf/Kpwsq49ApdW+FQaBZb6O6dZ4MAPbPUjp7hYQrw/RobqCzn64BRrQecGHw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oxc-project/types": "=0.156.0",
|
"@oxc-project/types": "=0.156.0",
|
||||||
"@rolldown/pluginutils": "^1.0.0"
|
"@rolldown/pluginutils": "^1.0.0"
|
||||||
@ -2373,7 +2339,6 @@
|
|||||||
"integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==",
|
"integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@ -2493,7 +2458,6 @@
|
|||||||
"integrity": "sha512-UI7dK29NnS54Fk+mmo7o+jI6U8Fp7hDl+X5Q1k45WH+d4dSlbLVUETGQ+hfkNgvLQGyoElPj837nZyv4MBc0VA==",
|
"integrity": "sha512-UI7dK29NnS54Fk+mmo7o+jI6U8Fp7hDl+X5Q1k45WH+d4dSlbLVUETGQ+hfkNgvLQGyoElPj837nZyv4MBc0VA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lightningcss": "^1.33.0",
|
"lightningcss": "^1.33.0",
|
||||||
"picomatch": "^4.0.7",
|
"picomatch": "^4.0.7",
|
||||||
|
|||||||
@ -22,11 +22,12 @@
|
|||||||
"LICENSE"
|
"LICENSE"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"examples": "vite examples",
|
||||||
"build": "rm -rf dist && tsc",
|
"build": "rm -rf dist && tsc",
|
||||||
"typecheck": "tsc --noEmit && tsc -p tsconfig.test.json",
|
"typecheck": "tsc --noEmit && tsc -p tsconfig.test.json",
|
||||||
"lint": "oxlint --type-aware src test",
|
"lint": "oxlint --type-aware src test examples",
|
||||||
"format": "oxfmt '*.md' '*.ts' 'src/**/*.ts' 'test/**/*.ts'",
|
"format": "oxfmt '*.md' '*.ts' 'src/**/*.ts' 'test/**/*.ts' 'examples/**/*.ts'",
|
||||||
"format:check": "oxfmt --check '*.md' '*.ts' 'src/**/*.ts' 'test/**/*.ts'",
|
"format:check": "oxfmt --check '*.md' '*.ts' 'src/**/*.ts' 'test/**/*.ts' 'examples/**/*.ts'",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"test:watch": "vitest",
|
"test:watch": "vitest",
|
||||||
"prepublishOnly": "npm run format:check && npm run lint && npm run typecheck && npm run test && npm run build"
|
"prepublishOnly": "npm run format:check && npm run lint && npm run typecheck && npm run test && npm run build"
|
||||||
@ -38,6 +39,7 @@
|
|||||||
"oxlint-tsgolint": "^7.0.2006",
|
"oxlint-tsgolint": "^7.0.2006",
|
||||||
"playwright": "^1.65.0",
|
"playwright": "^1.65.0",
|
||||||
"typescript": "^7.0.2",
|
"typescript": "^7.0.2",
|
||||||
|
"vite": "^8.3.6",
|
||||||
"vitest": "^5.1.0"
|
"vitest": "^5.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,5 +6,5 @@
|
|||||||
"declaration": false,
|
"declaration": false,
|
||||||
"declarationMap": false
|
"declarationMap": false
|
||||||
},
|
},
|
||||||
"include": ["src/**/*", "test/**/*"]
|
"include": ["src/**/*", "test/**/*", "examples/**/*"]
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user