@johnmorrisdotca/kazoku-ui 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.15.0] - 2026-11-03
6
+
7
+ One "Put it in your page" panel for every package of the family.
8
+
9
+ - **`<kazoku-embed-panel>`** (KAZOKU-UI-40 and KAZOKU-UI-30; John, 2026-11-03: "does every package that has embedded have all these options?", "if we do it better elsewhere then we should also use that as a template"). A package fills it with its own choices and its own code; the panel is the same everywhere: a **compact picker** for the thing being embedded (chips by group, a search box from twelve things up, buttons that scroll in their own box; or the package's own in the `picker` slot), a **size** (160, 240, 320, 480 by default), **a tab for each way a page is made** (HTML, ES module, React, Vue, Angular, Svelte, iframe, and any a package adds, such as an image address), **the picture the code makes**, built from the same choices and scaled to fit its box at every size on every screen (wakusei-planets' Venus was cut off on the right), **Copy** with its name on it and a polite "Copied", **Open the embed page**, **Read the docs** and **the credit line**. Slots keep what a package had: `options` (its own choices, between the size and the tabs), `actions` (Save SVG, Download), `picker` and `lede`; `note` and `credits` are a text or a function of the choices. `address` keeps the tab and the size in the link (`?embed=react&size=240`). `lang` (English and Japanese), `theme`, light and dark, 48 px targets, arrow keys, Home and End on the tabs. A package that draws nothing says `no-preview no-size` and still has the tabs, Copy, the docs link and the credit. New part: `@johnmorrisdotca/kazoku-ui/embed-panel`, `/element` and `/define`; the event is `kazoku-embedchange` with `{ kind, size, value, code, reason }`, and `kazoku-embedcopy` follows Copy.
10
+ - **`embedSnippet(kind, spec)`** writes the seven ways from one description of the element (`SnippetSpec`: package, CDN address, tag, entry points, attributes, a name, the iframe), taken from wakusei-planets' generators, byte for byte; with `EMBED_KINDS`, `attrText`, `parseKinds`, `parseSizes`, `nearestSize`, `filterItems`, `groupsOf`, `kindLabel`, the labels in both languages and the defaults. The module imports on a server.
11
+ - **The must-have list has a new item, `embed-panel`**: the demo's "Put it in your page" is kazoku-ui's panel (`templates/check-standard.mjs`, `STANDARD_VERSION` 2026-11-03; `docs/package-standard.md`). A package that cannot say why in `family.notApplicable`. A repo takes the new check when it is next touched; its rollout is a ticket on the modules board.
12
+ - New demo pages `embed-panel.html` (a made-up badge with every part, then a package that draws nothing) and `embed.html` (the badge's embed page), `e2e/embed-panel.demo.mjs` (Chromium and WebKit, desk and phone, light and dark), `test/embed-panel.test.ts`.
13
+ - Fixed: the demo server (`scripts/dev.mjs`) crashed on a file that does not exist after answering 200; it reads first.
14
+
15
+ ### Documentation
16
+
17
+ - A guide, `embed-panel.md`, with its Japanese page and a picture; [the survey](docs/embed-panel-survey.md) of every package's panel and which does each part best; the README, getting started, architecture, labels and upgrading pages; the API reference lists the new part.
18
+
5
19
  ## [0.14.0] - 2026-11-03
6
20
 
7
21
  One speed control for the whole family: the Auto-turn speeds are 0.5×, 1× and 2×, and the same buttons serve a solar system's days a second and a replay bar.
package/README.md CHANGED
@@ -11,7 +11,7 @@ The family's shared interface parts, as their own versioned package: one part pe
11
11
  The first part is the **view controls** for any 3D or zoomable view. They never draw the scene: they tell your page what the reader chose, and your canvas, WebGL,
12
12
  SVG or CSS 3D applies it.
13
13
 
14
- [Demo](https://johnmorrisdotca.github.io/kazoku-ui/) · [Layouts demo](https://johnmorrisdotca.github.io/kazoku-ui/layouts.html) · [Strict-CSP demo](https://johnmorrisdotca.github.io/kazoku-ui/csp.html) · [Theme switch](docs/theme-switch.md) · [Language picker](docs/language-picker.md) · [Address state](docs/address-state.md) · [Save image](docs/save-image.md) · [Speed buttons](docs/guides/speed-buttons.md) · [Shared 3D renderer](docs/renderer.md) · [The drag rule](docs/drag-rule.md) · [Content-Security-Policy](docs/csp.md) · [Labels, English and 日本語](docs/labels.md) · [Changelog](CHANGELOG.md) · [Builder](https://johnmorrisdotca.github.io/kazoku-ui/builder.html) · [API reference](https://github.com/johnmorrisdotca/kazoku-ui/blob/main/docs/API.md) · [Guides](https://github.com/johnmorrisdotca/kazoku-ui/blob/main/docs/guides)
14
+ [Demo](https://johnmorrisdotca.github.io/kazoku-ui/) · [Layouts demo](https://johnmorrisdotca.github.io/kazoku-ui/layouts.html) · [Strict-CSP demo](https://johnmorrisdotca.github.io/kazoku-ui/csp.html) · [Theme switch](docs/theme-switch.md) · [Language picker](docs/language-picker.md) · [Address state](docs/address-state.md) · [Save image](docs/save-image.md) · [Speed buttons](docs/guides/speed-buttons.md) · [Embed panel](docs/guides/embed-panel.md) · [Shared 3D renderer](docs/renderer.md) · [The drag rule](docs/drag-rule.md) · [Content-Security-Policy](docs/csp.md) · [Labels, English and 日本語](docs/labels.md) · [Changelog](CHANGELOG.md) · [Builder](https://johnmorrisdotca.github.io/kazoku-ui/builder.html) · [API reference](https://github.com/johnmorrisdotca/kazoku-ui/blob/main/docs/API.md) · [Guides](https://github.com/johnmorrisdotca/kazoku-ui/blob/main/docs/guides)
15
15
 
16
16
  ## In 30 seconds
17
17
 
@@ -58,6 +58,7 @@ console.log(zoomText(2.44)); // 2.4×
58
58
  - **Theme switch** (Light, Dark, Auto) and **language picker** (each language in its own name) that write `data-theme` and `lang` and tell the page.
59
59
  - **Save image** for a canvas, an SVG or any element, as PNG or SVG.
60
60
  - **Speed buttons**: one row of 0.5×, 1×, 2× (or the steps you name: a solar system's days a second, a replay's speeds) for the whole family, kept in an attribute and the address.
61
+ - **Embed panel**: the family's one "Put it in your page" panel: a compact picker, a size, a tab for HTML, ES module, React, Vue, Angular, Svelte and iframe (and your own, such as an image), a live picture that always fits its box, Copy, the embed page, the docs and the credit line.
61
62
  - **Address state**, with no element: choices kept in the link with `replaceState`.
62
63
  - **A shared 3D renderer** and its **freeze checks**: one WebGL context, shared assets, stills, a pixel budget.
63
64
  - **Labels in English and Japanese**, light and dark, 48 px touch targets, reduced motion respected.
@@ -160,6 +161,9 @@ Each its own subpath; later ones: picture-tile pickers, Help, GitHub and npm pil
160
161
  | `@johnmorrisdotca/kazoku-ui/speed-buttons` | The steps, how a speed reads, the nearest step and the address key of the speed buttons. Imports on a server. |
161
162
  | `@johnmorrisdotca/kazoku-ui/speed-buttons/element` | The `SpeedButtonsElement` class (needs a browser). |
162
163
  | `@johnmorrisdotca/kazoku-ui/speed-buttons/define` | Registers `<kazoku-speed-buttons>` (once, and only if the tag is free). |
164
+ | `@johnmorrisdotca/kazoku-ui/embed-panel` | The seven ways a page is made and their text (`embedSnippet`), the sizes, the picker's search and the labels of the embed panel. Imports on a server. |
165
+ | `@johnmorrisdotca/kazoku-ui/embed-panel/element` | The `EmbedPanelElement` class (needs a browser). |
166
+ | `@johnmorrisdotca/kazoku-ui/embed-panel/define` | Registers `<kazoku-embed-panel>` (once, and only if the tag is free). |
163
167
  | `@johnmorrisdotca/kazoku-ui/renderer` | The shared 3D renderer: one WebGL context for the focused view, shared assets, stills, drawing on demand, a pixel budget. three.js is an optional peer, loaded by your `import("three")`. Imports on a server. |
164
168
  | `@johnmorrisdotca/kazoku-ui/testing` | The freeze checks for a 3D package's e2e: count contexts, GPU objects, draws and frames; the canvas size against the budget. No test framework named. |
165
169
  | `@johnmorrisdotca/kazoku-ui/address-state` | Choices kept in the address: read, write and subscribe to URL parameters with `replaceState`. No element; imports on a server. |
@@ -441,6 +445,22 @@ What a picture of an element cannot carry (web fonts, `::before`, cross-origin i
441
445
 
442
446
  See [the guide](docs/guides/speed-buttons.md) for the attributes, the event and the functions without the element.
443
447
 
448
+ ## Embed panel
449
+
450
+ `<kazoku-embed-panel>` is the "Put it in your page" panel every package of the family shares: a compact picker for the thing being embedded, a size, a tab for each way a page is made, the picture the code makes (scaled to fit its box), Copy with its name on it, Open the embed page, Read the docs and the credit line. The package gives it `generate` (the code of a tab) and `preview` (the picture); `embedSnippet` writes all seven tabs from one description of the element.
451
+
452
+ ```html
453
+ <kazoku-embed-panel id="panel" kinds="html module react vue angular svelte iframe" docs-href="docs/"></kazoku-embed-panel>
454
+ <script type="module">
455
+ import "@johnmorrisdotca/kazoku-ui/embed-panel/define";
456
+ import { embedSnippet } from "@johnmorrisdotca/kazoku-ui/embed-panel";
457
+ document.getElementById("panel").generate = ({ kind, size }) =>
458
+ embedSnippet(kind, { package: "@scope/dial", cdn: "https://cdn.jsdelivr.net/npm/@scope/dial@1.0.0", tag: "scope-dial", element: "element", entries: [], attrs: [["size", String(size)]], name: "dial", iframe: { src: "https://example.com/embed.html", title: "Dial", width: size, height: size } });
459
+ </script>
460
+ ```
461
+
462
+ See [the guide](docs/guides/embed-panel.md) for the slots (a package's own choices and buttons), the picker, the events and a package that draws nothing, and [the survey](docs/embed-panel-survey.md) for which package's panel does each part best.
463
+
444
464
  ## Shared 3D renderer
445
465
 
446
466
  A page with many 3D views gets one WebGL context, handed to the focused view; the others are stills made once from the same renderer, kept in a budgeted list. Shared geometries,
@@ -0,0 +1,42 @@
1
+ /** The sizes (pixels across) the size buttons offer by default. */
2
+ export declare const DEFAULT_SIZES: readonly number[];
3
+ /** The size the panel starts on when the page names none and it is one of the sizes. */
4
+ export declare const DEFAULT_SIZE = 320;
5
+ /** The tabs the panel shows when the page names none: the seven ways a page is made. */
6
+ export declare const DEFAULT_KINDS: readonly string[];
7
+ /** The address parameters the panel keeps its two choices in. */
8
+ export declare const KIND_ADDRESS_KEY = "embed";
9
+ export declare const SIZE_ADDRESS_KEY = "size";
10
+ /** More than this many things to pick from and the picker shows a search box. */
11
+ export declare const SEARCH_FROM = 12;
12
+ /**
13
+ * One thing the compact picker offers: a planet, a jersey, a stadium.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * import { type PickItem } from "@johnmorrisdotca/kazoku-ui/embed-panel";
18
+ *
19
+ * const mars: PickItem = { id: "mars", label: "Mars", group: "Planets" };
20
+ * console.log(mars.label);
21
+ * ```
22
+ */
23
+ export interface PickItem {
24
+ /** What the page reads back from `value`. */
25
+ id: string;
26
+ /** What the button says. */
27
+ label: string;
28
+ /** The row of choices it is under (chips above the list); items with no group are only under All. */
29
+ group?: string;
30
+ /** More words the search matches (another name, a country). */
31
+ words?: string;
32
+ }
33
+ /** The tab ids from a text such as `html module react`: lower case, once each, in order; no usable id is the default list. */
34
+ export declare function parseKinds(text: string | null | undefined, fallback?: readonly string[]): string[];
35
+ /** The sizes from a text such as `160 240 320`: whole numbers from 16 up to 4096, once each, smallest first; no usable number is the default sizes. */
36
+ export declare function parseSizes(text: string | null | undefined, fallback?: readonly number[]): number[];
37
+ /** The size to use for a wish: the wish if it is one of the sizes, else the sizes' nearest to it, else the one nearest `DEFAULT_SIZE`. */
38
+ export declare function nearestSize(wish: number | string | null | undefined, sizes: readonly number[]): number;
39
+ /** The groups of a list, once each, in the order they first appear. */
40
+ export declare function groupsOf(items: readonly PickItem[]): string[];
41
+ /** The items that are in `group` (all of them when it is empty) and have every word of `query` in their label, id, group or words, ignoring case and accents. */
42
+ export declare function filterItems(items: readonly PickItem[], query?: string, group?: string): PickItem[];
@@ -0,0 +1,48 @@
1
+ // What the panel's choices are, without the element: the tabs, the sizes and the compact picker's list. Nothing here touches the DOM, so it imports on a server.
2
+ import { EMBED_KINDS } from "./snippets.js";
3
+ /** The sizes (pixels across) the size buttons offer by default. */
4
+ export const DEFAULT_SIZES = [160, 240, 320, 480];
5
+ /** The size the panel starts on when the page names none and it is one of the sizes. */
6
+ export const DEFAULT_SIZE = 320;
7
+ /** The tabs the panel shows when the page names none: the seven ways a page is made. */
8
+ export const DEFAULT_KINDS = EMBED_KINDS;
9
+ /** The address parameters the panel keeps its two choices in. */
10
+ export const KIND_ADDRESS_KEY = "embed";
11
+ export const SIZE_ADDRESS_KEY = "size";
12
+ /** More than this many things to pick from and the picker shows a search box. */
13
+ export const SEARCH_FROM = 12;
14
+ /** The tab ids from a text such as `html module react`: lower case, once each, in order; no usable id is the default list. */
15
+ export function parseKinds(text, fallback = DEFAULT_KINDS) {
16
+ const found = [...new Set((text ?? "").toLowerCase().split(/[\s,]+/).filter((id) => /^[a-z][a-z0-9-]*$/.test(id)))];
17
+ return found.length > 0 ? found : [...fallback];
18
+ }
19
+ /** The sizes from a text such as `160 240 320`: whole numbers from 16 up to 4096, once each, smallest first; no usable number is the default sizes. */
20
+ export function parseSizes(text, fallback = DEFAULT_SIZES) {
21
+ const found = [...new Set((text ?? "").split(/[\s,]+/).map(Number).filter((n) => Number.isInteger(n) && n >= 16 && n <= 4096))].sort((a, b) => a - b);
22
+ return found.length > 0 ? found : [...fallback];
23
+ }
24
+ /** The size to use for a wish: the wish if it is one of the sizes, else the sizes' nearest to it, else the one nearest `DEFAULT_SIZE`. */
25
+ export function nearestSize(wish, sizes) {
26
+ const want = Number(wish);
27
+ const aim = Number.isFinite(want) && wish !== null && wish !== undefined && String(wish).trim() !== "" ? want : DEFAULT_SIZE;
28
+ let best = sizes[0] ?? DEFAULT_SIZE;
29
+ for (const size of sizes)
30
+ if (Math.abs(size - aim) < Math.abs(best - aim))
31
+ best = size;
32
+ return best;
33
+ }
34
+ /** The groups of a list, once each, in the order they first appear. */
35
+ export function groupsOf(items) {
36
+ return [...new Set(items.flatMap((item) => (item.group !== undefined && item.group !== "" ? [item.group] : [])))];
37
+ }
38
+ /** The items that are in `group` (all of them when it is empty) and have every word of `query` in their label, id, group or words, ignoring case and accents. */
39
+ export function filterItems(items, query = "", group = "") {
40
+ const fold = (text) => text.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase();
41
+ const needles = fold(query).split(/\s+/).filter((word) => word !== "");
42
+ return items.filter((item) => {
43
+ if (group !== "" && item.group !== group)
44
+ return false;
45
+ const hay = fold(`${item.label} ${item.id} ${item.group ?? ""} ${item.words ?? ""}`);
46
+ return needles.every((word) => hay.includes(word));
47
+ });
48
+ }
@@ -0,0 +1 @@
1
+ export declare function defineEmbedPanel(registry?: CustomElementRegistry): boolean;
@@ -0,0 +1,9 @@
1
+ // Importing this registers <kazoku-embed-panel>, unless the tag is already taken (the first definition wins; see view-controls/define.ts for why a taken tag is left alone).
2
+ import { EmbedPanelElement, TAG } from "./element.js";
3
+ export function defineEmbedPanel(registry = customElements) {
4
+ if (registry.get(TAG) !== undefined)
5
+ return false;
6
+ registry.define(TAG, EmbedPanelElement);
7
+ return true;
8
+ }
9
+ defineEmbedPanel();
@@ -0,0 +1,84 @@
1
+ import { type PickItem } from "./choices.ts";
2
+ import { EVENT, TAG } from "./index.ts";
3
+ import { type PanelWords } from "./words.ts";
4
+ export { EVENT, TAG };
5
+ /** What the page's `generate`, `preview`, `credits` and `note` functions are told. */
6
+ export interface EmbedContext {
7
+ /** The tab: `html`, `module`, `react`, `vue`, `angular`, `svelte`, `iframe`, or one the page added. */
8
+ kind: string;
9
+ /** The size chosen, in pixels across. */
10
+ size: number;
11
+ /** True when the text is for the picture beside the code, not for the reader to copy: a page may use a relative address there so the picture works from where the page is. */
12
+ preview: boolean;
13
+ }
14
+ /** What the `kazoku-embedchange` event carries in `detail`. */
15
+ export interface EmbedChangeDetail {
16
+ kind: string;
17
+ size: number;
18
+ /** The picker's choice (an item's `id`), or an empty text where there is no built-in picker. */
19
+ value: string;
20
+ /** The text to copy now. */
21
+ code: string;
22
+ /** `init` (once, when it comes up), `kind` (a tab), `size` (a size button), `pick` (the picker) or `set` (a property or attribute). */
23
+ reason: "init" | "kind" | "size" | "pick" | "set";
24
+ }
25
+ type Text = string | ((context: EmbedContext) => string);
26
+ /**
27
+ * Attributes: `kinds` (the tabs, `html module react vue angular svelte iframe` by default; add your own such as `image`), `kind` (the tab to start on), `sizes` (`160 240 320 480`) and
28
+ * `size` (the start, 320), `no-size` (no size choice), `value` (the picker's choice), `open-href` (the embed page), `docs-href` (the docs), `aspect` (the picture's height over its width, 1),
29
+ * `no-preview` (no picture), `heading` (show the shared heading), `search` / `no-search` (force the picker's search box on or off), `pick-label` (the picker's name), `address` (keep the tab and
30
+ * the size in the address under `kind-key`, `embed`, and `size-key`, `size`; the starts are left off it), `lang` (`en` or `ja`), `theme` (`light` or `dark`).
31
+ * Slots: `lede` (a line under the heading), `picker` (your own picker in place of the built-in one), `options` (your own choices between the size and the tabs), `actions` (more buttons
32
+ * beside Copy: Save SVG, Download). Parts: `panel`, `title`, `picker`, `size`, `tabs`, `tab`, `preview`, `note`, `code`, `copy`, `open`, `docs`, `credits`.
33
+ * Custom properties: `--kazoku-preview-bg` (behind the picture, the page's surface by default), `--kazoku-preview-width` (the picture's column, 260px).
34
+ */
35
+ export declare class EmbedPanelElement extends HTMLElement {
36
+ #private;
37
+ static observedAttributes: string[];
38
+ constructor();
39
+ /** The tab now: one of `kinds`. Setting one that is not there is ignored. */
40
+ get kind(): string;
41
+ set kind(kind: string);
42
+ /** The size now, in pixels across: one of `sizes`. Setting a number that is not one takes the nearest. */
43
+ get size(): number;
44
+ set size(size: number | string);
45
+ /** The tabs, in order. Setting a list (or a text such as `html react iframe`) draws them again. */
46
+ get kinds(): readonly string[];
47
+ set kinds(list: readonly string[] | string);
48
+ /** The sizes the buttons offer, smallest first. */
49
+ get sizes(): readonly number[];
50
+ set sizes(list: readonly number[] | string);
51
+ /** The picker's choice: an item's `id`. Setting one that is not in `items` is ignored. */
52
+ get value(): string;
53
+ set value(id: string);
54
+ /** The things the built-in picker offers. Leave it empty and give the `picker` slot for a picker of your own. */
55
+ get items(): readonly PickItem[];
56
+ set items(items: readonly PickItem[]);
57
+ /** How a picker button looks inside: a node (a small picture) or a text. The label by default. */
58
+ set renderItem(render: ((item: PickItem) => Node | string) | undefined);
59
+ /** The text of a tab for the choices as they are. Required: without it the code box is empty. Call `update()` after the page's own choices change. */
60
+ set generate(generate: ((context: EmbedContext) => string) | undefined);
61
+ /** The picture the code makes, as a node (or nothing). It is called when the picture's choices change; the iframe tab draws the iframe itself when this returns nothing for it. */
62
+ set preview(preview: ((context: EmbedContext) => Node | undefined) | undefined);
63
+ /** The credit line shown under the code (and read by a screen reader): a text, or a function of the choices. */
64
+ set credits(credits: Text);
65
+ /** A line above the code for the tab shown ("The embed page is for a site that cannot run script."): a text, or a function of the choices. */
66
+ set note(note: Text);
67
+ /** The names of the tabs the family has no word for: `{ image: "Image address" }`. */
68
+ set labels(labels: Record<string, string>);
69
+ /** Your own words over the language's. */
70
+ set words(words: Partial<PanelWords>);
71
+ /** Where "Open the embed page" goes. Set it again when the choices change the address. */
72
+ get openHref(): string;
73
+ set openHref(href: string);
74
+ /** Where "Read the docs" goes. */
75
+ get docsHref(): string;
76
+ set docsHref(href: string);
77
+ /** The text of the tab shown: what Copy copies. */
78
+ get code(): string;
79
+ /** Reads the page's choices again: the code, the credit, the note, the links, and the picture if its choices changed. Call it whenever the page's own choices change. It tells no one. */
80
+ update(): void;
81
+ connectedCallback(): void;
82
+ disconnectedCallback(): void;
83
+ attributeChangedCallback(name: string, _old: string | null, value: string | null): void;
84
+ }