@realitycollective/webxr-uiextensions 0.1.0-preview.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/LICENSE +21 -0
  3. package/README.md +61 -0
  4. package/dist/adapter.d.ts +105 -0
  5. package/dist/adapter.js +2 -0
  6. package/dist/adapter.js.map +1 -0
  7. package/dist/chrome/markup.d.ts +40 -0
  8. package/dist/chrome/markup.js +56 -0
  9. package/dist/chrome/markup.js.map +1 -0
  10. package/dist/controls/element.d.ts +22 -0
  11. package/dist/controls/element.js +40 -0
  12. package/dist/controls/element.js.map +1 -0
  13. package/dist/controls/expandable-label.d.ts +32 -0
  14. package/dist/controls/expandable-label.js +63 -0
  15. package/dist/controls/expandable-label.js.map +1 -0
  16. package/dist/controls/log-view.d.ts +38 -0
  17. package/dist/controls/log-view.js +83 -0
  18. package/dist/controls/log-view.js.map +1 -0
  19. package/dist/controls/stepper.d.ts +32 -0
  20. package/dist/controls/stepper.js +78 -0
  21. package/dist/controls/stepper.js.map +1 -0
  22. package/dist/controls/toggle.d.ts +33 -0
  23. package/dist/controls/toggle.js +63 -0
  24. package/dist/controls/toggle.js.map +1 -0
  25. package/dist/controls/upgrade.d.ts +31 -0
  26. package/dist/controls/upgrade.js +87 -0
  27. package/dist/controls/upgrade.js.map +1 -0
  28. package/dist/core/dock-state.d.ts +58 -0
  29. package/dist/core/dock-state.js +64 -0
  30. package/dist/core/dock-state.js.map +1 -0
  31. package/dist/core/drag-math.d.ts +34 -0
  32. package/dist/core/drag-math.js +44 -0
  33. package/dist/core/drag-math.js.map +1 -0
  34. package/dist/core/events.d.ts +17 -0
  35. package/dist/core/events.js +40 -0
  36. package/dist/core/events.js.map +1 -0
  37. package/dist/core/expandable-model.d.ts +35 -0
  38. package/dist/core/expandable-model.js +57 -0
  39. package/dist/core/expandable-model.js.map +1 -0
  40. package/dist/core/hold-to-drag.d.ts +24 -0
  41. package/dist/core/hold-to-drag.js +34 -0
  42. package/dist/core/hold-to-drag.js.map +1 -0
  43. package/dist/core/log-model.d.ts +42 -0
  44. package/dist/core/log-model.js +69 -0
  45. package/dist/core/log-model.js.map +1 -0
  46. package/dist/core/region-layout.d.ts +41 -0
  47. package/dist/core/region-layout.js +79 -0
  48. package/dist/core/region-layout.js.map +1 -0
  49. package/dist/core/region-registry.d.ts +35 -0
  50. package/dist/core/region-registry.js +97 -0
  51. package/dist/core/region-registry.js.map +1 -0
  52. package/dist/core/stepper-model.d.ts +24 -0
  53. package/dist/core/stepper-model.js +46 -0
  54. package/dist/core/stepper-model.js.map +1 -0
  55. package/dist/core/toggle-model.d.ts +11 -0
  56. package/dist/core/toggle-model.js +25 -0
  57. package/dist/core/toggle-model.js.map +1 -0
  58. package/dist/core/window-manager.d.ts +81 -0
  59. package/dist/core/window-manager.js +172 -0
  60. package/dist/core/window-manager.js.map +1 -0
  61. package/dist/index.d.ts +29 -0
  62. package/dist/index.js +34 -0
  63. package/dist/index.js.map +1 -0
  64. package/dist/scene.d.ts +79 -0
  65. package/dist/scene.js +43 -0
  66. package/dist/scene.js.map +1 -0
  67. package/package.json +51 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,17 @@
1
+ # Changelog
2
+
3
+ Change log for the Reality Collective WebXR UI Extensions packages. All four packages are versioned and released together; the version below is the one carried by the `v<version>` release tag.
4
+
5
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Preview builds are not listed separately. The entry for a version accumulates while its previews are published, and is dated when that version is released.
6
+
7
+ ## [0.1.0]
8
+
9
+ ### Added
10
+
11
+ - `@realitycollective/webxr-uiextensions` - engine-free core: window manager, dock state and regions, drag maths, hold-to-drag, control models (stepper/toggle/expandable/log), the `SceneDescriptor` scene format, window chrome conventions and the platform-adapter contract.
12
+ - `@realitycollective/iwsdk-uiextensions` - Meta IWSDK adapter binding the core onto IWSDK's ECS, UIKitML and interaction systems, with shipped examples.
13
+ - `@realitycollective/xrblocks-uiextensions` - EXPERIMENTAL Google XR Blocks / plain three.js adapter: panel document, window host, follow and scale maths, desktop controls and locomotion, pointer forwarding.
14
+ - `@realitycollective/uix-devtools` - dev-only tooling: edit-session launch gate, runtime UIKitML compilation, and the `uix-dev` CLI (Cloudflare quick tunnel, QR onboarding, environment doctor).
15
+ - Demo clients: the IWSDK showcase, the devtools playground, and the multiplatform lab that picks its pipeline from the hardware.
16
+
17
+ [0.1.0]: https://github.com/realitycollective/WebXR-UIExtensions/commits/main
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Reality Collective
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 ADDED
@@ -0,0 +1,61 @@
1
+ # @realitycollective/webxr-uiextensions
2
+
3
+ The core of the Reality Collective UI Extensions. It provides:
4
+
5
+ - **Windows** - movable, resizable panels with a title bar and pin, dock, minimise and close buttons.
6
+ - **Docking** - snapping a window into a named region of the scene, such as a row along a wall.
7
+ - **Layout regions** - the named areas that windows snap into.
8
+ - **Drag maths** - the pure calculations behind dragging, dropping and following the user.
9
+ - **Controls** - the state behind steppers, toggles, expandable sections and log views.
10
+ - **Markup upgrading** - code that turns a plain element carrying a `data-uix` attribute into a working control, so you write markup rather than components.
11
+ - **Adapter interfaces** - what an engine package must implement to host all of the above.
12
+
13
+ **This package has zero runtime dependencies and imports no 3D engine.** A test, `test/architecture.test.ts`, fails the moment `three`, `@iwsdk/*`, `@pmndrs/*` or `xrblocks` appears anywhere in `src/`. That is what lets the same interface run unchanged on every three.js WebXR runtime.
14
+
15
+ ## You probably want an adapter, not this package
16
+
17
+ | Your engine | Install | Status |
18
+ | --- | --- | --- |
19
+ | Meta IWSDK (Quest / Horizon OS) | [`@realitycollective/iwsdk-uiextensions`](../iwsdk-uiextensions/README.md) | reference adapter, full feature set |
20
+ | Google XR Blocks / plain three.js | [`@realitycollective/xrblocks-uiextensions`](../xrblocks-uiextensions/README.md) | **experimental**, partial feature set |
21
+
22
+ Both adapters re-export everything here, so an app installs one package only. Depend on the core directly when writing headless logic, tests, tooling - or a new adapter.
23
+
24
+ ## What lives here
25
+
26
+ ```
27
+ src/core/ window manager, dock state machine, region slot math,
28
+ drag math, hold-to-drag, control models (stepper/toggle/
29
+ expandable/log) - pure logic, 100% coverage gated
30
+ src/controls/ data-uix markup upgraders, driven through the structural
31
+ UixElement interface (works on ANY conforming element tree)
32
+ src/chrome/ window chrome conventions: contractual element ids
33
+ (uix-titlebar, uix-pin, ...) + reference UIKitML snippet
34
+ src/adapter.ts the platform-adapter contract: PanelHost, PanelHandle,
35
+ HeadPoseSource, PointerInputSource (plain tuples, no engine)
36
+ ```
37
+
38
+ ## Writing an adapter
39
+
40
+ An adapter supplies three capabilities and drives the core from its frame loop:
41
+
42
+ 1. **Panels** - implement `PanelHost.createPanel(configJson)`: turn compiled UIKitML JSON into a live panel whose element tree satisfies `UixElement` (uikit does out of the box; ids land in `userData`).
43
+ 2. **Input** - deliver press/move/release into the core's `HoldToDrag` + drag math, or wire chrome clicks straight to `WindowManager`.
44
+ 3. **Viewer pose** - implement `HeadPoseSource` for follow mode and body-locked regions.
45
+
46
+ The IWSDK adapter is the reference implementation; the XR Blocks adapter shows the same contract bound without an ECS.
47
+
48
+ ## Testing
49
+
50
+ ```bash
51
+ npm test # from the workspace root - vitest, 100% thresholds on src/core
52
+ ```
53
+
54
+ ## Live demos
55
+
56
+ - Showcase: **[webxr-uiextensions.pages.dev](https://webxr-uiextensions.pages.dev)**
57
+ - Multiplatform lab: **[webxr-uix-lab.pages.dev](https://webxr-uix-lab.pages.dev)**
58
+
59
+ ## License
60
+
61
+ MIT © Reality Collective
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Platform-adapter contract.
3
+ *
4
+ * The core package owns every UX decision - window lifecycle, dock state,
5
+ * region slot math, drag math, control models - and knows nothing about any
6
+ * engine. An engine adapter supplies the three capabilities the core cannot
7
+ * provide for itself, and drives the core from its own frame loop:
8
+ *
9
+ * - {@link PanelHost}: turn compiled UIKitML JSON into a live spatial panel
10
+ * - {@link PointerInputSource}: deliver ray/pointer press-move-release
11
+ * - {@link HeadPoseSource}: the viewer pose, for follow mode & body-lock
12
+ *
13
+ * Known adapters:
14
+ * - `@realitycollective/iwsdk-uiextensions` - Meta IWSDK (ECS systems bind
15
+ * these capabilities to `@iwsdk/core` components)
16
+ * - `@realitycollective/xrblocks-uiextensions` - Google XR Blocks / plain
17
+ * three.js (experimental)
18
+ *
19
+ * The interfaces use plain tuples/records only - no engine, no three.js.
20
+ */
21
+ import type { UixElement } from './controls/element.js';
22
+ /** Position as [x, y, z] in meters, world space unless stated otherwise. */
23
+ export type Vec3Tuple = [number, number, number];
24
+ /** Orientation quaternion as [x, y, z, w]. */
25
+ export type QuatTuple = [number, number, number, number];
26
+ /** A viewer (head) pose sample. */
27
+ export interface HeadPose {
28
+ position: Vec3Tuple;
29
+ quaternion: QuatTuple;
30
+ }
31
+ /** Supplies the viewer pose each frame - camera on desktop, HMD in XR. */
32
+ export interface HeadPoseSource {
33
+ getHeadPose(): HeadPose;
34
+ }
35
+ /**
36
+ * A live spatial panel created from compiled UIKitML JSON.
37
+ * The `root` is traversable with the core's `walk`/`findRole` helpers and
38
+ * the `data-uix` control upgraders - identical markup works on every
39
+ * adapter.
40
+ */
41
+ export interface PanelHandle {
42
+ /** Root element of the interpreted panel (UixElement-conformant). */
43
+ readonly root: UixElement;
44
+ /** Look up an element by its markup `id`. */
45
+ getElementById(id: string): UixElement | undefined;
46
+ /** Constrain the panel to fit within width × height meters. */
47
+ setTargetDimensions(width: number, height: number): void;
48
+ /** Release panel resources. */
49
+ dispose(): void;
50
+ }
51
+ /** Creates spatial panels - the engine-specific half of UIKitML rendering. */
52
+ export interface PanelHost {
53
+ /**
54
+ * Create a panel from compiled UIKitML JSON (the `{ element, classes }`
55
+ * shape produced by the build plugin or by
56
+ * `@realitycollective/uix-devtools`' `compilePanelSource`).
57
+ */
58
+ createPanel(configJson: unknown): PanelHandle;
59
+ }
60
+ /**
61
+ * A window whose panel has finished loading and is ready to be wired.
62
+ * Delivered by {@link WindowHost.onPanelReady}.
63
+ */
64
+ export interface PanelReadyEvent {
65
+ /** The window's id, as given to the scene descriptor / create call. */
66
+ id: string;
67
+ /** The live panel - traverse it, or look elements up by markup id. */
68
+ panel: PanelHandle;
69
+ }
70
+ /**
71
+ * The engine-agnostic surface an app needs to build a UI: spawn windows and
72
+ * regions from portable data, observe when panels become wireable, and reach
73
+ * the shared `WindowManager`.
74
+ *
75
+ * Panels load asynchronously on every adapter (IWSDK fetches the config;
76
+ * uikit lays out over following frames), so app code must never assume a
77
+ * panel exists immediately after creating its window. {@link onPanelReady}
78
+ * is the portable answer - it replaces engine-specific discovery (ECS
79
+ * queries on IWSDK, polling anywhere else) and fires for panels that became
80
+ * ready before the listener was registered, so wiring order never matters.
81
+ */
82
+ export interface WindowHost extends PanelHost {
83
+ /**
84
+ * Subscribe to panel readiness. Late subscribers are replayed the windows
85
+ * that are already live. Returns an unsubscribe function.
86
+ */
87
+ onPanelReady(listener: (event: PanelReadyEvent) => void): () => void;
88
+ }
89
+ /** One pointer/ray interaction stream, engine-normalised. */
90
+ export interface PointerSample {
91
+ /** Pointer world position (ray origin or touch point). */
92
+ origin: Vec3Tuple;
93
+ /** Normalised pointing direction. */
94
+ direction: Vec3Tuple;
95
+ }
96
+ /**
97
+ * Delivers press-move-release for one interaction source (a controller ray,
98
+ * a hand pinch, a mouse). The core's `hold-to-drag` and `drag-math` consume
99
+ * these; the adapter decides what constitutes press/release.
100
+ */
101
+ export interface PointerInputSource {
102
+ onPress(listener: (sample: PointerSample) => void): () => void;
103
+ onMove(listener: (sample: PointerSample) => void): () => void;
104
+ onRelease(listener: (sample: PointerSample) => void): () => void;
105
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=adapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.js","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * Platform-adapter contract.\n *\n * The core package owns every UX decision - window lifecycle, dock state,\n * region slot math, drag math, control models - and knows nothing about any\n * engine. An engine adapter supplies the three capabilities the core cannot\n * provide for itself, and drives the core from its own frame loop:\n *\n * - {@link PanelHost}: turn compiled UIKitML JSON into a live spatial panel\n * - {@link PointerInputSource}: deliver ray/pointer press-move-release\n * - {@link HeadPoseSource}: the viewer pose, for follow mode & body-lock\n *\n * Known adapters:\n * - `@realitycollective/iwsdk-uiextensions` - Meta IWSDK (ECS systems bind\n * these capabilities to `@iwsdk/core` components)\n * - `@realitycollective/xrblocks-uiextensions` - Google XR Blocks / plain\n * three.js (experimental)\n *\n * The interfaces use plain tuples/records only - no engine, no three.js.\n */\nimport type { UixElement } from './controls/element.js';\n\n/** Position as [x, y, z] in meters, world space unless stated otherwise. */\nexport type Vec3Tuple = [number, number, number];\n\n/** Orientation quaternion as [x, y, z, w]. */\nexport type QuatTuple = [number, number, number, number];\n\n/** A viewer (head) pose sample. */\nexport interface HeadPose {\n position: Vec3Tuple;\n quaternion: QuatTuple;\n}\n\n/** Supplies the viewer pose each frame - camera on desktop, HMD in XR. */\nexport interface HeadPoseSource {\n getHeadPose(): HeadPose;\n}\n\n/**\n * A live spatial panel created from compiled UIKitML JSON.\n * The `root` is traversable with the core's `walk`/`findRole` helpers and\n * the `data-uix` control upgraders - identical markup works on every\n * adapter.\n */\nexport interface PanelHandle {\n /** Root element of the interpreted panel (UixElement-conformant). */\n readonly root: UixElement;\n /** Look up an element by its markup `id`. */\n getElementById(id: string): UixElement | undefined;\n /** Constrain the panel to fit within width × height meters. */\n setTargetDimensions(width: number, height: number): void;\n /** Release panel resources. */\n dispose(): void;\n}\n\n/** Creates spatial panels - the engine-specific half of UIKitML rendering. */\nexport interface PanelHost {\n /**\n * Create a panel from compiled UIKitML JSON (the `{ element, classes }`\n * shape produced by the build plugin or by\n * `@realitycollective/uix-devtools`' `compilePanelSource`).\n */\n createPanel(configJson: unknown): PanelHandle;\n}\n\n/**\n * A window whose panel has finished loading and is ready to be wired.\n * Delivered by {@link WindowHost.onPanelReady}.\n */\nexport interface PanelReadyEvent {\n /** The window's id, as given to the scene descriptor / create call. */\n id: string;\n /** The live panel - traverse it, or look elements up by markup id. */\n panel: PanelHandle;\n}\n\n/**\n * The engine-agnostic surface an app needs to build a UI: spawn windows and\n * regions from portable data, observe when panels become wireable, and reach\n * the shared `WindowManager`.\n *\n * Panels load asynchronously on every adapter (IWSDK fetches the config;\n * uikit lays out over following frames), so app code must never assume a\n * panel exists immediately after creating its window. {@link onPanelReady}\n * is the portable answer - it replaces engine-specific discovery (ECS\n * queries on IWSDK, polling anywhere else) and fires for panels that became\n * ready before the listener was registered, so wiring order never matters.\n */\nexport interface WindowHost extends PanelHost {\n /**\n * Subscribe to panel readiness. Late subscribers are replayed the windows\n * that are already live. Returns an unsubscribe function.\n */\n onPanelReady(listener: (event: PanelReadyEvent) => void): () => void;\n}\n\n/** One pointer/ray interaction stream, engine-normalised. */\nexport interface PointerSample {\n /** Pointer world position (ray origin or touch point). */\n origin: Vec3Tuple;\n /** Normalised pointing direction. */\n direction: Vec3Tuple;\n}\n\n/**\n * Delivers press-move-release for one interaction source (a controller ray,\n * a hand pinch, a mouse). The core's `hold-to-drag` and `drag-math` consume\n * these; the adapter decides what constitutes press/release.\n */\nexport interface PointerInputSource {\n onPress(listener: (sample: PointerSample) => void): () => void;\n onMove(listener: (sample: PointerSample) => void): () => void;\n onRelease(listener: (sample: PointerSample) => void): () => void;\n}\n"]}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Window chrome conventions.
3
+ *
4
+ * Windows are authored in UIKitML like any other IWSDK panel - the chrome is
5
+ * plain markup discovered by well-known element ids, not a parallel widget
6
+ * tree. Copy `WINDOW_CHROME_SNIPPET` into a `.uikitml` file and put the
7
+ * window body inside the `uix-content` element.
8
+ */
9
+ export declare const WINDOW_CHROME_IDS: {
10
+ /** Root container of the window. */
11
+ readonly window: "uix-window";
12
+ /** Drag surface; also focuses the window on press. */
13
+ readonly titlebar: "uix-titlebar";
14
+ /** Text element receiving `UIWindow.title`. */
15
+ readonly title: "uix-title";
16
+ /** Toggles body-follow ⇄ world-locked; label auto-syncs to PIN/UNPIN. */
17
+ readonly pin: "uix-pin";
18
+ /** Returns the window to its home (spawn region or original placement). */
19
+ readonly dock: "uix-dock";
20
+ /** Minimize/restore toggle. */
21
+ readonly minimize: "uix-minimize";
22
+ /** Closes (destroys) the window. */
23
+ readonly close: "uix-close";
24
+ /** Collapsed while minimized. */
25
+ readonly content: "uix-content";
26
+ };
27
+ /**
28
+ * Reference chrome markup. The styles are intentionally minimal - restyle
29
+ * freely, only the element ids are contractual.
30
+ *
31
+ * Notes:
32
+ * - The placeholder "." in `uix-title`: UIKitML only creates a Text node for
33
+ * elements with a literal string child, so dynamic text needs one.
34
+ * - The chrome buttons are `<div>`s on purpose: when an app registers a
35
+ * component kit (e.g. `@pmndrs/uikit-horizon`), lowercase `<button>` tags
36
+ * resolve to the kit's Button component, whose intrinsic sizing fights
37
+ * compact title-bar chrome. Plain containers stay fully styleable and
38
+ * still receive click events.
39
+ */
40
+ export declare const WINDOW_CHROME_SNIPPET = "\n<div id=\"uix-window\" class=\"uix-window\">\n <div id=\"uix-titlebar\" class=\"uix-titlebar\">\n <span id=\"uix-title\" class=\"uix-title\">.</span>\n <div class=\"uix-titlebar-buttons\">\n <div id=\"uix-pin\" class=\"uix-titlebar-button\">PIN</div>\n <div id=\"uix-dock\" class=\"uix-titlebar-button\">DOCK</div>\n <div id=\"uix-minimize\" class=\"uix-titlebar-button\">MIN</div>\n <div id=\"uix-close\" class=\"uix-titlebar-button\">X</div>\n </div>\n </div>\n <div id=\"uix-content\" class=\"uix-content\">\n <!-- window body goes here -->\n </div>\n</div>\n";
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Window chrome conventions.
3
+ *
4
+ * Windows are authored in UIKitML like any other IWSDK panel - the chrome is
5
+ * plain markup discovered by well-known element ids, not a parallel widget
6
+ * tree. Copy `WINDOW_CHROME_SNIPPET` into a `.uikitml` file and put the
7
+ * window body inside the `uix-content` element.
8
+ */
9
+ export const WINDOW_CHROME_IDS = {
10
+ /** Root container of the window. */
11
+ window: 'uix-window',
12
+ /** Drag surface; also focuses the window on press. */
13
+ titlebar: 'uix-titlebar',
14
+ /** Text element receiving `UIWindow.title`. */
15
+ title: 'uix-title',
16
+ /** Toggles body-follow ⇄ world-locked; label auto-syncs to PIN/UNPIN. */
17
+ pin: 'uix-pin',
18
+ /** Returns the window to its home (spawn region or original placement). */
19
+ dock: 'uix-dock',
20
+ /** Minimize/restore toggle. */
21
+ minimize: 'uix-minimize',
22
+ /** Closes (destroys) the window. */
23
+ close: 'uix-close',
24
+ /** Collapsed while minimized. */
25
+ content: 'uix-content',
26
+ };
27
+ /**
28
+ * Reference chrome markup. The styles are intentionally minimal - restyle
29
+ * freely, only the element ids are contractual.
30
+ *
31
+ * Notes:
32
+ * - The placeholder "." in `uix-title`: UIKitML only creates a Text node for
33
+ * elements with a literal string child, so dynamic text needs one.
34
+ * - The chrome buttons are `<div>`s on purpose: when an app registers a
35
+ * component kit (e.g. `@pmndrs/uikit-horizon`), lowercase `<button>` tags
36
+ * resolve to the kit's Button component, whose intrinsic sizing fights
37
+ * compact title-bar chrome. Plain containers stay fully styleable and
38
+ * still receive click events.
39
+ */
40
+ export const WINDOW_CHROME_SNIPPET = `
41
+ <div id="uix-window" class="uix-window">
42
+ <div id="uix-titlebar" class="uix-titlebar">
43
+ <span id="uix-title" class="uix-title">.</span>
44
+ <div class="uix-titlebar-buttons">
45
+ <div id="uix-pin" class="uix-titlebar-button">PIN</div>
46
+ <div id="uix-dock" class="uix-titlebar-button">DOCK</div>
47
+ <div id="uix-minimize" class="uix-titlebar-button">MIN</div>
48
+ <div id="uix-close" class="uix-titlebar-button">X</div>
49
+ </div>
50
+ </div>
51
+ <div id="uix-content" class="uix-content">
52
+ <!-- window body goes here -->
53
+ </div>
54
+ </div>
55
+ `;
56
+ //# sourceMappingURL=markup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markup.js","sourceRoot":"","sources":["../../src/chrome/markup.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAC/B,oCAAoC;IACpC,MAAM,EAAE,YAAY;IACpB,sDAAsD;IACtD,QAAQ,EAAE,cAAc;IACxB,+CAA+C;IAC/C,KAAK,EAAE,WAAW;IAClB,yEAAyE;IACzE,GAAG,EAAE,SAAS;IACd,2EAA2E;IAC3E,IAAI,EAAE,UAAU;IAChB,+BAA+B;IAC/B,QAAQ,EAAE,cAAc;IACxB,oCAAoC;IACpC,KAAK,EAAE,WAAW;IAClB,iCAAiC;IACjC,OAAO,EAAE,aAAa;CACd,CAAC;AAEX;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG;;;;;;;;;;;;;;;CAepC,CAAC","sourcesContent":["/**\n * Window chrome conventions.\n *\n * Windows are authored in UIKitML like any other IWSDK panel - the chrome is\n * plain markup discovered by well-known element ids, not a parallel widget\n * tree. Copy `WINDOW_CHROME_SNIPPET` into a `.uikitml` file and put the\n * window body inside the `uix-content` element.\n */\nexport const WINDOW_CHROME_IDS = {\n /** Root container of the window. */\n window: 'uix-window',\n /** Drag surface; also focuses the window on press. */\n titlebar: 'uix-titlebar',\n /** Text element receiving `UIWindow.title`. */\n title: 'uix-title',\n /** Toggles body-follow ⇄ world-locked; label auto-syncs to PIN/UNPIN. */\n pin: 'uix-pin',\n /** Returns the window to its home (spawn region or original placement). */\n dock: 'uix-dock',\n /** Minimize/restore toggle. */\n minimize: 'uix-minimize',\n /** Closes (destroys) the window. */\n close: 'uix-close',\n /** Collapsed while minimized. */\n content: 'uix-content',\n} as const;\n\n/**\n * Reference chrome markup. The styles are intentionally minimal - restyle\n * freely, only the element ids are contractual.\n *\n * Notes:\n * - The placeholder \".\" in `uix-title`: UIKitML only creates a Text node for\n * elements with a literal string child, so dynamic text needs one.\n * - The chrome buttons are `<div>`s on purpose: when an app registers a\n * component kit (e.g. `@pmndrs/uikit-horizon`), lowercase `<button>` tags\n * resolve to the kit's Button component, whose intrinsic sizing fights\n * compact title-bar chrome. Plain containers stay fully styleable and\n * still receive click events.\n */\nexport const WINDOW_CHROME_SNIPPET = `\n<div id=\"uix-window\" class=\"uix-window\">\n <div id=\"uix-titlebar\" class=\"uix-titlebar\">\n <span id=\"uix-title\" class=\"uix-title\">.</span>\n <div class=\"uix-titlebar-buttons\">\n <div id=\"uix-pin\" class=\"uix-titlebar-button\">PIN</div>\n <div id=\"uix-dock\" class=\"uix-titlebar-button\">DOCK</div>\n <div id=\"uix-minimize\" class=\"uix-titlebar-button\">MIN</div>\n <div id=\"uix-close\" class=\"uix-titlebar-button\">X</div>\n </div>\n </div>\n <div id=\"uix-content\" class=\"uix-content\">\n <!-- window body goes here -->\n </div>\n</div>\n`;\n"]}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Structural view of a uikit element as the controls layer needs it.
3
+ *
4
+ * UIKitML preserves `data-*` attributes into `element.userData` (camelCased,
5
+ * `data-` stripped): `data-uix="stepper"` → `userData.uix`. Controls are
6
+ * discovered by traversing a panel's object tree for those markers - no
7
+ * custom uikit component classes, so any markup (or kit) works.
8
+ */
9
+ export interface UixElement {
10
+ userData: Record<string, unknown>;
11
+ children: UixElement[];
12
+ addEventListener(type: string, listener: (event?: unknown) => void): void;
13
+ setProperties(props: Record<string, unknown>): void;
14
+ }
15
+ /** Depth-first traversal over uikit element children. */
16
+ export declare function walk(element: UixElement, visit: (element: UixElement) => void): void;
17
+ /** All descendants (including self) with `data-uix-role="<role>"`. */
18
+ export declare function findRoles(root: UixElement, role: string): UixElement[];
19
+ export declare function findRole(root: UixElement, role: string): UixElement | undefined;
20
+ export declare function attrString(element: UixElement, key: string): string | undefined;
21
+ export declare function attrNumber(element: UixElement, key: string): number | undefined;
22
+ export declare function attrBoolean(element: UixElement, key: string): boolean | undefined;
@@ -0,0 +1,40 @@
1
+ /** Depth-first traversal over uikit element children. */
2
+ export function walk(element, visit) {
3
+ visit(element);
4
+ for (const child of element.children ?? []) {
5
+ walk(child, visit);
6
+ }
7
+ }
8
+ /** All descendants (including self) with `data-uix-role="<role>"`. */
9
+ export function findRoles(root, role) {
10
+ const found = [];
11
+ walk(root, (element) => {
12
+ if (element.userData?.['uixRole'] === role) {
13
+ found.push(element);
14
+ }
15
+ });
16
+ return found;
17
+ }
18
+ export function findRole(root, role) {
19
+ return findRoles(root, role)[0];
20
+ }
21
+ export function attrString(element, key) {
22
+ const value = element.userData?.[key];
23
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
24
+ }
25
+ export function attrNumber(element, key) {
26
+ const raw = attrString(element, key);
27
+ if (raw === undefined) {
28
+ return undefined;
29
+ }
30
+ const value = Number(raw);
31
+ return Number.isFinite(value) ? value : undefined;
32
+ }
33
+ export function attrBoolean(element, key) {
34
+ const raw = attrString(element, key);
35
+ if (raw === undefined) {
36
+ return undefined;
37
+ }
38
+ return raw === 'true' || raw === '1' || raw === 'on';
39
+ }
40
+ //# sourceMappingURL=element.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"element.js","sourceRoot":"","sources":["../../src/controls/element.ts"],"names":[],"mappings":"AAeA,yDAAyD;AACzD,MAAM,UAAU,IAAI,CAAC,OAAmB,EAAE,KAAoC;IAC5E,KAAK,CAAC,OAAO,CAAC,CAAC;IACf,KAAK,MAAM,KAAK,IAAI,OAAO,CAAC,QAAQ,IAAI,EAAE,EAAE,CAAC;QAC3C,IAAI,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IACrB,CAAC;AACH,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,SAAS,CAAC,IAAgB,EAAE,IAAY;IACtD,MAAM,KAAK,GAAiB,EAAE,CAAC;IAC/B,IAAI,CAAC,IAAI,EAAE,CAAC,OAAO,EAAE,EAAE;QACrB,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC,SAAS,CAAC,KAAK,IAAI,EAAE,CAAC;YAC3C,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACtB,CAAC;IACH,CAAC,CAAC,CAAC;IACH,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,IAAgB,EAAE,IAAY;IACrD,OAAO,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;AAClC,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,OAAmB,EAAE,GAAW;IACzD,MAAM,KAAK,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,GAAG,CAAC,CAAC;IACtC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;AAC3E,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,OAAmB,EAAE,GAAW;IACzD,MAAM,GAAG,GAAG,UAAU,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IACrC,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;QACtB,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;IAC1B,OAAO,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;AACpD,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,OAAmB,EAAE,GAAW;IAC1D,MAAM,GAAG,GAAG,UAAU,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IACrC,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;QACtB,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,OAAO,GAAG,KAAK,MAAM,IAAI,GAAG,KAAK,GAAG,IAAI,GAAG,KAAK,IAAI,CAAC;AACvD,CAAC","sourcesContent":["/**\n * Structural view of a uikit element as the controls layer needs it.\n *\n * UIKitML preserves `data-*` attributes into `element.userData` (camelCased,\n * `data-` stripped): `data-uix=\"stepper\"` → `userData.uix`. Controls are\n * discovered by traversing a panel's object tree for those markers - no\n * custom uikit component classes, so any markup (or kit) works.\n */\nexport interface UixElement {\n userData: Record<string, unknown>;\n children: UixElement[];\n addEventListener(type: string, listener: (event?: unknown) => void): void;\n setProperties(props: Record<string, unknown>): void;\n}\n\n/** Depth-first traversal over uikit element children. */\nexport function walk(element: UixElement, visit: (element: UixElement) => void): void {\n visit(element);\n for (const child of element.children ?? []) {\n walk(child, visit);\n }\n}\n\n/** All descendants (including self) with `data-uix-role=\"<role>\"`. */\nexport function findRoles(root: UixElement, role: string): UixElement[] {\n const found: UixElement[] = [];\n walk(root, (element) => {\n if (element.userData?.['uixRole'] === role) {\n found.push(element);\n }\n });\n return found;\n}\n\nexport function findRole(root: UixElement, role: string): UixElement | undefined {\n return findRoles(root, role)[0];\n}\n\nexport function attrString(element: UixElement, key: string): string | undefined {\n const value = element.userData?.[key];\n return typeof value === 'string' && value.length > 0 ? value : undefined;\n}\n\nexport function attrNumber(element: UixElement, key: string): number | undefined {\n const raw = attrString(element, key);\n if (raw === undefined) {\n return undefined;\n }\n const value = Number(raw);\n return Number.isFinite(value) ? value : undefined;\n}\n\nexport function attrBoolean(element: UixElement, key: string): boolean | undefined {\n const raw = attrString(element, key);\n if (raw === undefined) {\n return undefined;\n }\n return raw === 'true' || raw === '1' || raw === 'on';\n}\n"]}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Expandable label - `data-uix="expandable-label"`.
3
+ *
4
+ * A multi-line label that collapses to a character budget with an ellipsis
5
+ * and an inline more/less affordance.
6
+ *
7
+ * ```html
8
+ * <div data-uix="expandable-label" data-uix-id="lore"
9
+ * data-uix-lines="2" data-uix-chars-per-line="40"
10
+ * data-uix-text="Long text..." class="my-label">
11
+ * <span data-uix-role="text">.</span>
12
+ * <button data-uix-role="toggle">more</button>
13
+ * </div>
14
+ * ```
15
+ */
16
+ import { Emitter } from '../core/events.js';
17
+ import { ExpandableModel } from '../core/expandable-model.js';
18
+ import { UixElement } from './element.js';
19
+ export declare class ExpandableLabelHandle {
20
+ readonly model: ExpandableModel;
21
+ private textElement;
22
+ private toggleElement;
23
+ readonly events: Emitter<{
24
+ change: boolean;
25
+ }>;
26
+ constructor(model: ExpandableModel, textElement: UixElement | undefined, toggleElement: UixElement | undefined);
27
+ get expanded(): boolean;
28
+ setText(text: string): void;
29
+ toggle(): void;
30
+ private render;
31
+ }
32
+ export declare function upgradeExpandableLabel(root: UixElement): ExpandableLabelHandle;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Expandable label - `data-uix="expandable-label"`.
3
+ *
4
+ * A multi-line label that collapses to a character budget with an ellipsis
5
+ * and an inline more/less affordance.
6
+ *
7
+ * ```html
8
+ * <div data-uix="expandable-label" data-uix-id="lore"
9
+ * data-uix-lines="2" data-uix-chars-per-line="40"
10
+ * data-uix-text="Long text..." class="my-label">
11
+ * <span data-uix-role="text">.</span>
12
+ * <button data-uix-role="toggle">more</button>
13
+ * </div>
14
+ * ```
15
+ */
16
+ import { Emitter } from '../core/events.js';
17
+ import { ExpandableModel } from '../core/expandable-model.js';
18
+ import { attrNumber, attrString, findRole } from './element.js';
19
+ export class ExpandableLabelHandle {
20
+ model;
21
+ textElement;
22
+ toggleElement;
23
+ events = new Emitter();
24
+ constructor(model, textElement, toggleElement) {
25
+ this.model = model;
26
+ this.textElement = textElement;
27
+ this.toggleElement = toggleElement;
28
+ this.render();
29
+ }
30
+ get expanded() {
31
+ return this.model.expanded;
32
+ }
33
+ setText(text) {
34
+ this.model.setText(text);
35
+ this.render();
36
+ }
37
+ toggle() {
38
+ this.events.emit('change', this.model.toggle());
39
+ this.render();
40
+ }
41
+ render() {
42
+ this.textElement?.setProperties({ text: this.model.displayText });
43
+ // Hide the affordance entirely when the text fits the collapsed budget.
44
+ this.toggleElement?.setProperties({
45
+ text: this.model.toggleLabel,
46
+ display: this.model.overflows ? 'flex' : 'none',
47
+ });
48
+ }
49
+ }
50
+ export function upgradeExpandableLabel(root) {
51
+ const lines = attrNumber(root, 'uixLines');
52
+ const charsPerLine = attrNumber(root, 'uixCharsPerLine');
53
+ const model = new ExpandableModel({
54
+ text: attrString(root, 'uixText') ?? '',
55
+ ...(lines !== undefined && { collapsedLines: lines }),
56
+ ...(charsPerLine !== undefined && { charsPerLine }),
57
+ });
58
+ const toggle = findRole(root, 'toggle');
59
+ const handle = new ExpandableLabelHandle(model, findRole(root, 'text'), toggle);
60
+ toggle?.addEventListener('click', () => handle.toggle());
61
+ return handle;
62
+ }
63
+ //# sourceMappingURL=expandable-label.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"expandable-label.js","sourceRoot":"","sources":["../../src/controls/expandable-label.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAC5C,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAC9D,OAAO,EAAc,UAAU,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAE5E,MAAM,OAAO,qBAAqB;IAIrB;IACD;IACA;IALD,MAAM,GAAG,IAAI,OAAO,EAAuB,CAAC;IAErD,YACW,KAAsB,EACvB,WAAmC,EACnC,aAAqC;QAFpC,UAAK,GAAL,KAAK,CAAiB;QACvB,gBAAW,GAAX,WAAW,CAAwB;QACnC,kBAAa,GAAb,aAAa,CAAwB;QAE7C,IAAI,CAAC,MAAM,EAAE,CAAC;IAChB,CAAC;IAED,IAAI,QAAQ;QACV,OAAO,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC;IAC7B,CAAC;IAED,OAAO,CAAC,IAAY;QAClB,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACzB,IAAI,CAAC,MAAM,EAAE,CAAC;IAChB,CAAC;IAED,MAAM;QACJ,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;QAChD,IAAI,CAAC,MAAM,EAAE,CAAC;IAChB,CAAC;IAEO,MAAM;QACZ,IAAI,CAAC,WAAW,EAAE,aAAa,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC;QAClE,wEAAwE;QACxE,IAAI,CAAC,aAAa,EAAE,aAAa,CAAC;YAChC,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,WAAW;YAC5B,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM;SAChD,CAAC,CAAC;IACL,CAAC;CACF;AAED,MAAM,UAAU,sBAAsB,CAAC,IAAgB;IACrD,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;IAC3C,MAAM,YAAY,GAAG,UAAU,CAAC,IAAI,EAAE,iBAAiB,CAAC,CAAC;IACzD,MAAM,KAAK,GAAG,IAAI,eAAe,CAAC;QAChC,IAAI,EAAE,UAAU,CAAC,IAAI,EAAE,SAAS,CAAC,IAAI,EAAE;QACvC,GAAG,CAAC,KAAK,KAAK,SAAS,IAAI,EAAE,cAAc,EAAE,KAAK,EAAE,CAAC;QACrD,GAAG,CAAC,YAAY,KAAK,SAAS,IAAI,EAAE,YAAY,EAAE,CAAC;KACpD,CAAC,CAAC;IACH,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;IACxC,MAAM,MAAM,GAAG,IAAI,qBAAqB,CAAC,KAAK,EAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC;IAChF,MAAM,EAAE,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;IACzD,OAAO,MAAM,CAAC;AAChB,CAAC","sourcesContent":["/**\n * Expandable label - `data-uix=\"expandable-label\"`.\n *\n * A multi-line label that collapses to a character budget with an ellipsis\n * and an inline more/less affordance.\n *\n * ```html\n * <div data-uix=\"expandable-label\" data-uix-id=\"lore\"\n * data-uix-lines=\"2\" data-uix-chars-per-line=\"40\"\n * data-uix-text=\"Long text...\" class=\"my-label\">\n * <span data-uix-role=\"text\">.</span>\n * <button data-uix-role=\"toggle\">more</button>\n * </div>\n * ```\n */\nimport { Emitter } from '../core/events.js';\nimport { ExpandableModel } from '../core/expandable-model.js';\nimport { UixElement, attrNumber, attrString, findRole } from './element.js';\n\nexport class ExpandableLabelHandle {\n readonly events = new Emitter<{ change: boolean }>();\n\n constructor(\n readonly model: ExpandableModel,\n private textElement: UixElement | undefined,\n private toggleElement: UixElement | undefined,\n ) {\n this.render();\n }\n\n get expanded(): boolean {\n return this.model.expanded;\n }\n\n setText(text: string): void {\n this.model.setText(text);\n this.render();\n }\n\n toggle(): void {\n this.events.emit('change', this.model.toggle());\n this.render();\n }\n\n private render(): void {\n this.textElement?.setProperties({ text: this.model.displayText });\n // Hide the affordance entirely when the text fits the collapsed budget.\n this.toggleElement?.setProperties({\n text: this.model.toggleLabel,\n display: this.model.overflows ? 'flex' : 'none',\n });\n }\n}\n\nexport function upgradeExpandableLabel(root: UixElement): ExpandableLabelHandle {\n const lines = attrNumber(root, 'uixLines');\n const charsPerLine = attrNumber(root, 'uixCharsPerLine');\n const model = new ExpandableModel({\n text: attrString(root, 'uixText') ?? '',\n ...(lines !== undefined && { collapsedLines: lines }),\n ...(charsPerLine !== undefined && { charsPerLine }),\n });\n const toggle = findRole(root, 'toggle');\n const handle = new ExpandableLabelHandle(model, findRole(root, 'text'), toggle);\n toggle?.addEventListener('click', () => handle.toggle());\n return handle;\n}\n"]}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Log view - `data-uix="log-view"`.
3
+ *
4
+ * A console-style scrolling list rendered into a fixed pool of row elements
5
+ * (UIKitML's text rule: each row needs a literal placeholder child). Rows
6
+ * are the descendants with role `line`, in document order; their count is
7
+ * the viewport height.
8
+ *
9
+ * ```html
10
+ * <div data-uix="log-view" data-uix-id="combat" data-uix-capacity="200">
11
+ * <div class="rows">
12
+ * <span data-uix-role="line">.</span>
13
+ * <span data-uix-role="line">.</span>
14
+ * <span data-uix-role="line">.</span>
15
+ * </div>
16
+ * <div class="log-buttons">
17
+ * <button data-uix-role="up">▲</button>
18
+ * <button data-uix-role="down">▼</button>
19
+ * <button data-uix-role="clear">clear</button>
20
+ * </div>
21
+ * <span data-uix-role="status">.</span>
22
+ * </div>
23
+ * ```
24
+ */
25
+ import { LogModel } from '../core/log-model.js';
26
+ import { UixElement } from './element.js';
27
+ export declare class LogViewHandle {
28
+ readonly model: LogModel;
29
+ private rows;
30
+ private statusElement;
31
+ constructor(model: LogModel, rows: UixElement[], statusElement: UixElement | undefined);
32
+ push(message: string, level?: string): void;
33
+ clear(): void;
34
+ scrollUp(rows?: number): void;
35
+ scrollDown(rows?: number): void;
36
+ private render;
37
+ }
38
+ export declare function upgradeLogView(root: UixElement): LogViewHandle;
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Log view - `data-uix="log-view"`.
3
+ *
4
+ * A console-style scrolling list rendered into a fixed pool of row elements
5
+ * (UIKitML's text rule: each row needs a literal placeholder child). Rows
6
+ * are the descendants with role `line`, in document order; their count is
7
+ * the viewport height.
8
+ *
9
+ * ```html
10
+ * <div data-uix="log-view" data-uix-id="combat" data-uix-capacity="200">
11
+ * <div class="rows">
12
+ * <span data-uix-role="line">.</span>
13
+ * <span data-uix-role="line">.</span>
14
+ * <span data-uix-role="line">.</span>
15
+ * </div>
16
+ * <div class="log-buttons">
17
+ * <button data-uix-role="up">▲</button>
18
+ * <button data-uix-role="down">▼</button>
19
+ * <button data-uix-role="clear">clear</button>
20
+ * </div>
21
+ * <span data-uix-role="status">.</span>
22
+ * </div>
23
+ * ```
24
+ */
25
+ import { LogModel } from '../core/log-model.js';
26
+ import { attrNumber, findRole, findRoles } from './element.js';
27
+ export class LogViewHandle {
28
+ model;
29
+ rows;
30
+ statusElement;
31
+ constructor(model, rows, statusElement) {
32
+ this.model = model;
33
+ this.rows = rows;
34
+ this.statusElement = statusElement;
35
+ this.render();
36
+ }
37
+ push(message, level = 'info') {
38
+ this.model.push(message, level);
39
+ this.render();
40
+ }
41
+ clear() {
42
+ this.model.clear();
43
+ this.render();
44
+ }
45
+ scrollUp(rows = 1) {
46
+ this.model.scrollUp(rows);
47
+ this.render();
48
+ }
49
+ scrollDown(rows = 1) {
50
+ this.model.scrollDown(rows);
51
+ this.render();
52
+ }
53
+ render() {
54
+ const visible = this.model.visible();
55
+ this.rows.forEach((row, index) => {
56
+ const entry = visible[index];
57
+ row.setProperties({
58
+ // UIKitML renders "" as empty text - exactly what vacant rows need.
59
+ text: entry ? entry.message : '',
60
+ });
61
+ });
62
+ if (this.statusElement) {
63
+ const suffix = this.model.isFollowing ? '' : ' (scrolled)';
64
+ this.statusElement.setProperties({
65
+ text: `${this.model.count} entries${suffix}`,
66
+ });
67
+ }
68
+ }
69
+ }
70
+ export function upgradeLogView(root) {
71
+ const rows = findRoles(root, 'line');
72
+ const capacity = attrNumber(root, 'uixCapacity');
73
+ const model = new LogModel({
74
+ visibleRows: Math.max(1, rows.length),
75
+ ...(capacity !== undefined && { capacity }),
76
+ });
77
+ const handle = new LogViewHandle(model, rows, findRole(root, 'status'));
78
+ findRole(root, 'up')?.addEventListener('click', () => handle.scrollUp());
79
+ findRole(root, 'down')?.addEventListener('click', () => handle.scrollDown());
80
+ findRole(root, 'clear')?.addEventListener('click', () => handle.clear());
81
+ return handle;
82
+ }
83
+ //# sourceMappingURL=log-view.js.map