@johnmorrisdotca/kazoku-ui 0.13.1 → 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.
Files changed (38) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +46 -8
  3. package/dist/embed-panel/choices.d.ts +42 -0
  4. package/dist/embed-panel/choices.js +48 -0
  5. package/dist/embed-panel/define.d.ts +1 -0
  6. package/dist/embed-panel/define.js +9 -0
  7. package/dist/embed-panel/element.d.ts +84 -0
  8. package/dist/embed-panel/element.js +683 -0
  9. package/dist/embed-panel/index.d.ts +5 -0
  10. package/dist/embed-panel/index.js +6 -0
  11. package/dist/embed-panel/snippets.d.ts +94 -0
  12. package/dist/embed-panel/snippets.js +73 -0
  13. package/dist/embed-panel/styles.d.ts +1 -0
  14. package/dist/embed-panel/styles.js +43 -0
  15. package/dist/embed-panel/words.d.ts +43 -0
  16. package/dist/embed-panel/words.js +69 -0
  17. package/dist/index.d.ts +1 -1
  18. package/dist/index.js +1 -1
  19. package/dist/speed-buttons/define.d.ts +1 -0
  20. package/dist/speed-buttons/define.js +9 -0
  21. package/dist/speed-buttons/element.d.ts +34 -0
  22. package/dist/speed-buttons/element.js +178 -0
  23. package/dist/speed-buttons/index.d.ts +4 -0
  24. package/dist/speed-buttons/index.js +5 -0
  25. package/dist/speed-buttons/steps.d.ts +18 -0
  26. package/dist/speed-buttons/steps.js +57 -0
  27. package/dist/speed-buttons/styles.d.ts +1 -0
  28. package/dist/speed-buttons/styles.js +9 -0
  29. package/dist/speed-buttons/words.d.ts +9 -0
  30. package/dist/speed-buttons/words.js +7 -0
  31. package/dist/view-controls/address.d.ts +3 -3
  32. package/dist/view-controls/address.js +12 -9
  33. package/dist/view-controls/element.d.ts +2 -1
  34. package/dist/view-controls/element.js +55 -26
  35. package/dist/view-controls/levels.d.ts +1 -1
  36. package/dist/view-controls/styles.d.ts +1 -1
  37. package/dist/view-controls/styles.js +6 -6
  38. package/package.json +43 -4
@@ -0,0 +1,57 @@
1
+ // The speed buttons' arithmetic: pure, so a server or a test can use it. A speed is a plain number (a multiple of the normal pace, or a rate such as days a second);
2
+ // the buttons are the steps the page chose, and one of them is on.
3
+ /** The steps when a page names none: half speed, normal and double. */
4
+ export const DEFAULT_STEPS = [0.5, 1, 2];
5
+ /** The word on each step's button after its number, when the page names none. */
6
+ export const DEFAULT_UNIT = "×";
7
+ /** The address key the buttons keep their choice under, unless `address-key` names another. */
8
+ export const SPEED_ADDRESS_KEY = "speed";
9
+ const number = (text) => (text.trim() === "" ? Number.NaN : Number(text));
10
+ /** The steps in a list such as `0.5 1 2` or `1, 10, 100`: finite numbers in the order given, each once. `fallback` when the list holds none. A step may be 0 (Paused) or negative (Back). */
11
+ export function parseSteps(text, fallback = DEFAULT_STEPS) {
12
+ const found = [];
13
+ for (const part of (text ?? "").split(/[\s,]+/)) {
14
+ const value = number(part);
15
+ if (Number.isFinite(value) && !found.includes(value))
16
+ found.push(value);
17
+ }
18
+ return found.length > 0 ? found : [...fallback];
19
+ }
20
+ /** The number as a button shows it: `0.5×`, `1×`, `100×`, `−1×` (a real minus sign). At most four significant figures, so 0.333333 reads `0.333×`. */
21
+ export function speedText(value, unit = DEFAULT_UNIT) {
22
+ const text = String(Number(value.toPrecision(4)));
23
+ return `${text.startsWith("-") ? `−${text.slice(1)}` : text}${unit}`;
24
+ }
25
+ /** The step nearest `value`: by ratio where both are above zero (so 2.5 is nearer 2 than 4), else by plain distance. `undefined` when there are no steps. */
26
+ export function nearestStep(value, steps) {
27
+ let best;
28
+ let gap = Number.POSITIVE_INFINITY;
29
+ for (const step of steps) {
30
+ const far = value > 0 && step > 0 ? Math.abs(Math.log(value / step)) : Math.abs(value - step);
31
+ if (far < gap) {
32
+ gap = far;
33
+ best = step;
34
+ }
35
+ }
36
+ return best;
37
+ }
38
+ /** The step the buttons start on: 1 where the steps have it (normal speed), else the first. */
39
+ export function startStep(steps) {
40
+ return steps.includes(1) ? 1 : (steps[0] ?? 1);
41
+ }
42
+ /** The step after (`direction` 1) or before (-1) `value`, going round at the ends: one press of a single button that cycles through the speeds. */
43
+ export function stepFrom(value, steps, direction = 1) {
44
+ if (steps.length === 0)
45
+ return value;
46
+ const at = steps.findIndex((step) => step === value);
47
+ const from = at >= 0 ? at : steps.indexOf(nearestStep(value, steps) ?? value);
48
+ return steps[(Math.max(from, 0) + direction + steps.length) % steps.length];
49
+ }
50
+ /** What an address or attribute says: a number that is (or is nearest to) one of the steps. `undefined` when it is not a number at all. The words `slow`, `medium` and `fast` of the first view controls mean 0.5, 1 and 2. */
51
+ export function parseSpeed(text, steps) {
52
+ if (text === null || text === undefined)
53
+ return undefined;
54
+ const word = { slow: 0.5, medium: 1, fast: 2 }[text.trim().toLowerCase()];
55
+ const value = word ?? number(text);
56
+ return Number.isFinite(value) ? nearestStep(value, steps) : undefined;
57
+ }
@@ -0,0 +1 @@
1
+ export declare const STYLES = "\n:host {\n --_ink: var(--kazoku-ink, #1b1b1f);\n --_surface: var(--kazoku-surface, #ffffff);\n --_line: var(--kazoku-line, #c9ccd3);\n --_accent: var(--kazoku-accent, #2457d6);\n display: inline-block;\n box-sizing: border-box;\n color: var(--_ink);\n font: 600 0.9rem/1.2 system-ui, -apple-system, \"Hiragino Sans\", \"Noto Sans JP\", \"Segoe UI\", sans-serif;\n color-scheme: light dark;\n}\n@media (prefers-color-scheme: dark) {\n :host(:not([theme=\"light\"]):not([data-scheme=\"light\"])) {\n --_ink: var(--kazoku-ink, #f1f2f6);\n --_surface: var(--kazoku-surface, #1c1d22);\n --_line: var(--kazoku-line, #4a4d58);\n --_accent: var(--kazoku-accent, #8fb0ff);\n }\n}\n:host([theme=\"dark\"]), :host([data-scheme=\"dark\"]:not([theme=\"light\"])) {\n --_ink: var(--kazoku-ink, #f1f2f6);\n --_surface: var(--kazoku-surface, #1c1d22);\n --_line: var(--kazoku-line, #4a4d58);\n --_accent: var(--kazoku-accent, #8fb0ff);\n color-scheme: dark;\n}\n:host([theme=\"light\"]) { color-scheme: light; }\n[hidden] { display: none !important; }\n.vh { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }\n.group { display: inline-flex; align-items: center; flex-wrap: wrap; gap: 4px; }\nbutton { -webkit-appearance: none; appearance: none; box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; gap: 6px; min-width: 48px; min-height: 48px; margin: 0; padding: 0 14px; border: 1px solid var(--_line); border-radius: 24px; background: var(--_surface); color: var(--_ink); font: inherit; cursor: pointer; white-space: nowrap; }\nbutton:hover:not(:disabled) { border-color: var(--_ink); }\nbutton:focus-visible { outline: 2px solid var(--_ink); outline-offset: 2px; }\nbutton[aria-pressed=\"true\"] { background: var(--_ink); color: var(--_surface); border-color: var(--_ink); }\nbutton:disabled { opacity: 0.4; cursor: default; }\nsvg { width: 18px; height: 18px; fill: currentColor; flex: none; }\n\n:host([hidden]) { display: none; }\n:host { display: inline-flex; align-items: center; gap: 4px; vertical-align: middle; }\n.lbl { margin-inline-end: 4px; }\nbutton { padding: 0 12px; min-width: 48px; font-variant-numeric: tabular-nums; }\n:host([no-label]) .lbl { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }\n";
@@ -0,0 +1,9 @@
1
+ import { BASE_STYLES } from "../shared/base.js";
2
+ // One button for each speed, side by side; the one that is on is filled. The name comes first and is hidden (for the eye) by `::part(label)` where a page is short of room.
3
+ export const STYLES = `${BASE_STYLES}
4
+ :host([hidden]) { display: none; }
5
+ :host { display: inline-flex; align-items: center; gap: 4px; vertical-align: middle; }
6
+ .lbl { margin-inline-end: 4px; }
7
+ button { padding: 0 12px; min-width: 48px; font-variant-numeric: tabular-nums; }
8
+ :host([no-label]) .lbl { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
9
+ `;
@@ -0,0 +1,9 @@
1
+ export interface SpeedWords {
2
+ /** The name of the group of buttons, said by a screen reader and shown before them. */
3
+ group: string;
4
+ }
5
+ export type Language = "en" | "ja";
6
+ export declare const WORDS: Record<Language, SpeedWords>;
7
+ /** The language to use for a `lang` value such as "ja", "ja-JP" or "en-CA": Japanese for ja, English for anything else. */
8
+ export declare const languageOf: (lang: string | null | undefined) => Language;
9
+ export declare const wordsFor: (language: Language, overrides?: Partial<SpeedWords>) => SpeedWords;
@@ -0,0 +1,7 @@
1
+ export const WORDS = {
2
+ en: { group: "Speed" },
3
+ ja: { group: "速さ" },
4
+ };
5
+ /** The language to use for a `lang` value such as "ja", "ja-JP" or "en-CA": Japanese for ja, English for anything else. */
6
+ export const languageOf = (lang) => (/^ja\b/i.test(lang ?? "") ? "ja" : "en");
7
+ export const wordsFor = (language, overrides = {}) => ({ ...WORDS[language], ...overrides });
@@ -1,4 +1,3 @@
1
- import { type Pace } from "./levels.ts";
2
1
  import { type Limits } from "./math.ts";
3
2
  export type DragMode = "move" | "turn";
4
3
  export interface AddressKeys {
@@ -6,7 +5,7 @@ export interface AddressKeys {
6
5
  tilt: string;
7
6
  zoom: string;
8
7
  drag: string;
9
- /** The auto-turn speed (`slow` or `fast`; Medium is the start and leaves the address). */
8
+ /** The auto-turn speed, a number (`0.5`, `2`; 1 is the start and leaves the address). `pace` by default, because a page's own `speed` (a clock's, a replay's) is its own. */
10
9
  speed?: string;
11
10
  }
12
11
  export declare const DEFAULT_KEYS: AddressKeys;
@@ -15,7 +14,8 @@ export interface AddressChoices {
15
14
  tilt: number;
16
15
  zoom: number;
17
16
  drag: DragMode;
18
- pace?: Pace;
17
+ /** The auto-turn speed as a multiple of the element's `speed`: the number of the button that is on. */
18
+ pace?: number;
19
19
  }
20
20
  /** Read the choices out of a query string (`?yaw=40`, with or without the `?`). Anything missing or not a number is left out of the result. */
21
21
  export declare function parseAddress(search: string, keys?: AddressKeys, limits?: Limits): Partial<AddressChoices>;
@@ -1,8 +1,8 @@
1
1
  // The choices kept in the page's address: pure string functions, so a server or a test can use them. Only what a person chose is kept
2
2
  // (angle, tilt, zoom, what a drag does); the pan is in pixels of one screen and the turning of a moment, so neither is.
3
- import { parsePace } from "./levels.js";
3
+ import { PACE_FACTOR, parsePace } from "./levels.js";
4
4
  import { clamp, DEFAULT_LIMITS, wrapYaw } from "./math.js";
5
- export const DEFAULT_KEYS = { yaw: "yaw", tilt: "tilt", zoom: "zoom", drag: "drag", speed: "speed" };
5
+ export const DEFAULT_KEYS = { yaw: "yaw", tilt: "tilt", zoom: "zoom", drag: "drag", speed: "pace" };
6
6
  const rounded = (value, places) => Number(value.toFixed(places));
7
7
  /** Read the choices out of a query string (`?yaw=40`, with or without the `?`). Anything missing or not a number is left out of the result. */
8
8
  export function parseAddress(search, keys = DEFAULT_KEYS, limits = DEFAULT_LIMITS) {
@@ -27,9 +27,12 @@ export function parseAddress(search, keys = DEFAULT_KEYS, limits = DEFAULT_LIMIT
27
27
  const drag = params.get(keys.drag);
28
28
  if (drag === "turn" || drag === "move")
29
29
  out.drag = drag;
30
- const pace = parsePace(params.get(keys.speed ?? "speed"));
31
- if (pace !== undefined)
32
- out.pace = pace;
30
+ const said = params.get(keys.speed ?? "pace");
31
+ // A number is the pace; so are the words of 0.12 and 0.13 (`slow`, `fast`), which a link made then still carries.
32
+ const named = parsePace(said);
33
+ const value = named !== undefined ? PACE_FACTOR[named] : said === null || said.trim() === "" ? Number.NaN : Number(said);
34
+ if (Number.isFinite(value))
35
+ out.pace = value;
33
36
  return out;
34
37
  }
35
38
  /**
@@ -49,10 +52,10 @@ export function formatAddress(search, choices, keys = DEFAULT_KEYS, leaveYaw = f
49
52
  set(keys.tilt, Math.round(choices.tilt) === 0 ? undefined : String(rounded(choices.tilt, 0)));
50
53
  set(keys.zoom, Math.abs(choices.zoom - startZoom) < 0.005 ? undefined : String(rounded(choices.zoom, 2)));
51
54
  set(keys.drag, choices.drag === "turn" ? "turn" : undefined);
52
- // The speed key is touched only when the caller has a pace, and a value that is not a pace word (a page's own `speed`) is left alone.
53
- const speedKey = keys.speed ?? "speed";
54
- if (choices.pace !== undefined && (choices.pace !== "medium" || parsePace(params.get(speedKey)) !== undefined))
55
- set(speedKey, choices.pace === "medium" ? undefined : choices.pace);
55
+ // The pace key is touched only when the caller has a pace; the start (1) leaves it off the address.
56
+ const paceKey = keys.speed ?? "pace";
57
+ if (choices.pace !== undefined)
58
+ set(paceKey, choices.pace === 1 ? undefined : String(rounded(choices.pace, 3)));
56
59
  const text = params.toString();
57
60
  return text === "" ? "" : `?${text}`;
58
61
  }
@@ -1,6 +1,7 @@
1
1
  import { type ViewController, type ViewState } from "./controller.ts";
2
2
  import { EVENT, TAG } from "./index.ts";
3
3
  import { type Point } from "./math.ts";
4
+ import "../speed-buttons/define.ts";
4
5
  import { type ViewWords } from "./words.ts";
5
6
  export { EVENT, TAG };
6
7
  /** What the `kazoku-viewchange` event carries in `detail`. */
@@ -11,7 +12,7 @@ export interface ViewChangeDetail extends ViewState {
11
12
  /**
12
13
  * Attributes: `for` (the id of the view to bind), `lang` (en or ja; else the page's), `yaw`, `tilt` (the starting view), `zoom` (`fit`, the default, or a scale where 1
13
14
  * is the scene's natural size), `fit-scale` (the page's scale at Fit: what zoom 1 means), `ignore` (a selector for what inside the view a pointer press leaves alone;
14
- * buttons, links and form controls by default), `autoplay` (turn by itself from the start, unless the reader prefers reduced motion), `autoplay-for` (stop that turn by itself after this many seconds of turning), `speed` (degrees a second),
15
+ * buttons, links and form controls by default), `autoplay` (turn by itself from the start, unless the reader prefers reduced motion), `autoplay-for` (stop that turn by itself after this many seconds of turning), `speed` (degrees a second at a pace of 1), `pace` (the auto-turn speed as a multiple of `speed`, 1 by default; the reader's choice of the buttons is written here and kept in the address as `pace`), `paces` (the speeds the buttons stand for, `0.5 1 2` by default),
15
16
  * `anchor` (`x,y`: the point the zoom buttons and keys zoom about, in pixels from the view's middle at Fit), `key-tilt` (`drag`, the default, or `wheel`: which way Arrow Up tips; the wheel itself zooms),
16
17
  * `controls` (`basic`, the default, or `advanced`: which controls are shown, the zoom readout in both, and the speed in both while the view turns; the reader's own choice with the More button is remembered in localStorage, under `kazoku-view-controls` or the key `storage` names, and wins),
17
18
  * `layout` (`classic`, the default, `horizontal`, `vertical` or `compact`: where the controls sit; the horizontal, vertical and compact layouts have the zoom slider),
@@ -6,9 +6,12 @@ import { localStore, remember } from "../shared/store.js";
6
6
  import { DEFAULT_KEYS, formatAddress, parseAddress, parseKeys } from "./address.js";
7
7
  import { createViewController, DEFAULT_IGNORE } from "./controller.js";
8
8
  import { EVENT, TAG } from "./index.js";
9
- import { LEVEL_STORAGE_KEY, PACE_FACTOR, PACES, parseLayout, parseLevel, zoomText } from "./levels.js";
9
+ import { LEVEL_STORAGE_KEY, parseLayout, parseLevel, zoomText } from "./levels.js";
10
10
  import { ZOOM_STEP, ZOOMED } from "./math.js";
11
11
  import { turnOnlyInSight } from "./sight.js";
12
+ import "../speed-buttons/define.js";
13
+ import { EVENT as SPEED_EVENT } from "../speed-buttons/index.js";
14
+ import { nearestStep, parseSpeed, parseSteps, speedText } from "../speed-buttons/steps.js";
12
15
  import { STYLES } from "./styles.js";
13
16
  import { languageOf, wordsFor } from "./words.js";
14
17
  import { SNAP_PX, zoomAtPosition, zoomMarks, zoomPosition, zoomSnap, zoomStep, zoomToMark } from "./zoomScale.js";
@@ -35,11 +38,7 @@ const HTML = `
35
38
  <div class="field tilt adv" part="field tilt"><label data-w="tilt" for="tilt"></label><input id="tilt" type="range" min="-30" max="30" step="1" value="0" part="slider" /><output for="tilt" data-o="tilt">0°</output></div>
36
39
  <button type="button" class="pace-now" data-b="pace-now" part="button" hidden><span class="lbl" data-w="speed"></span><span data-o="pace"></span></button>
37
40
  <button type="button" class="play" data-b="play" aria-pressed="false" part="button">${ICON.play}${ICON.pause}<span class="text" data-w="autoTurn"></span></button>
38
- <div class="group pace adv" role="group" data-g="pace"><span class="lbl" data-w="speed" aria-hidden="true"></span>
39
- <button type="button" data-b="slow" aria-pressed="false" part="button"><span data-w="slow"></span></button>
40
- <button type="button" data-b="medium" aria-pressed="true" part="button"><span data-w="medium"></span></button>
41
- <button type="button" data-b="fast" aria-pressed="false" part="button"><span data-w="fast"></span></button>
42
- </div>
41
+ <kazoku-speed-buttons class="pace adv" data-g="pace"></kazoku-speed-buttons>
43
42
  <button type="button" class="front adv" data-b="front" part="button"><span data-w="front"></span></button>
44
43
  <button type="button" class="centre" data-b="centre" part="button" hidden>${ICON.centre}<span class="text" data-w="centre"></span></button>
45
44
  <div class="group zoom" role="group" data-g="zoom">
@@ -65,7 +64,7 @@ const HTML = `
65
64
  /**
66
65
  * Attributes: `for` (the id of the view to bind), `lang` (en or ja; else the page's), `yaw`, `tilt` (the starting view), `zoom` (`fit`, the default, or a scale where 1
67
66
  * is the scene's natural size), `fit-scale` (the page's scale at Fit: what zoom 1 means), `ignore` (a selector for what inside the view a pointer press leaves alone;
68
- * buttons, links and form controls by default), `autoplay` (turn by itself from the start, unless the reader prefers reduced motion), `autoplay-for` (stop that turn by itself after this many seconds of turning), `speed` (degrees a second),
67
+ * buttons, links and form controls by default), `autoplay` (turn by itself from the start, unless the reader prefers reduced motion), `autoplay-for` (stop that turn by itself after this many seconds of turning), `speed` (degrees a second at a pace of 1), `pace` (the auto-turn speed as a multiple of `speed`, 1 by default; the reader's choice of the buttons is written here and kept in the address as `pace`), `paces` (the speeds the buttons stand for, `0.5 1 2` by default),
69
68
  * `anchor` (`x,y`: the point the zoom buttons and keys zoom about, in pixels from the view's middle at Fit), `key-tilt` (`drag`, the default, or `wheel`: which way Arrow Up tips; the wheel itself zooms),
70
69
  * `controls` (`basic`, the default, or `advanced`: which controls are shown, the zoom readout in both, and the speed in both while the view turns; the reader's own choice with the More button is remembered in localStorage, under `kazoku-view-controls` or the key `storage` names, and wins),
71
70
  * `layout` (`classic`, the default, `horizontal`, `vertical` or `compact`: where the controls sit; the horizontal, vertical and compact layouts have the zoom slider),
@@ -73,7 +72,7 @@ const HTML = `
73
72
  * or dark), `overlay` (sit over the bottom of a positioned parent).
74
73
  */
75
74
  export class ViewControlsElement extends HTMLElement {
76
- static observedAttributes = ["for", "lang", "yaw", "tilt", "zoom", "fit-scale", "ignore", "anchor", "key-tilt", "stop-turn", "tilt-step", "grab-radius", "object-size", "speed", "controls", "layout", "flat", "tilt-max", "zoom-min", "zoom-max", "address-keys"];
75
+ static observedAttributes = ["for", "lang", "yaw", "tilt", "zoom", "fit-scale", "ignore", "anchor", "key-tilt", "stop-turn", "tilt-step", "grab-radius", "object-size", "speed", "pace", "paces", "controls", "layout", "flat", "tilt-max", "zoom-min", "zoom-max", "address-keys"];
77
76
  #root;
78
77
  #controller;
79
78
  #view;
@@ -91,7 +90,7 @@ export class ViewControlsElement extends HTMLElement {
91
90
  #marksFor = "";
92
91
  /** The zoom the slider shows now; NaN until it has shown one, and again when the marks are redrawn for new limits. */
93
92
  #zoomShown = Number.NaN;
94
- #pace = "medium";
93
+ #pace = 1;
95
94
  #labels = wordsFor("en");
96
95
  #said = "";
97
96
  #sayTimer;
@@ -172,7 +171,7 @@ export class ViewControlsElement extends HTMLElement {
172
171
  Object.assign(start, chosen);
173
172
  c.turnable = !this.hasAttribute("flat");
174
173
  this.#level = parseLevel(this.#remembered()) ?? parseLevel(this.getAttribute("controls")) ?? "basic";
175
- this.#pace = pace ?? "medium";
174
+ this.#pace = this.#nearestPace(pace ?? parseSpeed(this.getAttribute("pace"), this.#paces()) ?? 1);
176
175
  this.#speed();
177
176
  c.ignore = this.getAttribute("ignore") ?? DEFAULT_IGNORE;
178
177
  for (const name of ["key-tilt", "stop-turn", "tilt-step", "grab-radius", "object-size"])
@@ -238,6 +237,11 @@ export class ViewControlsElement extends HTMLElement {
238
237
  this.#option(name);
239
238
  else if (name === "speed")
240
239
  this.#speed();
240
+ else if (name === "pace" || name === "paces") {
241
+ this.#pace = this.#nearestPace(parseSpeed(this.getAttribute("pace"), this.#paces()) ?? this.#pace);
242
+ this.#speed();
243
+ this.#show();
244
+ }
241
245
  else if (name === "layout") {
242
246
  this.#arrange();
243
247
  this.#show();
@@ -343,16 +347,16 @@ export class ViewControlsElement extends HTMLElement {
343
347
  remember(this.#storageKey(), this.#level);
344
348
  this.#show();
345
349
  });
346
- const choosePace = (pace) => {
347
- this.#pace = pace;
348
- this.#speed();
349
- this.#show();
350
- if (this.hasAttribute("address"))
351
- this.#save();
352
- };
353
- for (const pace of PACES)
354
- on(pace, () => choosePace(pace));
355
- on("pace-now", () => choosePace(PACES[(PACES.indexOf(this.#pace) + 1) % PACES.length]));
350
+ // The speed buttons are the family's own element; its choice is the pace, kept in the attribute and the address.
351
+ const speeds = q("kazoku-speed-buttons");
352
+ speeds.addEventListener(SPEED_EVENT, (event) => {
353
+ // The element's own, not one for the page to hear: the page hears `kazoku-viewchange`.
354
+ event.stopPropagation();
355
+ const { value, reason } = event.detail;
356
+ if (reason === "user")
357
+ this.#choosePace(value);
358
+ });
359
+ on("pace-now", () => speeds.step(1));
356
360
  on("left", () => c().panBy(-1, 0));
357
361
  on("right", () => c().panBy(1, 0));
358
362
  on("up", () => c().panBy(0, -1));
@@ -441,9 +445,26 @@ export class ViewControlsElement extends HTMLElement {
441
445
  return null;
442
446
  }
443
447
  }
448
+ /** The speeds the buttons stand for: the `paces` attribute, `0.5 1 2` by default. */
449
+ #paces() {
450
+ return parseSteps(this.getAttribute("paces"));
451
+ }
452
+ #nearestPace(value) {
453
+ return nearestStep(value, this.#paces()) ?? 1;
454
+ }
455
+ /** A button was pressed: the pace is the number on it. It is written to the `pace` attribute and the address. */
456
+ #choosePace(pace) {
457
+ this.#pace = pace;
458
+ this.#speed();
459
+ if (this.getAttribute("pace") !== String(pace))
460
+ this.setAttribute("pace", String(pace));
461
+ this.#show();
462
+ if (this.hasAttribute("address"))
463
+ this.#save();
464
+ }
444
465
  /** The turn's degrees a second: the `speed` attribute (30) times the pace. */
445
466
  #speed() {
446
- this.#ensure().speed = (Number(this.getAttribute("speed")) || 30) * PACE_FACTOR[this.#pace];
467
+ this.#ensure().speed = (Number(this.getAttribute("speed")) || 30) * this.#pace;
447
468
  }
448
469
  #limits() {
449
470
  const c = this.#ensure();
@@ -502,7 +523,9 @@ export class ViewControlsElement extends HTMLElement {
502
523
  set('[data-g="zoom"]', words.zoom);
503
524
  set('[data-g="pan"]', words.pan);
504
525
  set('[data-g="drag"]', words.drag);
505
- set('[data-g="pace"]', words.speed);
526
+ const speeds = this.#root.querySelector("kazoku-speed-buttons");
527
+ speeds?.setAttribute("lang", language);
528
+ speeds?.setAttribute("label", words.speed);
506
529
  set('[data-b="left"]', words.panLeft);
507
530
  set('[data-b="right"]', words.panRight);
508
531
  set('[data-b="up"]', words.panUp);
@@ -553,10 +576,16 @@ export class ViewControlsElement extends HTMLElement {
553
576
  }
554
577
  say(q('[data-b="level"]'), `${words.zoomLevel} ${zoom}, ${words.zoomReset}`);
555
578
  this.#sayZoom(`${words.zoomLevel} ${zoom}`);
556
- for (const pace of PACES)
557
- q(`[data-b="${pace}"]`).setAttribute("aria-pressed", String(this.#pace === pace));
558
- write(q('[data-o="pace"]'), words[this.#pace]);
559
- say(q('[data-b="pace-now"]'), `${words.speed}: ${words[this.#pace]}`);
579
+ const speeds = q("kazoku-speed-buttons");
580
+ const paces = this.#paces();
581
+ if (speeds.getAttribute("steps") !== paces.join(" "))
582
+ speeds.setAttribute("steps", paces.join(" "));
583
+ this.#pace = this.#nearestPace(this.#pace);
584
+ if (speeds.value !== this.#pace)
585
+ speeds.value = this.#pace;
586
+ const pace = speedText(this.#pace);
587
+ write(q('[data-o="pace"]'), pace);
588
+ say(q('[data-b="pace-now"]'), `${words.speed}: ${pace}`);
560
589
  const more = q('[data-b="more"]');
561
590
  more.setAttribute("aria-expanded", String(advanced));
562
591
  this.dataset.level = this.#level;
@@ -3,7 +3,7 @@ export type Level = "basic" | "advanced";
3
3
  export declare const parseLevel: (text: string | null | undefined) => Level | undefined;
4
4
  /** The localStorage key the level is remembered under, unless `storage` names another. */
5
5
  export declare const LEVEL_STORAGE_KEY = "kazoku-view-controls";
6
- /** Auto-turn speed: Medium is the element's `speed`, Slow half of it and Fast twice it. */
6
+ /** The three speeds of 0.12 and 0.13 by name: Medium is the element's `speed`, Slow half of it and Fast twice it. A pace is now any number (the `pace` attribute, the `paces` it chooses from); these words still read as 0.5, 1 and 2. */
7
7
  export type Pace = "slow" | "medium" | "fast";
8
8
  export declare const PACES: readonly Pace[];
9
9
  export declare const PACE_FACTOR: Record<Pace, number>;
@@ -1 +1 @@
1
- export declare const STYLES = "\n:host {\n --_ink: var(--kazoku-ink, #1b1b1f);\n --_surface: var(--kazoku-surface, #ffffff);\n --_line: var(--kazoku-line, #c9ccd3);\n --_accent: var(--kazoku-accent, #2457d6);\n display: block;\n box-sizing: border-box;\n color: var(--_ink);\n font: 600 0.9rem/1.2 system-ui, -apple-system, \"Hiragino Sans\", \"Noto Sans JP\", \"Segoe UI\", sans-serif;\n color-scheme: light dark;\n}\n@media (prefers-color-scheme: dark) {\n :host(:not([theme=\"light\"])) {\n --_ink: var(--kazoku-ink, #f1f2f6);\n --_surface: var(--kazoku-surface, #1c1d22);\n --_line: var(--kazoku-line, #4a4d58);\n --_accent: var(--kazoku-accent, #8fb0ff);\n }\n}\n:host([theme=\"dark\"]) {\n --_ink: var(--kazoku-ink, #f1f2f6);\n --_surface: var(--kazoku-surface, #1c1d22);\n --_line: var(--kazoku-line, #4a4d58);\n --_accent: var(--kazoku-accent, #8fb0ff);\n color-scheme: dark;\n}\n:host([theme=\"light\"]) { color-scheme: light; }\n:host([overlay]) { position: absolute; inset-inline: 0; bottom: 0; z-index: 1; }\n:host([overlay]) .bar { background: color-mix(in srgb, var(--_surface) 88%, transparent); }\n[hidden] { display: none !important; }\n.bar { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 12px; padding: 4px 8px; box-sizing: border-box; }\n.field { display: flex; align-items: center; gap: 8px; flex: 1 1 200px; min-width: 0; }\n.field.tilt { flex-basis: 150px; }\n.field label { flex: none; }\n.field output { flex: none; min-width: 3.2em; text-align: end; font-variant-numeric: tabular-nums; }\n.group { display: inline-flex; align-items: center; flex-wrap: wrap; gap: 4px; }\n.vh { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }\ninput[type=\"range\"] { -webkit-appearance: none; appearance: none; flex: 1 1 auto; width: 100%; min-width: 60px; height: 48px; margin: 0; padding: 0; background: transparent; cursor: pointer; }\ninput[type=\"range\"]::-webkit-slider-runnable-track { height: 6px; border-radius: 3px; background: var(--_line); }\ninput[type=\"range\"]::-moz-range-track { height: 6px; border-radius: 3px; background: var(--_line); }\ninput[type=\"range\"]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 28px; height: 28px; margin-top: -11px; border-radius: 50%; background: var(--_accent); border: 3px solid var(--_surface); box-shadow: 0 0 0 1px var(--_ink); }\ninput[type=\"range\"]::-moz-range-thumb { width: 22px; height: 22px; border-radius: 50%; background: var(--_accent); border: 3px solid var(--_surface); box-shadow: 0 0 0 1px var(--_ink); }\ninput[type=\"range\"]:focus-visible { outline: 2px solid var(--_ink); outline-offset: -4px; border-radius: 8px; }\nbutton { -webkit-appearance: none; appearance: none; box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; gap: 6px; min-width: 48px; min-height: 48px; margin: 0; padding: 0 14px; border: 1px solid var(--_line); border-radius: 24px; background: var(--_surface); color: var(--_ink); font: inherit; cursor: pointer; white-space: nowrap; }\nbutton:hover:not(:disabled) { border-color: var(--_ink); }\nbutton:focus-visible { outline: 2px solid var(--_ink); outline-offset: 2px; }\nbutton[aria-pressed=\"true\"] { background: var(--_ink); color: var(--_surface); border-color: var(--_ink); }\nbutton[aria-expanded=\"true\"] { border-color: var(--_ink); box-shadow: inset 0 0 0 1px var(--_ink); }\nbutton:disabled { opacity: 0.4; cursor: default; }\nsvg { width: 18px; height: 18px; fill: currentColor; flex: none; }\nbutton[aria-pressed=\"true\"] .i-play, button[aria-pressed=\"false\"] .i-pause { display: none; }\n.zoom button.level { min-width: 64px; padding: 0 8px; font-variant-numeric: tabular-nums; }\n.pace .lbl { margin-inline-end: 4px; }\n/* One small button: the word over the speed, so it takes little room. It comes in on Auto-turn's left, so Auto-turn stays under the pointer that just pressed it. The angle's running number makes the room, so the turning bar is never wider than the still one and adds no row; it stays for a screen reader. */\n.pace-now { flex-direction: column; gap: 0; padding: 0 10px; line-height: 1.1; }\n.pace-now .lbl { font-size: 0.7em; font-weight: 400; }\n:host([data-turning][data-level=\"basic\"]) .field.angle { flex-basis: 110px; }\n:host([data-turning][data-level=\"basic\"]) .field.angle output { position: absolute; width: 1px; min-width: 0; height: 1px; overflow: hidden; clip-path: inset(50%); }\n.drag { margin-inline-start: 8px; }\n.more { margin-inline-start: auto; }\n/* The classic bar on a phone: two rows to start with (angle, Auto-turn and More; the zoom), more rows when the reader asks for more; labels read only by a screen reader. */\n@media (max-width: 640px) {\n :host([data-layout=\"classic\"]) .bar { display: grid; grid-template-columns: minmax(0, 1fr) 48px 48px; grid-template-areas: \"angle play more\" \"tilt tilt front\" \"pace pace pace\" \"zoom zoom zoom\" \"pan pan pan\"; gap: 0 8px; padding: 2px 8px; }\n :host([data-layout=\"classic\"]) .field.angle { grid-area: angle; }\n :host([data-layout=\"classic\"]) .field.tilt { grid-area: tilt; }\n :host([data-layout=\"classic\"]) .field label, :host([data-layout=\"classic\"]) .field output, :host([data-layout=\"classic\"]) .play .text, :host([data-layout=\"classic\"]) .more .text, :host([data-layout=\"classic\"]) .pace .lbl { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }\n :host([data-layout=\"classic\"]) .play, :host([data-layout=\"classic\"]) .more { padding: 0; }\n :host([data-layout=\"classic\"]) .play { grid-area: play; }\n :host([data-layout=\"classic\"]) .more { grid-area: more; }\n :host([data-layout=\"classic\"]) .front { grid-area: front; padding: 0 12px; }\n :host([data-layout=\"classic\"]) .pace { grid-area: pace; justify-self: end; flex-wrap: nowrap; }\n :host([data-layout=\"classic\"]) .pace button { padding: 0 12px; }\n :host([data-layout=\"classic\"]) .zoom { grid-area: zoom; justify-self: end; flex-wrap: nowrap; }\n :host([data-layout=\"classic\"]) .zoom button { padding: 0 8px; }\n :host([data-layout=\"classic\"]) .zoom button.level { padding: 0 6px; }\n /* In the free room left of the zoom buttons, so showing it adds no row. */\n :host([data-layout=\"classic\"]) .pace-now { grid-area: zoom; justify-self: start; }\n :host([data-layout=\"classic\"]) .pan { grid-area: pan; justify-content: center; }\n :host([data-layout=\"classic\"]) .drag { margin-inline-start: 4px; }\n}\n/* In a panel narrower than the buttons and the speed need side by side, the speed takes the row under them. */\n:host([data-layout=\"classic\"]) { container-type: inline-size; }\n@container (max-width: 329px) {\n @media (max-width: 640px) {\n :host([data-layout=\"classic\"]) .pace-now { grid-area: pace; justify-self: end; }\n }\n}\n\n/* The zoom slider (horizontal, vertical and compact): a logarithmic track with ticks at its marks, the labels under it, and the part from the least zoom to the zoom now filled. */\n.zslide { display: none; }\n:host([data-layout=\"horizontal\"]) .zslide, :host([data-layout=\"vertical\"]) .zslide, :host([data-layout=\"compact\"]) .zslide { display: flex; }\n.zbox { position: relative; flex: 1 1 auto; min-width: 96px; height: 52px; }\n.zbox input { position: absolute; inset: 0 0 auto; z-index: 1; min-width: 0; }\n.zbox input::-webkit-slider-runnable-track { background: transparent; }\n.zbox input::-moz-range-track { background: transparent; }\n.ztrack { position: absolute; inset-inline: 14px; top: 21px; height: 6px; border-radius: 3px; background: var(--_line); }\n.zfill { position: absolute; inset-block: 0; left: 0; width: calc(var(--pos, 0) * 100%); border-radius: 3px; background: var(--_accent); }\n.ztick { position: absolute; top: 50%; left: calc(var(--at) * 100%); width: 2px; height: 14px; border-radius: 1px; background: var(--_ink); opacity: 0.5; transform: translate(-50%, -50%); }\n.zmarks { position: absolute; inset-inline: 14px; top: 36px; height: 16px; pointer-events: none; }\n.zmark { position: absolute; left: calc(var(--at) * 100%); transform: translateX(-50%); font-size: 0.72rem; font-weight: 500; line-height: 16px; opacity: 0.85; white-space: nowrap; font-variant-numeric: tabular-nums; }\n.zmark[data-end=\"start\"] { transform: translateX(-35%); }\n.zmark[data-end=\"end\"] { transform: translateX(-65%); }\n\n/* Horizontal: a tidy bar under the view. Two rows (the turn, then the zoom with its slider); one row when its own width allows. More adds its rows between them. */\n:host([data-layout=\"horizontal\"]) { container-type: inline-size; }\n:host([data-layout=\"horizontal\"]) .bar, :host([data-layout=\"vertical\"]) .bar, :host([data-layout=\"compact\"]) .bar { display: grid; row-gap: 0; align-items: center; }\n:host([data-layout=\"horizontal\"]) .bar > *, :host([data-layout=\"vertical\"]) .bar > *, :host([data-layout=\"compact\"]) .zoom > * { margin-block: 2px; }\n:host([data-layout=\"horizontal\"]) .bar { grid-template-columns: minmax(0, 1fr) auto auto auto; grid-template-areas: \"angle speed play more\" \"tilt tilt front front\" \"pace pace pace pace\" \"zoom zoom zoom centre\" \"pan pan pan pan\"; column-gap: 8px; padding: 4px 8px; }\n:host([data-layout=\"horizontal\"][data-flat]) .bar { grid-template-columns: minmax(0, 1fr) auto auto; grid-template-areas: \"zoom centre more\" \"pan pan pan\"; }\n:host([data-layout=\"horizontal\"]) .angle, :host([data-layout=\"vertical\"]) .angle, :host([data-layout=\"compact\"]) .angle { grid-area: angle; }\n:host([data-layout=\"horizontal\"]) .tilt, :host([data-layout=\"vertical\"]) .tilt, :host([data-layout=\"compact\"]) .tilt { grid-area: tilt; }\n:host([data-layout=\"horizontal\"]) .pace-now, :host([data-layout=\"vertical\"]) .pace-now { grid-area: speed; }\n:host([data-layout=\"horizontal\"]) .play, :host([data-layout=\"vertical\"]) .play, :host([data-layout=\"compact\"]) .play { grid-area: play; }\n:host([data-layout=\"horizontal\"]) .pace, :host([data-layout=\"vertical\"]) .pace, :host([data-layout=\"compact\"]) .pace { grid-area: pace; flex-wrap: nowrap; }\n:host([data-layout=\"horizontal\"]) .front, :host([data-layout=\"vertical\"]) .front, :host([data-layout=\"compact\"]) .front { grid-area: front; justify-self: start; }\n:host([data-layout=\"horizontal\"]) .zoom, :host([data-layout=\"vertical\"]) .zoom { grid-area: zoom; }\n:host([data-layout=\"horizontal\"]) .pan, :host([data-layout=\"vertical\"]) .pan, :host([data-layout=\"compact\"]) .pan { grid-area: pan; justify-self: start; }\n:host([data-layout=\"horizontal\"]) .more, :host([data-layout=\"vertical\"]) .more, :host([data-layout=\"compact\"]) .more { grid-area: more; margin-inline-start: 0; }\n/* Centre, shown once the view is zoomed or moved: beside the zoom, never in the turn's row, so Auto-turn and More stay where they are. */\n:host([data-layout=\"horizontal\"]) .centre, :host([data-layout=\"vertical\"]) .centre, :host([data-layout=\"compact\"]) .centre { grid-area: centre; justify-self: end; }\n/* In the two rows it shares More's column, so it is an icon (its name is read) and never widens that column, which would move Auto-turn. */\n@container (max-width: 1039px) {\n :host([data-layout=\"horizontal\"]) .centre .text { position: absolute; width: 1px; min-width: 0; height: 1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }\n :host([data-layout=\"horizontal\"]) .centre { padding: 0; }\n}\n:host([data-layout=\"horizontal\"]) .zoom { display: flex; flex-wrap: nowrap; gap: 4px; min-width: 0; }\n:host([data-layout=\"horizontal\"]) .zslide { flex: 1 1 auto; min-width: 0; }\n:host([data-layout=\"horizontal\"]) .zoom button, :host([data-layout=\"vertical\"]) .zoom button, :host([data-layout=\"compact\"]) .zoom button { padding: 0 10px; }\n:host([data-layout=\"horizontal\"]) .zoom button.level, :host([data-layout=\"vertical\"]) .zoom button.level, :host([data-layout=\"compact\"]) .zoom button.level { padding: 0 6px; }\n@container (max-width: 559px) {\n :host([data-layout=\"horizontal\"]) .field label, :host([data-layout=\"horizontal\"]) .field output, :host([data-layout=\"horizontal\"]) .play .text, :host([data-layout=\"horizontal\"]) .more .text, :host([data-layout=\"horizontal\"]) .pace .lbl { position: absolute; width: 1px; min-width: 0; height: 1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }\n :host([data-layout=\"horizontal\"]) .play, :host([data-layout=\"horizontal\"]) .more { padding: 0; }\n :host([data-layout=\"horizontal\"]) .zbox { min-width: 72px; }\n /* The readout is Fit here (its name says so), which leaves the slider the room for its marks. */\n :host([data-layout=\"horizontal\"]) [data-b=\"fit\"] { display: none; }\n}\n/* A phone-safe width (a phone's screen less the page's margins, and a face wider than the system's): the zoom slider takes a row of its own, so its marks never meet the buttons, and the buttons sit under it. */\n@container (max-width: 439px) {\n :host([data-layout=\"horizontal\"]) .zoom { flex-wrap: wrap; }\n :host([data-layout=\"horizontal\"]) .zslide { flex: 1 0 100%; }\n :host([data-layout=\"horizontal\"]) .centre { align-self: end; }\n :host([data-layout=\"horizontal\"]) .zbox { min-width: 0; }\n}\n/* Wide enough for the turn, Auto-turn, the zoom with its slider, Centre and More on one row, with the angle slider still a slider. */\n@container (min-width: 1040px) {\n :host([data-layout=\"horizontal\"]) .bar { grid-template-columns: minmax(0, 1fr) auto auto auto auto auto; grid-template-areas: \"angle speed play zoom centre more\" \"tilt tilt tilt pace front front\" \"pan pan pan pan pan pan\"; column-gap: 12px; }\n :host([data-layout=\"horizontal\"][data-flat]) .bar { grid-template-columns: minmax(0, 1fr) auto auto; grid-template-areas: \"zoom centre more\" \"pan pan pan\"; }\n :host([data-layout=\"horizontal\"]) .zslide { width: 15rem; }\n :host([data-layout=\"horizontal\"]) .zoom { display: grid; grid-auto-flow: column; grid-auto-columns: auto; }\n :host([data-layout=\"horizontal\"]) .pan { justify-self: end; }\n}\n\n/* Vertical: a column beside the view, its width --kazoku-column-width (16rem). Auto-turn and More at the top, so More stays put when it opens; then the turn, the zoom and its slider. */\n:host([data-layout=\"vertical\"]) { width: var(--kazoku-column-width, 16rem); max-width: 100%; }\n:host([data-layout=\"vertical\"]) .bar { grid-template-columns: auto auto minmax(0, 1fr) auto; grid-template-areas: \"play speed . more\" \"angle angle angle angle\" \"tilt tilt tilt tilt\" \"pace pace pace pace\" \"front front front front\" \"zoom zoom zoom zoom\" \"centre centre centre centre\" \"pan pan pan pan\"; column-gap: 8px; padding: 6px 8px; }\n:host([data-layout=\"vertical\"]) .field { flex: none; display: grid; grid-template-columns: minmax(0, 1fr) auto; grid-template-areas: \"label value\" \"slider slider\"; gap: 0 8px; }\n:host([data-layout=\"vertical\"]) .field label { grid-area: label; padding-top: 4px; }\n:host([data-layout=\"vertical\"]) .field output { grid-area: value; padding-top: 4px; }\n:host([data-layout=\"vertical\"]) .field input, :host([data-layout=\"vertical\"]) .zbox { grid-area: slider; }\n:host([data-layout=\"vertical\"]) .zoom { display: grid; grid-template-columns: auto auto auto minmax(0, 1fr); grid-template-areas: \"zs zs zs zs\" \"out level in fit\"; gap: 0 4px; }\n:host([data-layout=\"vertical\"]) .zslide { grid-area: zs; }\n:host([data-layout=\"vertical\"]) [data-b=\"out\"] { grid-area: out; }\n:host([data-layout=\"vertical\"]) [data-b=\"level\"] { grid-area: level; }\n:host([data-layout=\"vertical\"]) [data-b=\"in\"] { grid-area: in; }\n:host([data-layout=\"vertical\"]) [data-b=\"fit\"] { grid-area: fit; justify-self: start; }\n:host([data-layout=\"vertical\"]) .pace .lbl { position: absolute; width: 1px; min-width: 0; height: 1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }\n:host([data-layout=\"vertical\"]) .pace button { padding: 0 10px; }\n:host([data-layout=\"vertical\"]) .pan { flex-wrap: wrap; }\n:host([data-layout=\"vertical\"]) .drag { margin-inline-start: 0; }\n\n/* Compact: one short row (Auto-turn, zoom out, the zoom now, zoom in, More) for a card or a phone. More opens the rest under it: the zoom slider and Fit, the angle, and the advanced controls. */\n:host([data-layout=\"compact\"]) { display: block; }\n:host([data-layout=\"compact\"][overlay]) { inset-inline: 8px auto; bottom: 8px; }\n/* Over a card, an open More never reaches past the card's top: the controls scroll inside themselves. */\n:host([data-layout=\"compact\"][overlay]) { display: flex; flex-direction: column; max-height: calc(100% - 16px); }\n:host([data-layout=\"compact\"][overlay]) .bar { min-height: 0; overflow-y: auto; }\n:host([data-layout=\"compact\"]) .bar { width: fit-content; max-width: 100%; grid-template-columns: repeat(5, auto); grid-template-areas: \"play out level in more\" \"zs zs zs zs fit\" \"centre centre centre centre centre\" \"angle angle angle angle angle\" \"tilt tilt tilt tilt tilt\" \"pace pace pace pace pace\" \"front front front front front\" \"pan pan pan pan pan\"; column-gap: 2px; padding: 2px 4px; border: 1px solid var(--_line); border-radius: 28px; background: var(--_surface); }\n:host([data-layout=\"compact\"][data-level=\"advanced\"]) .bar { border-radius: 20px; padding: 2px 8px 6px; column-gap: 4px; }\n:host([data-layout=\"compact\"][data-flat]) .bar { grid-template-areas: \"out level in more .\" \"zs zs zs fit fit\" \"centre centre centre centre centre\" \"pan pan pan pan pan\"; }\n:host([data-layout=\"compact\"]) .zoom { display: contents; }\n:host([data-layout=\"compact\"]) .zslide { grid-area: zs; }\n:host([data-layout=\"compact\"]) [data-b=\"out\"] { grid-area: out; }\n:host([data-layout=\"compact\"]) [data-b=\"level\"] { grid-area: level; }\n:host([data-layout=\"compact\"]) [data-b=\"in\"] { grid-area: in; }\n:host([data-layout=\"compact\"]) [data-b=\"fit\"] { grid-area: fit; }\n:host([data-layout=\"compact\"]) .play, :host([data-layout=\"compact\"]) .more, :host([data-layout=\"compact\"]) [data-b=\"out\"], :host([data-layout=\"compact\"]) [data-b=\"level\"], :host([data-layout=\"compact\"]) [data-b=\"in\"] { border-color: transparent; }\n:host([data-layout=\"compact\"]) .play:hover:not(:disabled), :host([data-layout=\"compact\"]) .more:hover:not(:disabled), :host([data-layout=\"compact\"]) .zoom button:hover:not(:disabled) { border-color: var(--_ink); }\n:host([data-layout=\"compact\"]) .play .text, :host([data-layout=\"compact\"]) .more .text { position: absolute; width: 1px; min-width: 0; height: 1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }\n:host([data-layout=\"compact\"]) .play, :host([data-layout=\"compact\"]) .more { padding: 0; }\n:host([data-layout=\"compact\"]) .pace-now { display: none; }\n:host([data-layout=\"compact\"]) .pace { flex-wrap: wrap; }\n:host([data-layout=\"vertical\"]) .centre, :host([data-layout=\"compact\"]) .centre { justify-self: start; }\n/* The row stays one row: Centre is with Fit, one press of More away. */\n:host([data-layout=\"compact\"][data-level=\"basic\"]) .centre { display: none; }\n:host([data-layout=\"compact\"][data-level=\"basic\"]) .angle, :host([data-layout=\"compact\"][data-level=\"basic\"]) .zslide, :host([data-layout=\"compact\"][data-level=\"basic\"]) [data-b=\"fit\"] { display: none; }\n";
1
+ export declare const STYLES = "\n:host {\n --_ink: var(--kazoku-ink, #1b1b1f);\n --_surface: var(--kazoku-surface, #ffffff);\n --_line: var(--kazoku-line, #c9ccd3);\n --_accent: var(--kazoku-accent, #2457d6);\n display: block;\n box-sizing: border-box;\n color: var(--_ink);\n font: 600 0.9rem/1.2 system-ui, -apple-system, \"Hiragino Sans\", \"Noto Sans JP\", \"Segoe UI\", sans-serif;\n color-scheme: light dark;\n}\n@media (prefers-color-scheme: dark) {\n :host(:not([theme=\"light\"])) {\n --_ink: var(--kazoku-ink, #f1f2f6);\n --_surface: var(--kazoku-surface, #1c1d22);\n --_line: var(--kazoku-line, #4a4d58);\n --_accent: var(--kazoku-accent, #8fb0ff);\n }\n}\n:host([theme=\"dark\"]) {\n --_ink: var(--kazoku-ink, #f1f2f6);\n --_surface: var(--kazoku-surface, #1c1d22);\n --_line: var(--kazoku-line, #4a4d58);\n --_accent: var(--kazoku-accent, #8fb0ff);\n color-scheme: dark;\n}\n:host([theme=\"light\"]) { color-scheme: light; }\n:host([overlay]) { position: absolute; inset-inline: 0; bottom: 0; z-index: 1; }\n:host([overlay]) .bar { background: color-mix(in srgb, var(--_surface) 88%, transparent); }\n[hidden] { display: none !important; }\n.bar { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 12px; padding: 4px 8px; box-sizing: border-box; }\n.field { display: flex; align-items: center; gap: 8px; flex: 1 1 200px; min-width: 0; }\n.field.tilt { flex-basis: 150px; }\n.field label { flex: none; }\n.field output { flex: none; min-width: 3.2em; text-align: end; font-variant-numeric: tabular-nums; }\n.group { display: inline-flex; align-items: center; flex-wrap: wrap; gap: 4px; }\n.vh { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }\ninput[type=\"range\"] { -webkit-appearance: none; appearance: none; flex: 1 1 auto; width: 100%; min-width: 60px; height: 48px; margin: 0; padding: 0; background: transparent; cursor: pointer; }\ninput[type=\"range\"]::-webkit-slider-runnable-track { height: 6px; border-radius: 3px; background: var(--_line); }\ninput[type=\"range\"]::-moz-range-track { height: 6px; border-radius: 3px; background: var(--_line); }\ninput[type=\"range\"]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 28px; height: 28px; margin-top: -11px; border-radius: 50%; background: var(--_accent); border: 3px solid var(--_surface); box-shadow: 0 0 0 1px var(--_ink); }\ninput[type=\"range\"]::-moz-range-thumb { width: 22px; height: 22px; border-radius: 50%; background: var(--_accent); border: 3px solid var(--_surface); box-shadow: 0 0 0 1px var(--_ink); }\ninput[type=\"range\"]:focus-visible { outline: 2px solid var(--_ink); outline-offset: -4px; border-radius: 8px; }\nbutton { -webkit-appearance: none; appearance: none; box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; gap: 6px; min-width: 48px; min-height: 48px; margin: 0; padding: 0 14px; border: 1px solid var(--_line); border-radius: 24px; background: var(--_surface); color: var(--_ink); font: inherit; cursor: pointer; white-space: nowrap; }\nbutton:hover:not(:disabled) { border-color: var(--_ink); }\nbutton:focus-visible { outline: 2px solid var(--_ink); outline-offset: 2px; }\nbutton[aria-pressed=\"true\"] { background: var(--_ink); color: var(--_surface); border-color: var(--_ink); }\nbutton[aria-expanded=\"true\"] { border-color: var(--_ink); box-shadow: inset 0 0 0 1px var(--_ink); }\nbutton:disabled { opacity: 0.4; cursor: default; }\nsvg { width: 18px; height: 18px; fill: currentColor; flex: none; }\nbutton[aria-pressed=\"true\"] .i-play, button[aria-pressed=\"false\"] .i-pause { display: none; }\n.zoom button.level { min-width: 64px; padding: 0 8px; font-variant-numeric: tabular-nums; }\n/* The speed buttons are an element of their own: they are given the colours this bar has, set on the bar so that the buttons' own variables never read themselves. */\n.bar { --_bar-ink: var(--_ink); --_bar-surface: var(--_surface); --_bar-line: var(--_line); --_bar-accent: var(--_accent); }\n.pace { --kazoku-ink: var(--_bar-ink); --kazoku-surface: var(--_bar-surface); --kazoku-line: var(--_bar-line); --kazoku-accent: var(--_bar-accent); }\n/* One small button: the word over the speed, so it takes little room. It comes in on Auto-turn's left, so Auto-turn stays under the pointer that just pressed it. The angle's running number makes the room, so the turning bar is never wider than the still one and adds no row; it stays for a screen reader. */\n.pace-now { flex-direction: column; gap: 0; padding: 0 10px; line-height: 1.1; }\n.pace-now .lbl { font-size: 0.7em; font-weight: 400; }\n:host([data-turning][data-level=\"basic\"]) .field.angle { flex-basis: 110px; }\n:host([data-turning][data-level=\"basic\"]) .field.angle output { position: absolute; width: 1px; min-width: 0; height: 1px; overflow: hidden; clip-path: inset(50%); }\n.drag { margin-inline-start: 8px; }\n.more { margin-inline-start: auto; }\n/* The classic bar on a phone: two rows to start with (angle, Auto-turn and More; the zoom), more rows when the reader asks for more; labels read only by a screen reader. */\n@media (max-width: 640px) {\n :host([data-layout=\"classic\"]) .bar { display: grid; grid-template-columns: minmax(0, 1fr) 48px 48px; grid-template-areas: \"angle play more\" \"tilt tilt front\" \"pace pace pace\" \"zoom zoom zoom\" \"pan pan pan\"; gap: 0 8px; padding: 2px 8px; }\n :host([data-layout=\"classic\"]) .field.angle { grid-area: angle; }\n :host([data-layout=\"classic\"]) .field.tilt { grid-area: tilt; }\n :host([data-layout=\"classic\"]) .field label, :host([data-layout=\"classic\"]) .field output, :host([data-layout=\"classic\"]) .play .text, :host([data-layout=\"classic\"]) .more .text, :host([data-layout=\"classic\"]) .pace::part(label) { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }\n :host([data-layout=\"classic\"]) .play, :host([data-layout=\"classic\"]) .more { padding: 0; }\n :host([data-layout=\"classic\"]) .play { grid-area: play; }\n :host([data-layout=\"classic\"]) .more { grid-area: more; }\n :host([data-layout=\"classic\"]) .front { grid-area: front; padding: 0 12px; }\n :host([data-layout=\"classic\"]) .pace { grid-area: pace; justify-self: end; flex-wrap: nowrap; }\n :host([data-layout=\"classic\"]) .zoom { grid-area: zoom; justify-self: end; flex-wrap: nowrap; }\n :host([data-layout=\"classic\"]) .zoom button { padding: 0 8px; }\n :host([data-layout=\"classic\"]) .zoom button.level { padding: 0 6px; }\n /* In the free room left of the zoom buttons, so showing it adds no row. */\n :host([data-layout=\"classic\"]) .pace-now { grid-area: zoom; justify-self: start; }\n :host([data-layout=\"classic\"]) .pan { grid-area: pan; justify-content: center; }\n :host([data-layout=\"classic\"]) .drag { margin-inline-start: 4px; }\n}\n/* In a panel narrower than the buttons and the speed need side by side, the speed takes the row under them. */\n:host([data-layout=\"classic\"]) { container-type: inline-size; }\n@container (max-width: 329px) {\n @media (max-width: 640px) {\n :host([data-layout=\"classic\"]) .pace-now { grid-area: pace; justify-self: end; }\n }\n}\n\n/* The zoom slider (horizontal, vertical and compact): a logarithmic track with ticks at its marks, the labels under it, and the part from the least zoom to the zoom now filled. */\n.zslide { display: none; }\n:host([data-layout=\"horizontal\"]) .zslide, :host([data-layout=\"vertical\"]) .zslide, :host([data-layout=\"compact\"]) .zslide { display: flex; }\n.zbox { position: relative; flex: 1 1 auto; min-width: 96px; height: 52px; }\n.zbox input { position: absolute; inset: 0 0 auto; z-index: 1; min-width: 0; }\n.zbox input::-webkit-slider-runnable-track { background: transparent; }\n.zbox input::-moz-range-track { background: transparent; }\n.ztrack { position: absolute; inset-inline: 14px; top: 21px; height: 6px; border-radius: 3px; background: var(--_line); }\n.zfill { position: absolute; inset-block: 0; left: 0; width: calc(var(--pos, 0) * 100%); border-radius: 3px; background: var(--_accent); }\n.ztick { position: absolute; top: 50%; left: calc(var(--at) * 100%); width: 2px; height: 14px; border-radius: 1px; background: var(--_ink); opacity: 0.5; transform: translate(-50%, -50%); }\n.zmarks { position: absolute; inset-inline: 14px; top: 36px; height: 16px; pointer-events: none; }\n.zmark { position: absolute; left: calc(var(--at) * 100%); transform: translateX(-50%); font-size: 0.72rem; font-weight: 500; line-height: 16px; opacity: 0.85; white-space: nowrap; font-variant-numeric: tabular-nums; }\n.zmark[data-end=\"start\"] { transform: translateX(-35%); }\n.zmark[data-end=\"end\"] { transform: translateX(-65%); }\n\n/* Horizontal: a tidy bar under the view. Two rows (the turn, then the zoom with its slider); one row when its own width allows. More adds its rows between them. */\n:host([data-layout=\"horizontal\"]) { container-type: inline-size; }\n:host([data-layout=\"horizontal\"]) .bar, :host([data-layout=\"vertical\"]) .bar, :host([data-layout=\"compact\"]) .bar { display: grid; row-gap: 0; align-items: center; }\n:host([data-layout=\"horizontal\"]) .bar > *, :host([data-layout=\"vertical\"]) .bar > *, :host([data-layout=\"compact\"]) .zoom > * { margin-block: 2px; }\n:host([data-layout=\"horizontal\"]) .bar { grid-template-columns: minmax(0, 1fr) auto auto auto; grid-template-areas: \"angle speed play more\" \"tilt tilt front front\" \"pace pace pace pace\" \"zoom zoom zoom centre\" \"pan pan pan pan\"; column-gap: 8px; padding: 4px 8px; }\n:host([data-layout=\"horizontal\"][data-flat]) .bar { grid-template-columns: minmax(0, 1fr) auto auto; grid-template-areas: \"zoom centre more\" \"pan pan pan\"; }\n:host([data-layout=\"horizontal\"]) .angle, :host([data-layout=\"vertical\"]) .angle, :host([data-layout=\"compact\"]) .angle { grid-area: angle; }\n:host([data-layout=\"horizontal\"]) .tilt, :host([data-layout=\"vertical\"]) .tilt, :host([data-layout=\"compact\"]) .tilt { grid-area: tilt; }\n:host([data-layout=\"horizontal\"]) .pace-now, :host([data-layout=\"vertical\"]) .pace-now { grid-area: speed; }\n:host([data-layout=\"horizontal\"]) .play, :host([data-layout=\"vertical\"]) .play, :host([data-layout=\"compact\"]) .play { grid-area: play; }\n:host([data-layout=\"horizontal\"]) .pace, :host([data-layout=\"vertical\"]) .pace, :host([data-layout=\"compact\"]) .pace { grid-area: pace; flex-wrap: nowrap; }\n:host([data-layout=\"horizontal\"]) .front, :host([data-layout=\"vertical\"]) .front, :host([data-layout=\"compact\"]) .front { grid-area: front; justify-self: start; }\n:host([data-layout=\"horizontal\"]) .zoom, :host([data-layout=\"vertical\"]) .zoom { grid-area: zoom; }\n:host([data-layout=\"horizontal\"]) .pan, :host([data-layout=\"vertical\"]) .pan, :host([data-layout=\"compact\"]) .pan { grid-area: pan; justify-self: start; }\n:host([data-layout=\"horizontal\"]) .more, :host([data-layout=\"vertical\"]) .more, :host([data-layout=\"compact\"]) .more { grid-area: more; margin-inline-start: 0; }\n/* Centre, shown once the view is zoomed or moved: beside the zoom, never in the turn's row, so Auto-turn and More stay where they are. */\n:host([data-layout=\"horizontal\"]) .centre, :host([data-layout=\"vertical\"]) .centre, :host([data-layout=\"compact\"]) .centre { grid-area: centre; justify-self: end; }\n/* In the two rows it shares More's column, so it is an icon (its name is read) and never widens that column, which would move Auto-turn. */\n@container (max-width: 1039px) {\n :host([data-layout=\"horizontal\"]) .centre .text { position: absolute; width: 1px; min-width: 0; height: 1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }\n :host([data-layout=\"horizontal\"]) .centre { padding: 0; }\n}\n:host([data-layout=\"horizontal\"]) .zoom { display: flex; flex-wrap: nowrap; gap: 4px; min-width: 0; }\n:host([data-layout=\"horizontal\"]) .zslide { flex: 1 1 auto; min-width: 0; }\n:host([data-layout=\"horizontal\"]) .zoom button, :host([data-layout=\"vertical\"]) .zoom button, :host([data-layout=\"compact\"]) .zoom button { padding: 0 10px; }\n:host([data-layout=\"horizontal\"]) .zoom button.level, :host([data-layout=\"vertical\"]) .zoom button.level, :host([data-layout=\"compact\"]) .zoom button.level { padding: 0 6px; }\n@container (max-width: 559px) {\n :host([data-layout=\"horizontal\"]) .field label, :host([data-layout=\"horizontal\"]) .field output, :host([data-layout=\"horizontal\"]) .play .text, :host([data-layout=\"horizontal\"]) .more .text, :host([data-layout=\"horizontal\"]) .pace::part(label) { position: absolute; width: 1px; min-width: 0; height: 1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }\n :host([data-layout=\"horizontal\"]) .play, :host([data-layout=\"horizontal\"]) .more { padding: 0; }\n :host([data-layout=\"horizontal\"]) .zbox { min-width: 72px; }\n /* The readout is Fit here (its name says so), which leaves the slider the room for its marks. */\n :host([data-layout=\"horizontal\"]) [data-b=\"fit\"] { display: none; }\n}\n/* A phone-safe width (a phone's screen less the page's margins, and a face wider than the system's): the zoom slider takes a row of its own, so its marks never meet the buttons, and the buttons sit under it. */\n@container (max-width: 439px) {\n :host([data-layout=\"horizontal\"]) .zoom { flex-wrap: wrap; }\n :host([data-layout=\"horizontal\"]) .zslide { flex: 1 0 100%; }\n :host([data-layout=\"horizontal\"]) .centre { align-self: end; }\n :host([data-layout=\"horizontal\"]) .zbox { min-width: 0; }\n}\n/* Wide enough for the turn, Auto-turn, the zoom with its slider, Centre and More on one row, with the angle slider still a slider. */\n@container (min-width: 1040px) {\n :host([data-layout=\"horizontal\"]) .bar { grid-template-columns: minmax(0, 1fr) auto auto auto auto auto; grid-template-areas: \"angle speed play zoom centre more\" \"tilt tilt tilt pace front front\" \"pan pan pan pan pan pan\"; column-gap: 12px; }\n :host([data-layout=\"horizontal\"][data-flat]) .bar { grid-template-columns: minmax(0, 1fr) auto auto; grid-template-areas: \"zoom centre more\" \"pan pan pan\"; }\n :host([data-layout=\"horizontal\"]) .zslide { width: 15rem; }\n :host([data-layout=\"horizontal\"]) .zoom { display: grid; grid-auto-flow: column; grid-auto-columns: auto; }\n :host([data-layout=\"horizontal\"]) .pan { justify-self: end; }\n}\n\n/* Vertical: a column beside the view, its width --kazoku-column-width (16rem). Auto-turn and More at the top, so More stays put when it opens; then the turn, the zoom and its slider. */\n:host([data-layout=\"vertical\"]) { width: var(--kazoku-column-width, 16rem); max-width: 100%; }\n:host([data-layout=\"vertical\"]) .bar { grid-template-columns: auto auto minmax(0, 1fr) auto; grid-template-areas: \"play speed . more\" \"angle angle angle angle\" \"tilt tilt tilt tilt\" \"pace pace pace pace\" \"front front front front\" \"zoom zoom zoom zoom\" \"centre centre centre centre\" \"pan pan pan pan\"; column-gap: 8px; padding: 6px 8px; }\n:host([data-layout=\"vertical\"]) .field { flex: none; display: grid; grid-template-columns: minmax(0, 1fr) auto; grid-template-areas: \"label value\" \"slider slider\"; gap: 0 8px; }\n:host([data-layout=\"vertical\"]) .field label { grid-area: label; padding-top: 4px; }\n:host([data-layout=\"vertical\"]) .field output { grid-area: value; padding-top: 4px; }\n:host([data-layout=\"vertical\"]) .field input, :host([data-layout=\"vertical\"]) .zbox { grid-area: slider; }\n:host([data-layout=\"vertical\"]) .zoom { display: grid; grid-template-columns: auto auto auto minmax(0, 1fr); grid-template-areas: \"zs zs zs zs\" \"out level in fit\"; gap: 0 4px; }\n:host([data-layout=\"vertical\"]) .zslide { grid-area: zs; }\n:host([data-layout=\"vertical\"]) [data-b=\"out\"] { grid-area: out; }\n:host([data-layout=\"vertical\"]) [data-b=\"level\"] { grid-area: level; }\n:host([data-layout=\"vertical\"]) [data-b=\"in\"] { grid-area: in; }\n:host([data-layout=\"vertical\"]) [data-b=\"fit\"] { grid-area: fit; justify-self: start; }\n:host([data-layout=\"vertical\"]) .pace::part(label) { position: absolute; width: 1px; min-width: 0; height: 1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }\n:host([data-layout=\"vertical\"]) .pan { flex-wrap: wrap; }\n:host([data-layout=\"vertical\"]) .drag { margin-inline-start: 0; }\n\n/* Compact: one short row (Auto-turn, zoom out, the zoom now, zoom in, More) for a card or a phone. More opens the rest under it: the zoom slider and Fit, the angle, and the advanced controls. */\n:host([data-layout=\"compact\"]) { display: block; }\n:host([data-layout=\"compact\"][overlay]) { inset-inline: 8px auto; bottom: 8px; }\n/* Over a card, an open More never reaches past the card's top: the controls scroll inside themselves. */\n:host([data-layout=\"compact\"][overlay]) { display: flex; flex-direction: column; max-height: calc(100% - 16px); }\n:host([data-layout=\"compact\"][overlay]) .bar { min-height: 0; overflow-y: auto; }\n:host([data-layout=\"compact\"]) .bar { width: fit-content; max-width: 100%; grid-template-columns: repeat(5, auto); grid-template-areas: \"play out level in more\" \"zs zs zs zs fit\" \"centre centre centre centre centre\" \"angle angle angle angle angle\" \"tilt tilt tilt tilt tilt\" \"pace pace pace pace pace\" \"front front front front front\" \"pan pan pan pan pan\"; column-gap: 2px; padding: 2px 4px; border: 1px solid var(--_line); border-radius: 28px; background: var(--_surface); }\n:host([data-layout=\"compact\"][data-level=\"advanced\"]) .bar { border-radius: 20px; padding: 2px 8px 6px; column-gap: 4px; }\n:host([data-layout=\"compact\"][data-flat]) .bar { grid-template-areas: \"out level in more .\" \"zs zs zs fit fit\" \"centre centre centre centre centre\" \"pan pan pan pan pan\"; }\n:host([data-layout=\"compact\"]) .zoom { display: contents; }\n:host([data-layout=\"compact\"]) .zslide { grid-area: zs; }\n:host([data-layout=\"compact\"]) [data-b=\"out\"] { grid-area: out; }\n:host([data-layout=\"compact\"]) [data-b=\"level\"] { grid-area: level; }\n:host([data-layout=\"compact\"]) [data-b=\"in\"] { grid-area: in; }\n:host([data-layout=\"compact\"]) [data-b=\"fit\"] { grid-area: fit; }\n:host([data-layout=\"compact\"]) .play, :host([data-layout=\"compact\"]) .more, :host([data-layout=\"compact\"]) [data-b=\"out\"], :host([data-layout=\"compact\"]) [data-b=\"level\"], :host([data-layout=\"compact\"]) [data-b=\"in\"] { border-color: transparent; }\n:host([data-layout=\"compact\"]) .play:hover:not(:disabled), :host([data-layout=\"compact\"]) .more:hover:not(:disabled), :host([data-layout=\"compact\"]) .zoom button:hover:not(:disabled) { border-color: var(--_ink); }\n:host([data-layout=\"compact\"]) .play .text, :host([data-layout=\"compact\"]) .more .text { position: absolute; width: 1px; min-width: 0; height: 1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }\n:host([data-layout=\"compact\"]) .play, :host([data-layout=\"compact\"]) .more { padding: 0; }\n:host([data-layout=\"compact\"]) .pace-now { display: none; }\n:host([data-layout=\"compact\"]) .pace { flex-wrap: wrap; }\n:host([data-layout=\"vertical\"]) .centre, :host([data-layout=\"compact\"]) .centre { justify-self: start; }\n/* The row stays one row: Centre is with Fit, one press of More away. */\n:host([data-layout=\"compact\"][data-level=\"basic\"]) .centre { display: none; }\n:host([data-layout=\"compact\"][data-level=\"basic\"]) .angle, :host([data-layout=\"compact\"][data-level=\"basic\"]) .zslide, :host([data-layout=\"compact\"][data-level=\"basic\"]) [data-b=\"fit\"] { display: none; }\n";
@@ -59,7 +59,9 @@ button:disabled { opacity: 0.4; cursor: default; }
59
59
  svg { width: 18px; height: 18px; fill: currentColor; flex: none; }
60
60
  button[aria-pressed="true"] .i-play, button[aria-pressed="false"] .i-pause { display: none; }
61
61
  .zoom button.level { min-width: 64px; padding: 0 8px; font-variant-numeric: tabular-nums; }
62
- .pace .lbl { margin-inline-end: 4px; }
62
+ /* The speed buttons are an element of their own: they are given the colours this bar has, set on the bar so that the buttons' own variables never read themselves. */
63
+ .bar { --_bar-ink: var(--_ink); --_bar-surface: var(--_surface); --_bar-line: var(--_line); --_bar-accent: var(--_accent); }
64
+ .pace { --kazoku-ink: var(--_bar-ink); --kazoku-surface: var(--_bar-surface); --kazoku-line: var(--_bar-line); --kazoku-accent: var(--_bar-accent); }
63
65
  /* One small button: the word over the speed, so it takes little room. It comes in on Auto-turn's left, so Auto-turn stays under the pointer that just pressed it. The angle's running number makes the room, so the turning bar is never wider than the still one and adds no row; it stays for a screen reader. */
64
66
  .pace-now { flex-direction: column; gap: 0; padding: 0 10px; line-height: 1.1; }
65
67
  .pace-now .lbl { font-size: 0.7em; font-weight: 400; }
@@ -72,13 +74,12 @@ button[aria-pressed="true"] .i-play, button[aria-pressed="false"] .i-pause { dis
72
74
  ${CLASSIC} .bar { display: grid; grid-template-columns: minmax(0, 1fr) 48px 48px; grid-template-areas: "angle play more" "tilt tilt front" "pace pace pace" "zoom zoom zoom" "pan pan pan"; gap: 0 8px; padding: 2px 8px; }
73
75
  ${CLASSIC} .field.angle { grid-area: angle; }
74
76
  ${CLASSIC} .field.tilt { grid-area: tilt; }
75
- ${CLASSIC} .field label, ${CLASSIC} .field output, ${CLASSIC} .play .text, ${CLASSIC} .more .text, ${CLASSIC} .pace .lbl { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
77
+ ${CLASSIC} .field label, ${CLASSIC} .field output, ${CLASSIC} .play .text, ${CLASSIC} .more .text, ${CLASSIC} .pace::part(label) { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
76
78
  ${CLASSIC} .play, ${CLASSIC} .more { padding: 0; }
77
79
  ${CLASSIC} .play { grid-area: play; }
78
80
  ${CLASSIC} .more { grid-area: more; }
79
81
  ${CLASSIC} .front { grid-area: front; padding: 0 12px; }
80
82
  ${CLASSIC} .pace { grid-area: pace; justify-self: end; flex-wrap: nowrap; }
81
- ${CLASSIC} .pace button { padding: 0 12px; }
82
83
  ${CLASSIC} .zoom { grid-area: zoom; justify-self: end; flex-wrap: nowrap; }
83
84
  ${CLASSIC} .zoom button { padding: 0 8px; }
84
85
  ${CLASSIC} .zoom button.level { padding: 0 6px; }
@@ -137,7 +138,7 @@ ${H} .zslide { flex: 1 1 auto; min-width: 0; }
137
138
  ${H} .zoom button, ${V} .zoom button, ${C} .zoom button { padding: 0 10px; }
138
139
  ${H} .zoom button.level, ${V} .zoom button.level, ${C} .zoom button.level { padding: 0 6px; }
139
140
  @container (max-width: 559px) {
140
- ${H} .field label, ${H} .field output, ${H} .play .text, ${H} .more .text, ${H} .pace .lbl { ${UNSEEN} }
141
+ ${H} .field label, ${H} .field output, ${H} .play .text, ${H} .more .text, ${H} .pace::part(label) { ${UNSEEN} }
141
142
  ${H} .play, ${H} .more { padding: 0; }
142
143
  ${H} .zbox { min-width: 72px; }
143
144
  /* The readout is Fit here (its name says so), which leaves the slider the room for its marks. */
@@ -172,8 +173,7 @@ ${V} [data-b="out"] { grid-area: out; }
172
173
  ${V} [data-b="level"] { grid-area: level; }
173
174
  ${V} [data-b="in"] { grid-area: in; }
174
175
  ${V} [data-b="fit"] { grid-area: fit; justify-self: start; }
175
- ${V} .pace .lbl { ${UNSEEN} }
176
- ${V} .pace button { padding: 0 10px; }
176
+ ${V} .pace::part(label) { ${UNSEEN} }
177
177
  ${V} .pan { flex-wrap: wrap; }
178
178
  ${V} .drag { margin-inline-start: 0; }
179
179
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@johnmorrisdotca/kazoku-ui",
3
- "version": "0.13.1",
4
- "description": "Kazoku UI 家族, the family's shared interface parts as their own versioned package: view controls for any 3D or zoomable view (angle, tilt, auto-turn, zoom about the pointer, pan, touch, address state) a light, dark and auto theme switch, a language picker, choices kept in the address and a save-as-image button, each a framework-free custom element and a plain controller. Zero dependencies; the shared 3D renderer takes three.js as an optional peer.",
3
+ "version": "0.15.0",
4
+ "description": "Kazoku UI 家族, the family's shared interface parts as their own versioned package: view controls for any 3D or zoomable view (angle, tilt, auto-turn, zoom about the pointer, pan, touch, address state) a light, dark and auto theme switch, a language picker, choices kept in the address, a save-as-image button and one set of speed buttons (0.5×, 1×, 2× or your own steps) and the \"Put it in your page\" embed panel (a compact picker, sizes, HTML, ES module, React, Vue, Angular, Svelte and iframe tabs, a picture that fits its box, Copy), each a framework-free custom element and a plain controller. Zero dependencies; the shared 3D renderer takes three.js as an optional peer.",
5
5
  "keywords": [
6
6
  "view-controls",
7
7
  "3d",
@@ -30,7 +30,14 @@
30
30
  "webgl",
31
31
  "three",
32
32
  "three.js",
33
- "renderer"
33
+ "renderer",
34
+ "speed-buttons",
35
+ "playback-speed",
36
+ "time-speed",
37
+ "embed",
38
+ "embed-panel",
39
+ "snippets",
40
+ "iframe"
34
41
  ],
35
42
  "homepage": "https://johnmorrisdotca.github.io/kazoku-ui/",
36
43
  "repository": {
@@ -47,7 +54,9 @@
47
54
  "./dist/view-controls/define.js",
48
55
  "./dist/theme-switch/define.js",
49
56
  "./dist/language-picker/define.js",
50
- "./dist/save-image/define.js"
57
+ "./dist/save-image/define.js",
58
+ "./dist/speed-buttons/define.js",
59
+ "./dist/embed-panel/define.js"
51
60
  ],
52
61
  "main": "./dist/index.js",
53
62
  "module": "./dist/index.js",
@@ -123,6 +132,36 @@
123
132
  "import": "./dist/save-image/define.js",
124
133
  "default": "./dist/save-image/define.js"
125
134
  },
135
+ "./speed-buttons": {
136
+ "types": "./dist/speed-buttons/index.d.ts",
137
+ "import": "./dist/speed-buttons/index.js",
138
+ "default": "./dist/speed-buttons/index.js"
139
+ },
140
+ "./speed-buttons/element": {
141
+ "types": "./dist/speed-buttons/element.d.ts",
142
+ "import": "./dist/speed-buttons/element.js",
143
+ "default": "./dist/speed-buttons/element.js"
144
+ },
145
+ "./speed-buttons/define": {
146
+ "types": "./dist/speed-buttons/define.d.ts",
147
+ "import": "./dist/speed-buttons/define.js",
148
+ "default": "./dist/speed-buttons/define.js"
149
+ },
150
+ "./embed-panel": {
151
+ "types": "./dist/embed-panel/index.d.ts",
152
+ "import": "./dist/embed-panel/index.js",
153
+ "default": "./dist/embed-panel/index.js"
154
+ },
155
+ "./embed-panel/element": {
156
+ "types": "./dist/embed-panel/element.d.ts",
157
+ "import": "./dist/embed-panel/element.js",
158
+ "default": "./dist/embed-panel/element.js"
159
+ },
160
+ "./embed-panel/define": {
161
+ "types": "./dist/embed-panel/define.d.ts",
162
+ "import": "./dist/embed-panel/define.js",
163
+ "default": "./dist/embed-panel/define.js"
164
+ },
126
165
  "./renderer": {
127
166
  "types": "./dist/renderer/index.d.ts",
128
167
  "import": "./dist/renderer/index.js",