@johnmorrisdotca/kazoku-ui 0.0.0-stage → 0.1.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 ADDED
@@ -0,0 +1,13 @@
1
+ # Changelog
2
+
3
+ ## [0.1.0] - 2026-11-01
4
+
5
+ The first release, and its first part: view controls, at `@johnmorrisdotca/kazoku-ui/view-controls`.
6
+
7
+ - `<kazoku-view-controls>`, a framework-free custom element (`/view-controls/element`, registered by `/view-controls/define`, which leaves a tag already taken alone), and `createViewController`, the same hands without an element.
8
+ - Angle and tilt sliders, Auto-turn with play and pause, Front; zoom out, 2 times, in and Fit; zoom about the pointer and with Ctrl or Command and the wheel; the four-arrow pan pad and the Move or Turn choice for a drag.
9
+ - Touch: one finger turns at 1x and moves the view when zoomed in; two fingers pinch.
10
+ - Choices kept in the address, with keys you can rename (`parseAddress`, `formatAddress`).
11
+ - It never draws: it reports `{ yaw, tilt, zoom, panX, panY, turning, drag }` in a `kazoku-viewchange` event or an `onChange` callback.
12
+ - Labels in English and Japanese; light and dark; 48 px touch targets; a compact bar on a phone; no automatic turn for a reader who prefers reduced motion.
13
+ - No runtime dependencies.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 John Morris
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,157 @@
1
- # Temporary Holding Version
1
+ # Kazoku UI 家族
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ <picture>
4
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/johnmorrisdotca/kazoku-ui/main/docs/images/hero-dark.webp">
5
+ <img alt="The demo: a white card with a blue circle and a black triangle, turned a little and tipped, in a frame with a dotted floor. Under it the view controls: Angle and Tilt sliders, Auto-turn and Front buttons, zoom out, 2 times, zoom in and Fit, four arrows to move the zoomed view, and the Move and Turn choice for a drag." src="https://raw.githubusercontent.com/johnmorrisdotca/kazoku-ui/main/docs/images/hero-light.webp" width="720">
6
+ </picture>
7
+
8
+ The family's shared interface parts, as their own versioned package: one part per subpath, so a page loads only what it uses. No runtime dependencies, MIT.
9
+ The first part is the **view controls** for any 3D or zoomable view. They never draw the scene: they tell your page what the reader chose, and your canvas, WebGL,
10
+ SVG or CSS 3D applies it.
11
+
12
+ [Demo](https://johnmorrisdotca.github.io/kazoku-ui/) · [Labels, English and 日本語](docs/labels.md) · [Changelog](CHANGELOG.md)
13
+
14
+ ```sh
15
+ npm install @johnmorrisdotca/kazoku-ui
16
+ ```
17
+
18
+ Parts (each its own subpath; later ones: picture-tile pickers, theme swatches with light and dark, the English and 日本語 switch, Help, GitHub and npm pills, Save SVG and PNG, the family's backgrounds):
19
+
20
+ | Subpath | What it is |
21
+ |---|---|
22
+ | `@johnmorrisdotca/kazoku-ui/view-controls` | The controller, the arithmetic, the address and the labels. Imports on a server. |
23
+ | `@johnmorrisdotca/kazoku-ui/view-controls/element` | The `ViewControlsElement` class (needs a browser). |
24
+ | `@johnmorrisdotca/kazoku-ui/view-controls/define` | Registers `<kazoku-view-controls>` (once, and only if the tag is free). |
25
+
26
+ ## View controls
27
+
28
+ - Angle and tilt sliders, Auto-turn with play and pause, Front.
29
+ - Zoom out, 2 times, zoom in and Fit. Zoom is about the pointer (and Ctrl or Command with the wheel, or a trackpad's pinch).
30
+ - The four-arrow pan pad (shown once zoomed in) and the Move or Turn choice for what a drag does.
31
+ - Touch: one finger turns at 1x and moves the view when zoomed in; two fingers pinch.
32
+ - Keys on the view: arrows turn and tip it, `+` and `-` zoom, `0` or Home is Front and Fit.
33
+ - The choices are kept in the address, with keys you can rename.
34
+ - Labels in English and Japanese; light and dark; 48 px touch targets; a compact bar on a phone; no automatic turn for a reader who prefers reduced motion.
35
+ - **A control never stops or resets the turn.** Zoom, tilt, pan, the Move or Turn choice, Fit and Front all leave Auto-turn running (Front only sets the angle to 0 and the turn goes on from there). A hand on the angle slider, or turning the view by dragging it, holds the turn still while it is down and the turn carries on from where the hand left it. Only the Auto-turn button pauses it.
36
+
37
+ ### In plain HTML
38
+
39
+ ```html
40
+ <div id="scene" tabindex="0" style="height: 320px">...your scene...</div>
41
+ <kazoku-view-controls for="scene" address></kazoku-view-controls>
42
+
43
+ <script type="module">
44
+ import "@johnmorrisdotca/kazoku-ui/view-controls/define";
45
+
46
+ const scene = document.getElementById("scene");
47
+ document.querySelector("kazoku-view-controls").addEventListener("kazoku-viewchange", (event) => {
48
+ const { yaw, tilt, zoom, panX, panY } = event.detail;
49
+ scene.style.transform = `translate(${panX}px, ${panY}px) scale(${zoom}) rotateX(${tilt}deg) rotateY(${yaw}deg)`;
50
+ });
51
+ </script>
52
+ ```
53
+
54
+ Put the element on the view or right beside it, so it is on screen with the view. `for` names the element the hands bind to (the pointer, pinch, wheel and keys); give it a `tabindex` if you want the keys on it, and size it to the view, because the pan is held to the room the zoom gives. With `overlay` the bar sits over the bottom of its positioned parent instead.
55
+
56
+ ### In a framework
57
+
58
+ The element is plain DOM, so any framework uses it with a ref and one listener. React:
59
+
60
+ ```jsx
61
+ import "@johnmorrisdotca/kazoku-ui/view-controls/define";
62
+ import { useEffect, useRef } from "react";
63
+
64
+ export function Viewer({ draw }) {
65
+ const view = useRef(null);
66
+ const controls = useRef(null);
67
+ useEffect(() => {
68
+ const bar = controls.current;
69
+ bar.view = view.current;
70
+ const on = (event) => draw(event.detail);
71
+ bar.addEventListener("kazoku-viewchange", on);
72
+ return () => bar.removeEventListener("kazoku-viewchange", on);
73
+ }, [draw]);
74
+ return (
75
+ <>
76
+ <div ref={view} tabIndex={0} style={{ height: 320 }} />
77
+ <kazoku-view-controls ref={controls} lang="en" />
78
+ </>
79
+ );
80
+ }
81
+ ```
82
+
83
+ ### Without the element
84
+
85
+ `createViewController` is the same hands and state with no markup of its own, for a page that draws its own buttons. It touches the DOM only in `bind`.
86
+
87
+ ```js
88
+ import { createViewController } from "@johnmorrisdotca/kazoku-ui/view-controls";
89
+
90
+ const view = createViewController({
91
+ speed: 30, // degrees a second for Auto-turn
92
+ onChange(state, { final, reason }) {
93
+ draw(state); // { yaw, tilt, zoom, panX, panY, turning, drag }
94
+ },
95
+ });
96
+ const unbind = view.bind(document.getElementById("scene")); // pointer, pinch, wheel, keys
97
+ myPlayButton.onclick = () => view.toggle();
98
+ myZoomIn.onclick = () => view.zoomBy(1.4);
99
+ ```
100
+
101
+ ## The event API
102
+
103
+ `kazoku-viewchange` is dispatched on the element (it bubbles and crosses shadow roots). `event.detail`, and the first argument of `onChange`, is:
104
+
105
+ | Field | Meaning |
106
+ |---|---|
107
+ | `yaw` | Degrees, -180 up to 180: the turn about the vertical. Dragging right makes it larger. |
108
+ | `tilt` | Degrees, held to plus or minus `tilt-max` (30): the top tips away as it gets larger (dragging up). |
109
+ | `zoom` | 1 to 5 (`zoom-min`, `zoom-max`). |
110
+ | `panX`, `panY` | CSS pixels the zoomed picture is moved from the middle of the view; 0 at 1x. |
111
+ | `turning` | True while Auto-turn is running. |
112
+ | `drag` | `"move"` or `"turn"`: what a plain drag does while zoomed in. |
113
+ | `final` | False for the steps of a drag, a pinch, a slider and the automatic turn; true when something settled. Do the costly drawing on `final`, the cheap transform on every event. |
114
+ | `reason` | What changed it: `init`, `auto`, `drag`, `pinch`, `wheel`, `key`, `slider`, `zoom`, `pan`, `front`, `fit`, `play`, `pause` or `set`. |
115
+
116
+ An `init` event is sent once, a moment after the element connects, with the starting view (from the attributes and the address). `element.state` is the current view at any time.
117
+
118
+ ### Attributes
119
+
120
+ | Attribute | |
121
+ |---|---|
122
+ | `for` | The id of the view the hands bind to. Or set `element.view = someElement`. |
123
+ | `lang` | `en` or `ja`. Otherwise the nearest `lang` above it, else English. |
124
+ | `yaw`, `tilt`, `zoom` | The starting view. |
125
+ | `autoplay` | Turn by itself from the start, unless the reader prefers reduced motion. |
126
+ | `speed` | Degrees a second (30). |
127
+ | `flat` | The view cannot turn: no angle, tilt, Auto-turn or Front, and a drag moves it. |
128
+ | `tilt-max`, `zoom-min`, `zoom-max` | The limits (30, 1, 5). |
129
+ | `address` | Keep the choices in the page's address (`?yaw=40&tilt=10&zoom=2&drag=turn`). |
130
+ | `address-keys` | Other keys: `yaw:a,zoom:z`. |
131
+ | `theme` | `light` or `dark`; otherwise the page's colour scheme. |
132
+ | `overlay` | Sit over the bottom of a positioned parent. |
133
+
134
+ Methods: `play()`, `pause()`, `front()`, `fit()`, `set({ yaw, tilt, zoom, panX, panY, drag })`, and `controller` (the `createViewController` object behind it). `element.words = { front: "Home" }` replaces labels.
135
+ Colours are custom properties: `--kazoku-ink`, `--kazoku-surface`, `--kazoku-line`, `--kazoku-accent`; the parts are `bar`, `field`, `slider` and `button`.
136
+
137
+ ### The address
138
+
139
+ Only what a reader chose is kept: angle, tilt, zoom and the drag choice. 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 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?)`.
140
+
141
+ ### Why the tag is only defined when it is free
142
+
143
+ `customElements.define` throws when a name is taken, and a page can meet that without any fault: two copies of this package (an app and a widget that each bundle one, or two versions), the script loaded twice, or a page that registered the tag to supply its own element. A throw would stop the importing module and the page's script with it. So `define` calls `customElements.get` first and the first definition wins; `defineViewControls(registry?)` returns whether it defined the tag.
144
+
145
+ ## Size
146
+
147
+ Gzipped, unbundled: the element and its definition about 4.8 KB, the controller 3.5 KB, the maths 1.4 KB, the address 1.2 KB, the labels 0.7 KB and the styles 1.6 KB (`pnpm test:package` prints them). Nothing is loaded from the network.
148
+
149
+ ## Checks
150
+
151
+ `pnpm check` (lint, types, 25 unit tests of the maths, the address and the controller, the build and the packed package), and `pnpm test:demo` (the demo in Chromium and WebKit at a phone's width and a desk's: the controls in the first screen, a toggle during Auto-turn leaves the angle advancing, zoom about the pointer, pan when zoomed, touch, the address, the labels, reduced motion).
152
+
153
+ ## Not yet
154
+
155
+ Not published to npm yet. The Japanese labels have not yet been read by a native speaker. The demo's scene is a CSS 3D card; the package has no opinion about what draws.
156
+
157
+ Licence: MIT. No brand, league or sponsor mark is in this package.
@@ -0,0 +1,3 @@
1
+ /** The subpaths of the parts, in the order they were added. */
2
+ export declare const PARTS: readonly ["view-controls"];
3
+ export type Part = (typeof PARTS)[number];
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ // Kazoku UI 家族: the family's shared interface parts, each its own subpath so a page loads only what it uses.
2
+ /** The subpaths of the parts, in the order they were added. */
3
+ export const PARTS = ["view-controls"];
@@ -0,0 +1,24 @@
1
+ import { type Limits } from "./math.ts";
2
+ export type DragMode = "move" | "turn";
3
+ export interface AddressKeys {
4
+ yaw: string;
5
+ tilt: string;
6
+ zoom: string;
7
+ drag: string;
8
+ }
9
+ export declare const DEFAULT_KEYS: AddressKeys;
10
+ export interface AddressChoices {
11
+ yaw: number;
12
+ tilt: number;
13
+ zoom: number;
14
+ drag: DragMode;
15
+ }
16
+ /** 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. */
17
+ export declare function parseAddress(search: string, keys?: AddressKeys, limits?: Limits): Partial<AddressChoices>;
18
+ /**
19
+ * The query string (with its `?`, or empty) that keeps `choices` and every other parameter of `search`. A choice at its starting value is left out, so
20
+ * the plain address stays plain. `leaveYaw` keeps the angle out while the view turns by itself (it would change every frame).
21
+ */
22
+ export declare function formatAddress(search: string, choices: AddressChoices, keys?: AddressKeys, leaveYaw?: boolean): string;
23
+ /** Read `address-keys`: `yaw:a,tilt:b` renames the keys it names and leaves the others. */
24
+ export declare function parseKeys(text: string | null): AddressKeys;
@@ -0,0 +1,60 @@
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
+ // (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 { clamp, DEFAULT_LIMITS, wrapYaw } from "./math.js";
4
+ export const DEFAULT_KEYS = { yaw: "yaw", tilt: "tilt", zoom: "zoom", drag: "drag" };
5
+ const rounded = (value, places) => Number(value.toFixed(places));
6
+ /** 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. */
7
+ export function parseAddress(search, keys = DEFAULT_KEYS, limits = DEFAULT_LIMITS) {
8
+ const params = new URLSearchParams(search);
9
+ const out = {};
10
+ const number = (key) => {
11
+ const text = params.get(key);
12
+ if (text === null || text.trim() === "")
13
+ return undefined;
14
+ const value = Number(text);
15
+ return Number.isFinite(value) ? value : undefined;
16
+ };
17
+ const yaw = number(keys.yaw);
18
+ const tilt = number(keys.tilt);
19
+ const zoom = number(keys.zoom);
20
+ if (yaw !== undefined)
21
+ out.yaw = rounded(wrapYaw(yaw), 0);
22
+ if (tilt !== undefined)
23
+ out.tilt = rounded(clamp(tilt, -limits.tiltMax, limits.tiltMax), 0);
24
+ if (zoom !== undefined)
25
+ out.zoom = rounded(clamp(zoom, limits.zoomMin, limits.zoomMax), 2);
26
+ const drag = params.get(keys.drag);
27
+ if (drag === "turn" || drag === "move")
28
+ out.drag = drag;
29
+ return out;
30
+ }
31
+ /**
32
+ * The query string (with its `?`, or empty) that keeps `choices` and every other parameter of `search`. A choice at its starting value is left out, so
33
+ * the plain address stays plain. `leaveYaw` keeps the angle out while the view turns by itself (it would change every frame).
34
+ */
35
+ export function formatAddress(search, choices, keys = DEFAULT_KEYS, leaveYaw = false) {
36
+ const params = new URLSearchParams(search);
37
+ const set = (key, value) => {
38
+ if (value === undefined)
39
+ params.delete(key);
40
+ else
41
+ params.set(key, value);
42
+ };
43
+ if (!leaveYaw)
44
+ set(keys.yaw, Math.round(choices.yaw) === 0 ? undefined : String(rounded(wrapYaw(choices.yaw), 0)));
45
+ set(keys.tilt, Math.round(choices.tilt) === 0 ? undefined : String(rounded(choices.tilt, 0)));
46
+ set(keys.zoom, Math.abs(choices.zoom - 1) < 0.005 ? undefined : String(rounded(choices.zoom, 2)));
47
+ set(keys.drag, choices.drag === "turn" ? "turn" : undefined);
48
+ const text = params.toString();
49
+ return text === "" ? "" : `?${text}`;
50
+ }
51
+ /** Read `address-keys`: `yaw:a,tilt:b` renames the keys it names and leaves the others. */
52
+ export function parseKeys(text) {
53
+ const keys = { ...DEFAULT_KEYS };
54
+ for (const part of (text ?? "").split(",")) {
55
+ const [name, key] = part.split(":").map((s) => s.trim());
56
+ if (name !== undefined && key !== undefined && key !== "" && name in keys)
57
+ keys[name] = key;
58
+ }
59
+ return keys;
60
+ }
@@ -0,0 +1,56 @@
1
+ import type { DragMode } from "./address.ts";
2
+ import { type Camera, type Limits, type Point, type Size } from "./math.ts";
3
+ export interface ViewState extends Camera {
4
+ /** True while the view turns by itself. A control never changes this; only play, pause and a grab of the view's own do not either (see `holdTurn`). */
5
+ turning: boolean;
6
+ /** What a plain drag does while zoomed in: move the view, or turn the piece. */
7
+ drag: DragMode;
8
+ }
9
+ export interface ChangeInfo {
10
+ /** False for the steps of a drag, a pinch, a slider or the automatic turn; true when something settled (a button, a release, a key, a wheel step). */
11
+ final: boolean;
12
+ /** What changed it: "set", "auto", "drag", "pinch", "wheel", "key", "zoom", "pan", "front", "fit", "play", "pause", "tap". */
13
+ reason: string;
14
+ }
15
+ export interface Clock {
16
+ request(callback: (now: number) => void): number;
17
+ cancel(id: number): void;
18
+ }
19
+ export interface ViewControllerOptions {
20
+ initial?: Partial<ViewState>;
21
+ /** False for a flat view that cannot turn: a drag then always moves it and the angle and tilt never change. */
22
+ turnable?: boolean;
23
+ /** Degrees a second when it turns by itself. */
24
+ speed?: number;
25
+ limits?: Partial<Limits>;
26
+ /** The view's size in CSS pixels, for holding the pan; `bind` gives it from the element when this is left out. */
27
+ size?: () => Size | undefined;
28
+ onChange?: (state: ViewState, info: ChangeInfo) => void;
29
+ /** Animation frames; the browser's requestAnimationFrame by default. A test passes its own. */
30
+ clock?: Clock;
31
+ }
32
+ export interface ViewController {
33
+ readonly state: ViewState;
34
+ turnable: boolean;
35
+ speed: number;
36
+ readonly limits: Limits;
37
+ set(next: Partial<ViewState>, final?: boolean, reason?: string): void;
38
+ play(): void;
39
+ pause(): void;
40
+ toggle(): void;
41
+ /** Hold the automatic turn still while a hand (a slider being dragged) has the angle; `releaseTurn` lets it carry on from where the hand left it. Counts, so holds nest. */
42
+ holdTurn(): void;
43
+ releaseTurn(): void;
44
+ front(): void;
45
+ fit(): void;
46
+ /** Zoom to `next` about the point `at` (client pixels; the view's middle when left out). */
47
+ zoomAt(next: number, at?: Point, final?: boolean): void;
48
+ zoomBy(factor: number, at?: Point): void;
49
+ /** Move the view by whole fractions of its size: `panBy(1, 0)` is a quarter of its width to the right. */
50
+ panBy(stepsX: number, stepsY: number): void;
51
+ setDrag(drag: DragMode): void;
52
+ /** Bind the pointer, pinch, wheel and key hands to `view`. Returns a function that unbinds them. */
53
+ bind(view: HTMLElement): () => void;
54
+ destroy(): void;
55
+ }
56
+ export declare function createViewController(options?: ViewControllerOptions): ViewController;