@johnmorrisdotca/kazoku-ui 0.15.2 → 0.16.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 +8 -0
- package/README.md +6 -5
- package/dist/view-controls/address.d.ts +5 -0
- package/dist/view-controls/address.js +7 -2
- package/dist/view-controls/element.d.ts +3 -2
- package/dist/view-controls/element.js +75 -14
- package/dist/view-controls/levels.d.ts +3 -0
- package/dist/view-controls/levels.js +1 -0
- package/dist/view-controls/styles.d.ts +1 -1
- package/dist/view-controls/styles.js +6 -3
- package/dist/view-controls/words.d.ts +4 -0
- package/dist/view-controls/words.js +6 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.16.0] - 2026-11-04
|
|
6
|
+
|
|
7
|
+
Which way Auto-turn goes is the reader's to choose.
|
|
8
|
+
|
|
9
|
+
- **Left and Right buttons** (KAZOKU-UI-34 and KAZOKU-UI-22). Under More, beside the speed buttons, a pair of buttons chooses which way Auto-turn goes. The choice is kept in the new `direction` attribute (`left` or `right`) and in the address as `?direction=left`; the angle goes on from where it was, at the same speed. By the drag rule `yaw` goes up as the hand goes right, so right is the positive `speed`. With no `direction`, the sign of `speed` decides (a negative `speed` starts the turn to the left, as before), and the address keeps a way only when it is not the page's own start. `address-keys` can rename the key (`direction:dir`). New words in both languages: `direction` (Turn direction, 回る向き), `turnLeft` (Left, 左へ), `turnRight` (Right, 右へ). A package with its own Reverse button (senshu-jerseys, dougu-sports' old `?dir=cw`) can drop it for these.
|
|
10
|
+
- **`autoplay-for` limits every turn** (KAZOKU-UI-22). A turn the Auto-turn button or `element.play()` starts now stops after `autoplay-for` seconds of turning, as the one `autoplay` starts always did; `play(seconds)` takes a limit of its own. Without `autoplay-for` nothing changes. A page that sets `autoplay-for` and wants its Auto-turn button unlimited keeps its own timer instead.
|
|
11
|
+
- **Flaky tests fixed at the cause** (KAZOKU-UI-11, 31, 25, 36, 32; tests and a template only, no change to the package's code). `address-state` wrote the page's own choice a quarter of a second after the click and the theme and language at once, so the order of the parameters was the order of the writes, which a slow machine changes: the test compares the parts, not the order (13 of 160 runs failed before, 0 of 160 after). The renderer's "turning draws once a frame" counted the stills' draws as the turn's and a fixed 800 ms of a slow software frame as too few: it now waits for the stills to finish and for the turn to be drawing, and for the last frame after the pause. The autoplay tests' "it turns while the view is on screen" waited a fixed 700 ms: they wait for the changes to arrive. The docs site's Tab test (new family template `templates/docs-site.demo.mjs`, to replace the copy in chikyu-earth and wakusei-planets) presses Alt+Tab where WebKit on a Mac does not Tab to links.
|
|
12
|
+
|
|
5
13
|
## [0.15.2] - 2026-11-04
|
|
6
14
|
|
|
7
15
|
The embed panel no longer scrolls the page.
|
package/README.md
CHANGED
|
@@ -172,7 +172,7 @@ Each its own subpath; later ones: picture-tile pickers, Help, GitHub and npm pil
|
|
|
172
172
|
|
|
173
173
|
- **Two levels, so the bar stays simple.** The default, `controls="basic"`, shows the angle slider, Auto-turn, zoom out and in with a **zoom readout** (`2.4×`; tap it to go back to Fit), Fit, and a small More button; while the view turns, one small **speed** button beside Auto-turn (it shows the word Speed over `1×`, and a press steps to the next speed). More (or `controls="advanced"`) adds the tilt slider, the speed buttons (0.5×, 1×, 2×), Front, and, once zoomed in, the four-arrow pan pad and the Move or Turn choice for what a drag does. The reader's choice with More is remembered in `localStorage` and beats the attribute.
|
|
174
174
|
- The zoom readout is the view's `zoom` (1× is Fit, whatever `fit-scale` is), one decimal, the one zoom number on the bar: it follows the buttons, the wheel, a pinch, a double press, Fit, the keys, the address, `set` and a changed limit. A screen reader is told "Zoom level 2.4×" once the zoom has settled for a moment, not for every step of a pinch.
|
|
175
|
-
- Auto-turn with play and pause; its speed is chosen with the family's [speed buttons](docs/guides/speed-buttons.md): 0.5× (half the `speed` attribute), 1× (the `speed` attribute, 30 degrees a second) or 2× (twice it), kept in the `pace` attribute and in the address as `?pace=0.5` or `?pace=2`.
|
|
175
|
+
- Auto-turn with play and pause; its speed is chosen with the family's [speed buttons](docs/guides/speed-buttons.md): 0.5× (half the `speed` attribute), 1× (the `speed` attribute, 30 degrees a second) or 2× (twice it), kept in the `pace` attribute and in the address as `?pace=0.5` or `?pace=2`. **Left and Right** (under More, beside the speed buttons) choose which way it goes, kept in the `direction` attribute and in the address as `?direction=left`; the angle goes on from where it was, at the same speed.
|
|
176
176
|
- **The wheel zooms.** The wheel over the view zooms it about the pointer, like a map; a trackpad's pinch (and Ctrl or Command with the wheel) zooms too. Shift and the wheel, or a sideways swipe, turns it. Tipping is a drag, the arrow keys or the tilt slider. With the pointer anywhere else the wheel scrolls the page, and a wheel that starts while the zoom is already at its end that way (turned down at Fit) scrolls the page too, so the page is never trapped.
|
|
177
177
|
- Touch: one finger turns at 1x and moves the view when zoomed in; two fingers pinch.
|
|
178
178
|
- Keys on the view: arrows turn and tip it, `+` and `-` zoom, `0` or Home is Front and Fit.
|
|
@@ -362,8 +362,9 @@ An `init` event is sent once, a moment after the element connects, with the star
|
|
|
362
362
|
| `object-size` | `w,h`: the object's size at Fit in CSS pixels, when it is smaller than the view. A pan then keeps at least a quarter of the object in view, however far it is dragged; left out, the object is the picture, which keeps covering the view. Or set `element.objectSize = { width, height }`. |
|
|
363
363
|
| `grab-radius` | Exact grab, for a globe or any turnable piece: its on-screen radius in CSS pixels at Fit, or `fit` for half the smaller side of the view. A drag then turns it by the angle that keeps the touched point under the pointer (a drag of *d* pixels moves the ground at the middle by *d* pixels), and the radius grows with the zoom. Left out, a drag turns by a fixed angle across the view, as before. See [docs/drag-rule.md](docs/drag-rule.md). |
|
|
364
364
|
| `autoplay` | Turn by itself from the start, unless the reader prefers reduced motion. |
|
|
365
|
-
| `autoplay-for` | Seconds of turning after which
|
|
366
|
-
| `speed` | Degrees a second at a pace of 1 (30). |
|
|
365
|
+
| `autoplay-for` | Seconds of turning after which a turn stops by itself (as Auto-turn pressed): the one `autoplay` starts, and one the Auto-turn button or `play()` starts. Left out, a turn never stops. Time spent paused off screen or in a hidden tab is not counted. `play(seconds)` takes its own limit. |
|
|
366
|
+
| `speed` | Degrees a second at a pace of 1 (30). A negative number starts the turn to the left. |
|
|
367
|
+
| `direction` | Which way Auto-turn goes, `left` or `right` (by the drag rule `yaw` goes up as the hand goes right, so right is the positive `speed`). The reader's choice of the Left and Right buttons (under More) is written here and kept in the address as `direction`; the angle goes on from where it was, at the same speed. Left out, the sign of `speed` decides, and the address keeps a way only when it is not that start. |
|
|
367
368
|
| `pace` | The Auto-turn speed as a multiple of `speed`: 1 by default, 0.5 and 2 on the default buttons. The reader's choice is written here, and kept in the address as `pace`. |
|
|
368
369
|
| `paces` | The speeds the buttons stand for, `0.5 1 2` by default (any numbers: `paces="1 4 8"`). |
|
|
369
370
|
| `controls` | `basic` (the default) or `advanced`: which controls are shown (see above; the zoom readout in both, the speed in both while the view turns). Written in the page's markup it is where the reader starts; what the reader chose with More is remembered under `kazoku-view-controls` in `localStorage` (or the key `storage` names) and wins. Setting the attribute later changes the level at once. The host carries `data-level` with the level now. |
|
|
@@ -371,7 +372,7 @@ An `init` event is sent once, a moment after the element connects, with the star
|
|
|
371
372
|
| `storage` | The `localStorage` key the level is remembered under (`kazoku-view-controls`). |
|
|
372
373
|
| `flat` | The view cannot turn: no angle, tilt, Auto-turn or Front, and a drag moves it. |
|
|
373
374
|
| `tilt-max`, `zoom-min`, `zoom-max` | The limits (30, 1, 5). |
|
|
374
|
-
| `address` | Keep the choices in the page's address (`?yaw=40&tilt=10&zoom=2&drag=turn&pace=2`). |
|
|
375
|
+
| `address` | Keep the choices in the page's address (`?yaw=40&tilt=10&zoom=2&drag=turn&pace=2&direction=left`). |
|
|
375
376
|
| `address-keys` | Other keys: `yaw:a,zoom:z`. |
|
|
376
377
|
| `theme` | `light` or `dark`; otherwise the page's colour scheme. |
|
|
377
378
|
| `overlay` | Sit over the bottom of a positioned parent. With `layout="compact"` the row floats 8 px in from the bottom start corner instead of spanning the bottom. |
|
|
@@ -381,7 +382,7 @@ Colours are custom properties: `--kazoku-ink`, `--kazoku-surface`, `--kazoku-lin
|
|
|
381
382
|
|
|
382
383
|
### The address
|
|
383
384
|
|
|
384
|
-
Only what a reader chose is kept: angle, tilt, zoom, the drag choice
|
|
385
|
+
Only what a reader chose is kept: angle, tilt, zoom, the drag choice, the auto-turn speed (`pace=0.5` or `pace=2`; 1 leaves the address) and the way it turns (`direction=left` or `direction=right`; the page's own start leaves the address). A page's own `speed` parameter is never touched, and `?pace=slow` or `fast` of 0.12 and 0.13 still read as 0.5 and 2. The pan is in pixels of one screen and the turning is of a moment, so neither is; while the view turns by itself the angle is left out. A value at its start (Fit, or the `zoom` the page opens at) leaves the address, and other parameters are kept. The address is written at most four times a second (Safari stops a page that changes its address more than 100 times in 30 seconds). Without the element: `parseAddress(search, keys?)` and `formatAddress(search, choices, keys?, leaveYaw?, startZoom?)`.
|
|
385
386
|
|
|
386
387
|
### Why the tag is only defined when it is free
|
|
387
388
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Direction } from "./levels.ts";
|
|
1
2
|
import { type Limits } from "./math.ts";
|
|
2
3
|
export type DragMode = "move" | "turn";
|
|
3
4
|
export interface AddressKeys {
|
|
@@ -7,6 +8,8 @@ export interface AddressKeys {
|
|
|
7
8
|
drag: string;
|
|
8
9
|
/** 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. */
|
|
9
10
|
speed?: string;
|
|
11
|
+
/** Which way Auto-turn goes, `left` or `right`; `direction` by default. */
|
|
12
|
+
direction?: string;
|
|
10
13
|
}
|
|
11
14
|
export declare const DEFAULT_KEYS: AddressKeys;
|
|
12
15
|
export interface AddressChoices {
|
|
@@ -16,6 +19,8 @@ export interface AddressChoices {
|
|
|
16
19
|
drag: DragMode;
|
|
17
20
|
/** The auto-turn speed as a multiple of the element's `speed`: the number of the button that is on. */
|
|
18
21
|
pace?: number;
|
|
22
|
+
/** The way Auto-turn goes. In `formatAddress`: a way is written; `null` leaves it off the address (it is the page's own start); left out, the key is not touched. */
|
|
23
|
+
direction?: Direction | null;
|
|
19
24
|
}
|
|
20
25
|
/** 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
26
|
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 { PACE_FACTOR, parsePace } from "./levels.js";
|
|
3
|
+
import { PACE_FACTOR, parseDirection, 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: "pace" };
|
|
5
|
+
export const DEFAULT_KEYS = { yaw: "yaw", tilt: "tilt", zoom: "zoom", drag: "drag", speed: "pace", direction: "direction" };
|
|
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) {
|
|
@@ -33,6 +33,9 @@ export function parseAddress(search, keys = DEFAULT_KEYS, limits = DEFAULT_LIMIT
|
|
|
33
33
|
const value = named !== undefined ? PACE_FACTOR[named] : said === null || said.trim() === "" ? Number.NaN : Number(said);
|
|
34
34
|
if (Number.isFinite(value))
|
|
35
35
|
out.pace = value;
|
|
36
|
+
const direction = parseDirection(params.get(keys.direction ?? "direction"));
|
|
37
|
+
if (direction !== undefined)
|
|
38
|
+
out.direction = direction;
|
|
36
39
|
return out;
|
|
37
40
|
}
|
|
38
41
|
/**
|
|
@@ -56,6 +59,8 @@ export function formatAddress(search, choices, keys = DEFAULT_KEYS, leaveYaw = f
|
|
|
56
59
|
const paceKey = keys.speed ?? "pace";
|
|
57
60
|
if (choices.pace !== undefined)
|
|
58
61
|
set(paceKey, choices.pace === 1 ? undefined : String(rounded(choices.pace, 3)));
|
|
62
|
+
if (choices.direction !== undefined)
|
|
63
|
+
set(keys.direction ?? "direction", choices.direction === null ? undefined : choices.direction);
|
|
59
64
|
const text = params.toString();
|
|
60
65
|
return text === "" ? "" : `?${text}`;
|
|
61
66
|
}
|
|
@@ -12,7 +12,7 @@ export interface ViewChangeDetail extends ViewState {
|
|
|
12
12
|
/**
|
|
13
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
|
|
14
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;
|
|
15
|
-
* buttons, links and form controls by default), `autoplay` (turn by itself from the start, unless the reader prefers reduced motion), `autoplay-for` (stop
|
|
15
|
+
* buttons, links and form controls by default), `autoplay` (turn by itself from the start, unless the reader prefers reduced motion), `autoplay-for` (stop a turn by itself after this many seconds of turning: the one `autoplay` starts, and one the Play button or `play()` starts), `speed` (degrees a second at a pace of 1; a negative one starts the turn to the left), `direction` (`left` or `right`: which way Auto-turn goes; the reader's choice of the Left and Right buttons is written here and kept in the address as `direction`, and the angle goes on from where it was at the same speed), `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),
|
|
16
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),
|
|
17
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),
|
|
18
18
|
* `layout` (`classic`, the default, `horizontal`, `vertical` or `compact`: where the controls sit; the horizontal, vertical and compact layouts have the zoom slider),
|
|
@@ -32,7 +32,8 @@ export declare class ViewControlsElement extends HTMLElement {
|
|
|
32
32
|
set view(element: HTMLElement | undefined);
|
|
33
33
|
/** Your own labels over the language's, for example `{ front: "Home" }`. */
|
|
34
34
|
set words(words: Partial<ViewWords>);
|
|
35
|
-
|
|
35
|
+
/** Turn by itself. With `seconds`, stop after that many seconds of turning; else the `autoplay-for` attribute's, if there is one. */
|
|
36
|
+
play(seconds?: number): void;
|
|
36
37
|
pause(): void;
|
|
37
38
|
front(): void;
|
|
38
39
|
/** Put the object back in the middle at Fit and the default angle, with a brief move (at once for a reader who prefers reduced motion, or with `false`). */
|
|
@@ -6,7 +6,7 @@ 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, parseLayout, parseLevel, zoomText } from "./levels.js";
|
|
9
|
+
import { LEVEL_STORAGE_KEY, parseDirection, parseLayout, parseLevel, zoomText } from "./levels.js";
|
|
10
10
|
import { ZOOM_STEP, ZOOMED } from "./math.js";
|
|
11
11
|
import { turnOnlyInSight } from "./sight.js";
|
|
12
12
|
import "../speed-buttons/define.js";
|
|
@@ -38,7 +38,13 @@ const HTML = `
|
|
|
38
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>
|
|
39
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>
|
|
40
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>
|
|
41
|
-
<
|
|
41
|
+
<div class="pacing adv" data-g="pacing">
|
|
42
|
+
<kazoku-speed-buttons class="pace" data-g="pace"></kazoku-speed-buttons>
|
|
43
|
+
<div class="group dir" role="group" data-g="direction">
|
|
44
|
+
<button type="button" data-b="turn-left" aria-pressed="false" part="button">${ICON.left}<span data-w="turnLeft"></span></button>
|
|
45
|
+
<button type="button" data-b="turn-right" aria-pressed="true" part="button">${ICON.right}<span data-w="turnRight"></span></button>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
42
48
|
<button type="button" class="front adv" data-b="front" part="button"><span data-w="front"></span></button>
|
|
43
49
|
<button type="button" class="centre" data-b="centre" part="button" hidden>${ICON.centre}<span class="text" data-w="centre"></span></button>
|
|
44
50
|
<div class="group zoom" role="group" data-g="zoom">
|
|
@@ -64,7 +70,7 @@ const HTML = `
|
|
|
64
70
|
/**
|
|
65
71
|
* 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
|
|
66
72
|
* 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;
|
|
67
|
-
* buttons, links and form controls by default), `autoplay` (turn by itself from the start, unless the reader prefers reduced motion), `autoplay-for` (stop
|
|
73
|
+
* buttons, links and form controls by default), `autoplay` (turn by itself from the start, unless the reader prefers reduced motion), `autoplay-for` (stop a turn by itself after this many seconds of turning: the one `autoplay` starts, and one the Play button or `play()` starts), `speed` (degrees a second at a pace of 1; a negative one starts the turn to the left), `direction` (`left` or `right`: which way Auto-turn goes; the reader's choice of the Left and Right buttons is written here and kept in the address as `direction`, and the angle goes on from where it was at the same speed), `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),
|
|
68
74
|
* `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),
|
|
69
75
|
* `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),
|
|
70
76
|
* `layout` (`classic`, the default, `horizontal`, `vertical` or `compact`: where the controls sit; the horizontal, vertical and compact layouts have the zoom slider),
|
|
@@ -72,7 +78,7 @@ const HTML = `
|
|
|
72
78
|
* or dark), `overlay` (sit over the bottom of a positioned parent).
|
|
73
79
|
*/
|
|
74
80
|
export class ViewControlsElement extends HTMLElement {
|
|
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"];
|
|
81
|
+
static observedAttributes = ["for", "lang", "yaw", "tilt", "zoom", "fit-scale", "ignore", "anchor", "key-tilt", "stop-turn", "tilt-step", "grab-radius", "object-size", "speed", "direction", "pace", "paces", "controls", "layout", "flat", "tilt-max", "zoom-min", "zoom-max", "address-keys"];
|
|
76
82
|
#root;
|
|
77
83
|
#controller;
|
|
78
84
|
#view;
|
|
@@ -91,6 +97,10 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
91
97
|
/** The zoom the slider shows now; NaN until it has shown one, and again when the marks are redrawn for new limits. */
|
|
92
98
|
#zoomShown = Number.NaN;
|
|
93
99
|
#pace = 1;
|
|
100
|
+
/** Which way the turn goes now, and the way the page itself starts it (its `direction`, else the sign of its `speed`): the address keeps a way only when it is not the start. */
|
|
101
|
+
#direction = "right";
|
|
102
|
+
#start;
|
|
103
|
+
#writing = false;
|
|
94
104
|
#labels = wordsFor("en");
|
|
95
105
|
#said = "";
|
|
96
106
|
#sayTimer;
|
|
@@ -118,8 +128,9 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
118
128
|
this.#words = words;
|
|
119
129
|
this.#label();
|
|
120
130
|
}
|
|
121
|
-
|
|
122
|
-
|
|
131
|
+
/** Turn by itself. With `seconds`, stop after that many seconds of turning; else the `autoplay-for` attribute's, if there is one. */
|
|
132
|
+
play(seconds) {
|
|
133
|
+
this.#ensure().play(seconds ?? this.#playLimit());
|
|
123
134
|
}
|
|
124
135
|
pause() {
|
|
125
136
|
this.#ensure().pause();
|
|
@@ -167,11 +178,15 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
167
178
|
if (this.hasAttribute(key) && Number.isFinite(value))
|
|
168
179
|
start[key] = value;
|
|
169
180
|
}
|
|
170
|
-
const { pace, ...chosen } = this.hasAttribute("address") ? parseAddress(globalThis.location?.search ?? "", this.#keys, c.limits) : {};
|
|
181
|
+
const { pace, direction, ...chosen } = this.hasAttribute("address") ? parseAddress(globalThis.location?.search ?? "", this.#keys, c.limits) : {};
|
|
171
182
|
Object.assign(start, chosen);
|
|
172
183
|
c.turnable = !this.hasAttribute("flat");
|
|
173
184
|
this.#level = parseLevel(this.#remembered()) ?? parseLevel(this.getAttribute("controls")) ?? "basic";
|
|
174
185
|
this.#pace = this.#nearestPace(pace ?? parseSpeed(this.getAttribute("pace"), this.#paces()) ?? 1);
|
|
186
|
+
// Read once: put back on a page after a move, the element is still started by the page and not by the way the reader has since chosen (which is in the attribute now).
|
|
187
|
+
const first = this.#start === undefined;
|
|
188
|
+
this.#start ??= this.#startDirection();
|
|
189
|
+
this.#direction = direction ?? (first ? this.#start : this.#direction);
|
|
175
190
|
this.#speed();
|
|
176
191
|
c.ignore = this.getAttribute("ignore") ?? DEFAULT_IGNORE;
|
|
177
192
|
for (const name of ["key-tilt", "stop-turn", "tilt-step", "grab-radius", "object-size"])
|
|
@@ -197,7 +212,7 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
197
212
|
return;
|
|
198
213
|
this.#dispatch(c.state, { final: true, reason: "init" });
|
|
199
214
|
if (this.hasAttribute("autoplay") && !globalThis.matchMedia?.("(prefers-reduced-motion: reduce)").matches)
|
|
200
|
-
c.play(
|
|
215
|
+
c.play(this.#playLimit());
|
|
201
216
|
});
|
|
202
217
|
}
|
|
203
218
|
disconnectedCallback() {
|
|
@@ -235,8 +250,22 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
235
250
|
c.ignore = value ?? DEFAULT_IGNORE;
|
|
236
251
|
else if (name === "anchor" || name === "key-tilt" || name === "stop-turn" || name === "tilt-step" || name === "grab-radius" || name === "object-size")
|
|
237
252
|
this.#option(name);
|
|
238
|
-
else if (name === "speed")
|
|
253
|
+
else if (name === "speed") {
|
|
254
|
+
// The page's own start follows its speed's sign, unless the reader has chosen the other way.
|
|
255
|
+
const start = this.#startDirection();
|
|
256
|
+
if (this.#direction === (this.#start ?? start))
|
|
257
|
+
this.#direction = start;
|
|
258
|
+
this.#start = start;
|
|
239
259
|
this.#speed();
|
|
260
|
+
this.#show();
|
|
261
|
+
}
|
|
262
|
+
else if (name === "direction") {
|
|
263
|
+
if (this.#writing)
|
|
264
|
+
return;
|
|
265
|
+
this.#start = this.#direction = this.#startDirection();
|
|
266
|
+
this.#speed();
|
|
267
|
+
this.#show();
|
|
268
|
+
}
|
|
240
269
|
else if (name === "pace" || name === "paces") {
|
|
241
270
|
this.#pace = this.#nearestPace(parseSpeed(this.getAttribute("pace"), this.#paces()) ?? this.#pace);
|
|
242
271
|
this.#speed();
|
|
@@ -335,7 +364,12 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
335
364
|
slider("tilt");
|
|
336
365
|
this.#zoomSlider(q("#zoom"), c);
|
|
337
366
|
const on = (b, run) => q(`[data-b="${b}"]`).addEventListener("click", run);
|
|
338
|
-
on("play", () =>
|
|
367
|
+
on("play", () => {
|
|
368
|
+
if (c().state.turning)
|
|
369
|
+
c().pause();
|
|
370
|
+
else
|
|
371
|
+
c().play(this.#playLimit());
|
|
372
|
+
});
|
|
339
373
|
on("front", () => c().front());
|
|
340
374
|
on("out", () => c().zoomBy(1 / ZOOM_STEP));
|
|
341
375
|
on("in", () => c().zoomBy(ZOOM_STEP));
|
|
@@ -357,6 +391,8 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
357
391
|
this.#choosePace(value);
|
|
358
392
|
});
|
|
359
393
|
on("pace-now", () => speeds.step(1));
|
|
394
|
+
on("turn-left", () => this.#chooseDirection("left"));
|
|
395
|
+
on("turn-right", () => this.#chooseDirection("right"));
|
|
360
396
|
on("left", () => c().panBy(-1, 0));
|
|
361
397
|
on("right", () => c().panBy(1, 0));
|
|
362
398
|
on("up", () => c().panBy(0, -1));
|
|
@@ -462,9 +498,31 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
462
498
|
if (this.hasAttribute("address"))
|
|
463
499
|
this.#save();
|
|
464
500
|
}
|
|
465
|
-
/** The
|
|
501
|
+
/** The way the page itself starts the turn: its `direction`, else left for a negative `speed` and right for the rest. */
|
|
502
|
+
#startDirection() {
|
|
503
|
+
return parseDirection(this.getAttribute("direction")) ?? (Number(this.getAttribute("speed")) < 0 ? "left" : "right");
|
|
504
|
+
}
|
|
505
|
+
/** The seconds `autoplay-for` limits a turn to, or `undefined` for none. */
|
|
506
|
+
#playLimit() {
|
|
507
|
+
const seconds = Number(this.getAttribute("autoplay-for"));
|
|
508
|
+
return seconds > 0 && Number.isFinite(seconds) ? seconds : undefined;
|
|
509
|
+
}
|
|
510
|
+
/** A way was pressed. It is written to the `direction` attribute and the address; the angle goes on from where it was, at the same speed. */
|
|
511
|
+
#chooseDirection(direction) {
|
|
512
|
+
this.#direction = direction;
|
|
513
|
+
this.#speed();
|
|
514
|
+
if (this.getAttribute("direction") !== direction) {
|
|
515
|
+
this.#writing = true;
|
|
516
|
+
this.setAttribute("direction", direction);
|
|
517
|
+
this.#writing = false;
|
|
518
|
+
}
|
|
519
|
+
this.#show();
|
|
520
|
+
if (this.hasAttribute("address"))
|
|
521
|
+
this.#save();
|
|
522
|
+
}
|
|
523
|
+
/** The turn's degrees a second: the size of the `speed` attribute (30) times the pace, going left when it is negative. */
|
|
466
524
|
#speed() {
|
|
467
|
-
this.#ensure().speed = (Number(this.getAttribute("speed")) || 30) * this.#pace;
|
|
525
|
+
this.#ensure().speed = Math.abs(Number(this.getAttribute("speed")) || 30) * this.#pace * (this.#direction === "left" ? -1 : 1);
|
|
468
526
|
}
|
|
469
527
|
#limits() {
|
|
470
528
|
const c = this.#ensure();
|
|
@@ -522,6 +580,7 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
522
580
|
set('[data-b="in"]', words.zoomIn);
|
|
523
581
|
set('[data-g="zoom"]', words.zoom);
|
|
524
582
|
set('[data-g="pan"]', words.pan);
|
|
583
|
+
set('[data-g="direction"]', words.direction);
|
|
525
584
|
set('[data-g="drag"]', words.drag);
|
|
526
585
|
const speeds = this.#root.querySelector("kazoku-speed-buttons");
|
|
527
586
|
speeds?.setAttribute("lang", language);
|
|
@@ -586,6 +645,8 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
586
645
|
const pace = speedText(this.#pace);
|
|
587
646
|
write(q('[data-o="pace"]'), pace);
|
|
588
647
|
say(q('[data-b="pace-now"]'), `${words.speed}: ${pace}`);
|
|
648
|
+
q('[data-b="turn-left"]').setAttribute("aria-pressed", String(this.#direction === "left"));
|
|
649
|
+
q('[data-b="turn-right"]').setAttribute("aria-pressed", String(this.#direction === "right"));
|
|
589
650
|
const more = q('[data-b="more"]');
|
|
590
651
|
more.setAttribute("aria-expanded", String(advanced));
|
|
591
652
|
this.dataset.level = this.#level;
|
|
@@ -598,7 +659,7 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
598
659
|
const flat = !c.turnable;
|
|
599
660
|
for (const selector of [".angle", ".play"])
|
|
600
661
|
q(selector).hidden = flat;
|
|
601
|
-
for (const selector of [".tilt", ".front", '[data-g="drag"]', '[data-g="
|
|
662
|
+
for (const selector of [".tilt", ".front", '[data-g="drag"]', '[data-g="pacing"]'])
|
|
602
663
|
q(selector).hidden = flat || !advanced;
|
|
603
664
|
// The speed is always in sight while the view turns: one small button in basic, which steps through the three; advanced always has the three.
|
|
604
665
|
q(".pace-now").hidden = flat || advanced || !s.turning;
|
|
@@ -636,7 +697,7 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
636
697
|
#save() {
|
|
637
698
|
this.#writer.write((search) => {
|
|
638
699
|
const s = this.#ensure().state;
|
|
639
|
-
return formatAddress(search, { ...s, pace: this.#pace }, this.#keys, s.turning, this.#ensure().startZoom);
|
|
700
|
+
return formatAddress(search, { ...s, pace: this.#pace, direction: this.#direction === (this.#start ?? this.#direction) ? null : this.#direction }, this.#keys, s.turning, this.#ensure().startZoom);
|
|
640
701
|
});
|
|
641
702
|
}
|
|
642
703
|
}
|
|
@@ -8,6 +8,9 @@ export type Pace = "slow" | "medium" | "fast";
|
|
|
8
8
|
export declare const PACES: readonly Pace[];
|
|
9
9
|
export declare const PACE_FACTOR: Record<Pace, number>;
|
|
10
10
|
export declare const parsePace: (text: string | null | undefined) => Pace | undefined;
|
|
11
|
+
/** Which way Auto-turn goes. By the drag rule `yaw` goes up as the hand goes right, so `right` is a positive speed and `left` a negative one; the page's drawing decides what that looks like. */
|
|
12
|
+
export type Direction = "left" | "right";
|
|
13
|
+
export declare const parseDirection: (text: string | null | undefined) => Direction | undefined;
|
|
11
14
|
/** The zoom as the readout shows it: `1×`, `2.4×`; below 1, two figures, so a small zoom never reads `0×`. */
|
|
12
15
|
export declare const zoomText: (zoom: number) => string;
|
|
13
16
|
/**
|
|
@@ -5,6 +5,7 @@ export const LEVEL_STORAGE_KEY = "kazoku-view-controls";
|
|
|
5
5
|
export const PACES = ["slow", "medium", "fast"];
|
|
6
6
|
export const PACE_FACTOR = { slow: 0.5, medium: 1, fast: 2 };
|
|
7
7
|
export const parsePace = (text) => PACES.find((pace) => pace === text);
|
|
8
|
+
export const parseDirection = (text) => (text === "left" || text === "right" ? text : undefined);
|
|
8
9
|
/** The zoom as the readout shows it: `1×`, `2.4×`; below 1, two figures, so a small zoom never reads `0×`. */
|
|
9
10
|
export const zoomText = (zoom) => `${Number(zoom < 1 ? zoom.toPrecision(2) : zoom.toFixed(1))}×`;
|
|
10
11
|
export const LAYOUTS = ["classic", "horizontal", "vertical", "compact"];
|
|
@@ -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/* 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";
|
|
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/* The speed buttons and the Left and Right buttons are one row (two when the room is short) and take the place the speed buttons had. */\n.pacing { display: inline-flex; align-items: center; flex-wrap: wrap; gap: 4px 12px; }\n.dir button { padding: 0 12px; }\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\"]) .pacing { grid-area: pace; justify-self: end; }\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\"]) .pacing, :host([data-layout=\"vertical\"]) .pacing, :host([data-layout=\"compact\"]) .pacing { grid-area: pace; }\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\"]) .pacing { 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";
|
|
@@ -63,6 +63,9 @@ button[aria-pressed="true"] .i-play, button[aria-pressed="false"] .i-pause { dis
|
|
|
63
63
|
.bar { --_bar-ink: var(--_ink); --_bar-surface: var(--_surface); --_bar-line: var(--_line); --_bar-accent: var(--_accent); }
|
|
64
64
|
.pace { --kazoku-ink: var(--_bar-ink); --kazoku-surface: var(--_bar-surface); --kazoku-line: var(--_bar-line); --kazoku-accent: var(--_bar-accent); }
|
|
65
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. */
|
|
66
|
+
/* The speed buttons and the Left and Right buttons are one row (two when the room is short) and take the place the speed buttons had. */
|
|
67
|
+
.pacing { display: inline-flex; align-items: center; flex-wrap: wrap; gap: 4px 12px; }
|
|
68
|
+
.dir button { padding: 0 12px; }
|
|
66
69
|
.pace-now { flex-direction: column; gap: 0; padding: 0 10px; line-height: 1.1; }
|
|
67
70
|
.pace-now .lbl { font-size: 0.7em; font-weight: 400; }
|
|
68
71
|
:host([data-turning][data-level="basic"]) .field.angle { flex-basis: 110px; }
|
|
@@ -79,7 +82,7 @@ button[aria-pressed="true"] .i-play, button[aria-pressed="false"] .i-pause { dis
|
|
|
79
82
|
${CLASSIC} .play { grid-area: play; }
|
|
80
83
|
${CLASSIC} .more { grid-area: more; }
|
|
81
84
|
${CLASSIC} .front { grid-area: front; padding: 0 12px; }
|
|
82
|
-
${CLASSIC} .
|
|
85
|
+
${CLASSIC} .pacing { grid-area: pace; justify-self: end; }
|
|
83
86
|
${CLASSIC} .zoom { grid-area: zoom; justify-self: end; flex-wrap: nowrap; }
|
|
84
87
|
${CLASSIC} .zoom button { padding: 0 8px; }
|
|
85
88
|
${CLASSIC} .zoom button.level { padding: 0 6px; }
|
|
@@ -121,7 +124,7 @@ ${H} .angle, ${V} .angle, ${C} .angle { grid-area: angle; }
|
|
|
121
124
|
${H} .tilt, ${V} .tilt, ${C} .tilt { grid-area: tilt; }
|
|
122
125
|
${H} .pace-now, ${V} .pace-now { grid-area: speed; }
|
|
123
126
|
${H} .play, ${V} .play, ${C} .play { grid-area: play; }
|
|
124
|
-
${H} .
|
|
127
|
+
${H} .pacing, ${V} .pacing, ${C} .pacing { grid-area: pace; }
|
|
125
128
|
${H} .front, ${V} .front, ${C} .front { grid-area: front; justify-self: start; }
|
|
126
129
|
${H} .zoom, ${V} .zoom { grid-area: zoom; }
|
|
127
130
|
${H} .pan, ${V} .pan, ${C} .pan { grid-area: pan; justify-self: start; }
|
|
@@ -197,7 +200,7 @@ ${C} .play:hover:not(:disabled), ${C} .more:hover:not(:disabled), ${C} .zoom but
|
|
|
197
200
|
${C} .play .text, ${C} .more .text { ${UNSEEN} }
|
|
198
201
|
${C} .play, ${C} .more { padding: 0; }
|
|
199
202
|
${C} .pace-now { display: none; }
|
|
200
|
-
${C} .
|
|
203
|
+
${C} .pacing { flex-wrap: wrap; }
|
|
201
204
|
${V} .centre, ${C} .centre { justify-self: start; }
|
|
202
205
|
/* The row stays one row: Centre is with Fit, one press of More away. */
|
|
203
206
|
:host([data-layout="compact"][data-level="basic"]) .centre { display: none; }
|
|
@@ -23,6 +23,10 @@ export interface ViewWords {
|
|
|
23
23
|
zoomReset: string;
|
|
24
24
|
more: string;
|
|
25
25
|
speed: string;
|
|
26
|
+
/** The name of the pair of buttons that choose which way Auto-turn goes. */
|
|
27
|
+
direction: string;
|
|
28
|
+
turnLeft: string;
|
|
29
|
+
turnRight: string;
|
|
26
30
|
slow: string;
|
|
27
31
|
medium: string;
|
|
28
32
|
fast: string;
|
|
@@ -24,6 +24,9 @@ export const WORDS = {
|
|
|
24
24
|
zoomReset: "back to Fit",
|
|
25
25
|
more: "More",
|
|
26
26
|
speed: "Speed",
|
|
27
|
+
direction: "Turn direction",
|
|
28
|
+
turnLeft: "Left",
|
|
29
|
+
turnRight: "Right",
|
|
27
30
|
slow: "Slow",
|
|
28
31
|
medium: "Medium",
|
|
29
32
|
fast: "Fast",
|
|
@@ -52,6 +55,9 @@ export const WORDS = {
|
|
|
52
55
|
zoomReset: "全体に戻す",
|
|
53
56
|
more: "詳細",
|
|
54
57
|
speed: "速さ",
|
|
58
|
+
direction: "回る向き",
|
|
59
|
+
turnLeft: "左へ",
|
|
60
|
+
turnRight: "右へ",
|
|
55
61
|
slow: "ゆっくり",
|
|
56
62
|
medium: "ふつう",
|
|
57
63
|
fast: "はやい",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@johnmorrisdotca/kazoku-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
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",
|