@realitycollective/webxr-uiextensions 0.1.0-preview.0 → 0.1.0-preview.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -13,5 +13,26 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
13
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
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
15
  - Demo clients: the IWSDK showcase, the devtools playground, and the multiplatform lab that picks its pipeline from the hardware.
16
+ - `uixComponentSet`, exported from `@realitycollective/iwsdk-uiextensions`. Pass it to `spatialUI.componentSets` so the IWSDK 0.5 parser accepts the `<uix-*>` control elements. Without it a panel using any control fails to parse and never attaches. No equivalent is needed on the three.js or XR Blocks adapters.
17
+ - `WindowHandle` and `WindowOptionsBase` in `@realitycollective/webxr-uiextensions`. `WindowHandle` is what every adapter's `createWindow` hands back: an `id`, a `panel` that is `undefined` until the adapter has attached the document, and `onReady(listener)`, which runs once and fires immediately if the panel is already there. `WindowOptionsBase` holds the options every adapter understands, and both adapters' option types now extend it, so an option name means the same thing on each. `createWindow` itself stays adapter-specific, because the `config` payload differs per engine.
18
+ - `WindowHost.supportsStandalonePanels`. It says whether `createPanel` works on a host, rather than leaving callers to find out by catching. `false` on the IWSDK host, where the ECS owns panel lifecycles and `createPanel` throws; `true` on the three.js / XR Blocks host.
19
+ - `PanelReadyEvent.kind`, either `window` or `panel`. `window` means the panel came from the adapter's window factory and is managed by the window manager. `panel` means a bare panel the adapter noticed, whose `id` is then the adapter's best stable identifier for it - the config path on IWSDK. The field is optional, so existing listeners keep compiling.
20
+ - `createWindow` on the IWSDK scene host, returning an `IwsdkWindowHandle`: a `WindowHandle` plus the ECS `entity`. It spawns through `createUIWindow`, names the window `uix-window-<n>` when no id is given, and resolves `panel` and `onReady` once the document attaches, so wiring one window needs neither an ECS query nor a filtered `onPanelReady`. `createUIWindow` is unchanged and still returns the entity.
21
+ - `getPanelHandle(entity)` in `@realitycollective/iwsdk-uiextensions`: the panel handle for an entity you already hold, or `undefined` while IWSDK is still loading its document.
22
+
23
+ ### Changed
24
+
25
+ - **Controls are declared as custom elements rather than attributes.** `<uix-stepper>` replaces `<div data-uix="stepper">`, and each part is its own element: `<uix-value>`, `<uix-line>`, `<uix-label>` and so on, replacing `data-uix-role`. Parameters are unchanged and stay `data-uix-*`. This is what makes one markup file portable: IWSDK 0.5 parses with `@drawcall/uikitml`, which rejects every `data-*` attribute on a built-in tag but accepts custom tags declared in a component set, while the three.js and XR Blocks parser accepts custom tags with no registration at all. Both expose the declared tag on `userData.customElement.componentName`, so the engine-free upgraders read one contract on every adapter.
26
+ - The expandable label's `toggle` part is now `<uix-more>`. As an element the old name would have collided with the `<uix-toggle>` control.
27
+ - `@realitycollective/iwsdk-uiextensions` targets **IWSDK 0.5.x**: peer range `>=0.5.0 <0.6.0`, developed against `@iwsdk/core` 0.5.3. `PanelUI` in 0.5 declares a single field, `config`, holding the URL of the UIKitML **source**; the compiled JSON it took on 0.4.x, and the `@iwsdk/vite-plugin-uikitml` step that produced it, are both gone. `maxWidth` and `maxHeight` were removed from the component schema, and ECS ignores unknown fields silently, so a panel that still writes them is mis-sized with no error. Size through `createUIWindow`, which routes to `UIWindow.targetWidth/Height`.
28
+ - `UixWindowHost` resolves a window's `config` by fetching the `.uikitml` source and parsing it, where it previously fetched compiled JSON. Supplying `loadConfig` is no longer necessary for an ordinary panel; override it only for an unusual transport.
29
+ - `compilePanelSource` in `@realitycollective/uix-devtools` serves the **source** on its `blob:` URL rather than compiled JSON, matching what 0.5 fetches. `CompiledPanel.json` is now `CompiledPanel.source`, and the `resolveFile` option is gone because the 0.5 parser has no `<link ref>` stylesheet resolution; put shared rules in a `<style>` block, of which several are allowed and merge. Validation now runs on `@drawcall/uikitml`, pinned to the version `@iwsdk/core` depends on, so a source that passes here is one `PanelUISystem` will accept and no second parser enters the tree.
30
+ - Example and demo markup uses `rgba()` or eight-digit hex rather than `background-opacity`, which the 0.5 parser removed. Both parsers accept the replacement, so one stylesheet serves every adapter.
31
+ - The IWSDK component kit is selected by name, `spatialUI: { kit: 'horizon' }`, rather than by passing an imported kit module. The kits now live inside the parser.
32
+ - `@types/three` is pinned to `0.184.0` across the workspace. IWSDK 0.5.3 and xrblocks resolved different copies, and two copies of the three typings make structurally identical `Object3D` types mutually unassignable, which surfaces as spurious errors far from their cause.
33
+ - `onPanelReady` on the IWSDK host announces bare panels as well as managed windows. A `PanelUI` entity with no `UIWindow`, or with an empty `windowId`, used to be dropped silently; it is now announced with `kind: 'panel'` and its config path as the id. Listeners that only want managed windows should check `kind`.
34
+ - `id` is optional in the XR Blocks host's `CreateWindowOptions`; a window created without one is named `uix-window-<n>`, as on IWSDK. The options also accept `movable` for parity, but this host has no title-bar drag of its own yet, so the flag is recorded and not acted on.
35
+ - The XR Blocks window handle gains `panel` - the document, which already implements `PanelHandle` - and `onReady`, which fires synchronously because uikitml interprets the markup during `createWindow`. The interface is now named `XrBlocksWindowHandle`, with `WindowHandle` kept as an alias, because the core exports a `WindowHandle` of its own.
36
+ - Coverage gates `packages/iwsdk-uiextensions/src/factory.ts` and `src/scene-host.ts` at the same 100% as the core. `new World()` from `@iwsdk/core` constructs headlessly - no renderer, no WebGL, no XR session - so the factories, the scene host and its ECS query all run for real in a node test. The per-frame ECS systems still need a live world and stay outside the gate.
16
37
 
17
38
  [0.1.0]: https://github.com/realitycollective/WebXR-UIExtensions/commits/main
package/README.md CHANGED
@@ -32,7 +32,8 @@ src/controls/ data-uix markup upgraders, driven through the structural
32
32
  src/chrome/ window chrome conventions: contractual element ids
33
33
  (uix-titlebar, uix-pin, ...) + reference UIKitML snippet
34
34
  src/adapter.ts the platform-adapter contract: PanelHost, PanelHandle,
35
- HeadPoseSource, PointerInputSource (plain tuples, no engine)
35
+ WindowHost, WindowHandle, WindowOptionsBase, HeadPoseSource,
36
+ PointerInputSource (plain tuples, no engine)
36
37
  ```
37
38
 
38
39
  ## Writing an adapter
@@ -45,6 +46,18 @@ An adapter supplies three capabilities and drives the core from its frame loop:
45
46
 
46
47
  The IWSDK adapter is the reference implementation; the XR Blocks adapter shows the same contract bound without an ECS.
47
48
 
49
+ ### The window surface
50
+
51
+ `WindowHost` is what app code writes against once panels exist. Four members carry the whole contract, and both shipped adapters honour all four:
52
+
53
+ - `supportsStandalonePanels: boolean` - whether `createPanel` works here. IWSDK reports `false` because the ECS owns panel lifecycles and `createPanel` throws; the three.js host reports `true`. Check it rather than guessing, and spawn a window when it is `false`.
54
+ - `onPanelReady(listener)` - fires as each panel becomes wireable and replays the ones already live, so wiring order never matters. The event's `kind` says what became ready: `window` for one created through the window factory, `panel` for a bare panel the adapter noticed. A bare panel's `id` is the adapter's best stable identifier for it, which on IWSDK is the config path. Only a host that discovers panels the app created outside the window factory ever reports `panel`; the three.js and XR Blocks host reports `window` only, because a standalone `createPanel` document is handed back to the caller and never announced.
55
+ - `createWindow(options)` - adapter-specific, because the `config` payload differs per engine, but it always returns a `WindowHandle`.
56
+ - `WindowHandle` - `id`, `panel` (`undefined` until the document is attached) and `onReady(listener)`, which runs once and fires immediately if the panel is already there. It is the per-window form of `onPanelReady`, for when you hold a handle and want only that window.
57
+ - Getting the panel later, when you did not keep the handle - on IWSDK call `getPanelHandle(entity)` with the window's entity; on the three.js and XR Blocks host call `host.window(id)?.panel`. Both return the same `PanelHandle`.
58
+
59
+ Options are shared even though `createWindow` is not: every adapter's option type extends `WindowOptionsBase` (`id`, `title`, `dockMode`, `position`, `maxWidth`/`maxHeight`, `movable`, `closable`, `minimizable`, `pinnable`, `followOffset`/`followSpeed`/`followTolerance`, `region`). An option means the same thing everywhere, so one `SceneWindow` maps onto every adapter with no translation table.
60
+
48
61
  ## Testing
49
62
 
50
63
  ```bash
package/dist/adapter.d.ts CHANGED
@@ -10,6 +10,15 @@
10
10
  * - {@link PointerInputSource}: deliver ray/pointer press-move-release
11
11
  * - {@link HeadPoseSource}: the viewer pose, for follow mode & body-lock
12
12
  *
13
+ * On top of those, {@link WindowHost} adds the portable window surface:
14
+ * {@link WindowHost.onPanelReady} for readiness, {@link PanelReadyEvent.kind}
15
+ * to tell a managed window from a bare panel, and
16
+ * {@link WindowHost.supportsStandalonePanels} to say whether `createPanel` is
17
+ * usable at all. Each adapter's own `createWindow` returns a
18
+ * {@link WindowHandle} and takes options extending {@link WindowOptionsBase},
19
+ * so window code reads the same on every engine even though the `config`
20
+ * payload does not.
21
+ *
13
22
  * Known adapters:
14
23
  * - `@realitycollective/iwsdk-uiextensions` - Meta IWSDK (ECS systems bind
15
24
  * these capabilities to `@iwsdk/core` components)
@@ -18,6 +27,7 @@
18
27
  *
19
28
  * The interfaces use plain tuples/records only - no engine, no three.js.
20
29
  */
30
+ import type { DockModeValue } from './core/dock-state.js';
21
31
  import type { UixElement } from './controls/element.js';
22
32
  /** Position as [x, y, z] in meters, world space unless stated otherwise. */
23
33
  export type Vec3Tuple = [number, number, number];
@@ -66,6 +76,18 @@ export interface PanelReadyEvent {
66
76
  id: string;
67
77
  /** The live panel - traverse it, or look elements up by markup id. */
68
78
  panel: PanelHandle;
79
+ /**
80
+ * What became ready.
81
+ *
82
+ * - `window` - created through the adapter's window factory and managed by
83
+ * the window manager, so `id` is the id the caller asked for.
84
+ * - `panel` - a bare panel the adapter noticed. `id` is then the adapter's
85
+ * best stable identifier for it, which on IWSDK is the panel's config
86
+ * path.
87
+ *
88
+ * Left optional so existing listeners keep compiling; adapters set it.
89
+ */
90
+ kind?: 'window' | 'panel';
69
91
  }
70
92
  /**
71
93
  * The engine-agnostic surface an app needs to build a UI: spawn windows and
@@ -80,12 +102,74 @@ export interface PanelReadyEvent {
80
102
  * ready before the listener was registered, so wiring order never matters.
81
103
  */
82
104
  export interface WindowHost extends PanelHost {
105
+ /**
106
+ * Whether {@link PanelHost.createPanel} works on this host. When `false`
107
+ * the method is not available and throws; spawn a window instead, so the
108
+ * engine owns the panel lifecycle. IWSDK is `false`, three.js/XR Blocks is
109
+ * `true`.
110
+ */
111
+ readonly supportsStandalonePanels: boolean;
83
112
  /**
84
113
  * Subscribe to panel readiness. Late subscribers are replayed the windows
85
114
  * that are already live. Returns an unsubscribe function.
86
115
  */
87
116
  onPanelReady(listener: (event: PanelReadyEvent) => void): () => void;
88
117
  }
118
+ /**
119
+ * A window an adapter spawned, before its panel necessarily exists.
120
+ *
121
+ * `createWindow` itself stays adapter-specific because the `config` payload
122
+ * differs per engine, but what it hands back is the same everywhere: an id, a
123
+ * panel once there is one, and a one-shot readiness callback.
124
+ */
125
+ export interface WindowHandle {
126
+ /** The window's id - the one passed in, or one the adapter generated. */
127
+ readonly id: string;
128
+ /**
129
+ * The live panel, or `undefined` until the adapter has attached the
130
+ * document. IWSDK loads and parses the markup over later frames; the
131
+ * three.js host interprets it during `createWindow`, so there it is set
132
+ * straight away.
133
+ */
134
+ readonly panel: PanelHandle | undefined;
135
+ /**
136
+ * Run `listener` once, when the panel is attached. Fires immediately if it
137
+ * already is, so wiring order never matters. Returns an unsubscribe
138
+ * function for the case where the caller gives up first.
139
+ */
140
+ onReady(listener: (panel: PanelHandle) => void): () => void;
141
+ }
142
+ /**
143
+ * The window options every adapter understands.
144
+ *
145
+ * An adapter's own `CreateWindowOptions` extends this and adds only what its
146
+ * engine needs - chiefly `config`, whose type differs (IWSDK takes a source
147
+ * path, the three.js host takes parsed markup). Keeping the rest here is what
148
+ * lets one `SceneWindow` map onto every adapter without a translation table.
149
+ */
150
+ export interface WindowOptionsBase {
151
+ /** Stable window id. Adapters generate one when it is absent. */
152
+ id?: string;
153
+ /** Title text written into the window chrome's title element. */
154
+ title?: string;
155
+ dockMode?: DockModeValue;
156
+ /** World position for world-locked windows. */
157
+ position?: Vec3Tuple;
158
+ /** Fit the panel into this box in meters, preserving aspect ratio. */
159
+ maxWidth?: number;
160
+ maxHeight?: number;
161
+ /** Whether the title bar drags the window. */
162
+ movable?: boolean;
163
+ closable?: boolean;
164
+ minimizable?: boolean;
165
+ pinnable?: boolean;
166
+ /** Head-relative offset used in body-follow mode (meters). */
167
+ followOffset?: Vec3Tuple;
168
+ followSpeed?: number;
169
+ followTolerance?: number;
170
+ /** Dock straight into this region on spawn. */
171
+ region?: string;
172
+ }
89
173
  /** One pointer/ray interaction stream, engine-normalised. */
90
174
  export interface PointerSample {
91
175
  /** Pointer world position (ray origin or touch point). */
@@ -1 +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"]}
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 * On top of those, {@link WindowHost} adds the portable window surface:\n * {@link WindowHost.onPanelReady} for readiness, {@link PanelReadyEvent.kind}\n * to tell a managed window from a bare panel, and\n * {@link WindowHost.supportsStandalonePanels} to say whether `createPanel` is\n * usable at all. Each adapter's own `createWindow` returns a\n * {@link WindowHandle} and takes options extending {@link WindowOptionsBase},\n * so window code reads the same on every engine even though the `config`\n * payload does not.\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 { DockModeValue } from './core/dock-state.js';\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 * What became ready.\n *\n * - `window` - created through the adapter's window factory and managed by\n * the window manager, so `id` is the id the caller asked for.\n * - `panel` - a bare panel the adapter noticed. `id` is then the adapter's\n * best stable identifier for it, which on IWSDK is the panel's config\n * path.\n *\n * Left optional so existing listeners keep compiling; adapters set it.\n */\n kind?: 'window' | 'panel';\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 * Whether {@link PanelHost.createPanel} works on this host. When `false`\n * the method is not available and throws; spawn a window instead, so the\n * engine owns the panel lifecycle. IWSDK is `false`, three.js/XR Blocks is\n * `true`.\n */\n readonly supportsStandalonePanels: boolean;\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/**\n * A window an adapter spawned, before its panel necessarily exists.\n *\n * `createWindow` itself stays adapter-specific because the `config` payload\n * differs per engine, but what it hands back is the same everywhere: an id, a\n * panel once there is one, and a one-shot readiness callback.\n */\nexport interface WindowHandle {\n /** The window's id - the one passed in, or one the adapter generated. */\n readonly id: string;\n /**\n * The live panel, or `undefined` until the adapter has attached the\n * document. IWSDK loads and parses the markup over later frames; the\n * three.js host interprets it during `createWindow`, so there it is set\n * straight away.\n */\n readonly panel: PanelHandle | undefined;\n /**\n * Run `listener` once, when the panel is attached. Fires immediately if it\n * already is, so wiring order never matters. Returns an unsubscribe\n * function for the case where the caller gives up first.\n */\n onReady(listener: (panel: PanelHandle) => void): () => void;\n}\n\n/**\n * The window options every adapter understands.\n *\n * An adapter's own `CreateWindowOptions` extends this and adds only what its\n * engine needs - chiefly `config`, whose type differs (IWSDK takes a source\n * path, the three.js host takes parsed markup). Keeping the rest here is what\n * lets one `SceneWindow` map onto every adapter without a translation table.\n */\nexport interface WindowOptionsBase {\n /** Stable window id. Adapters generate one when it is absent. */\n id?: string;\n /** Title text written into the window chrome's title element. */\n title?: string;\n dockMode?: DockModeValue;\n /** World position for world-locked windows. */\n position?: Vec3Tuple;\n /** Fit the panel into this box in meters, preserving aspect ratio. */\n maxWidth?: number;\n maxHeight?: number;\n /** Whether the title bar drags the window. */\n movable?: boolean;\n closable?: boolean;\n minimizable?: boolean;\n pinnable?: boolean;\n /** Head-relative offset used in body-follow mode (meters). */\n followOffset?: Vec3Tuple;\n followSpeed?: number;\n followTolerance?: number;\n /** Dock straight into this region on spawn. */\n region?: string;\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"]}
@@ -1,10 +1,17 @@
1
1
  /**
2
2
  * Structural view of a uikit element as the controls layer needs it.
3
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.
4
+ * Controls and their parts are declared as CUSTOM ELEMENTS - `<uix-stepper>`,
5
+ * `<uix-value>` - rather than as attributes on a `<div>`. That is what makes
6
+ * one markup file portable across every adapter: the IWSDK 0.5 parser rejects
7
+ * unknown attributes on built-in tags, but accepts a custom tag declared in a
8
+ * component set, and the three.js / XR Blocks parser accepts custom tags with
9
+ * no registration at all.
10
+ *
11
+ * Both paths expose the declared tag the same way, on
12
+ * `userData.customElement.componentName`. Parameters stay `data-uix-*`
13
+ * attributes, which land in `userData` camelCased with `data-` stripped:
14
+ * `data-uix-min="0"` → `userData.uixMin`.
8
15
  */
9
16
  export interface UixElement {
10
17
  userData: Record<string, unknown>;
@@ -14,7 +21,12 @@ export interface UixElement {
14
21
  }
15
22
  /** Depth-first traversal over uikit element children. */
16
23
  export declare function walk(element: UixElement, visit: (element: UixElement) => void): void;
17
- /** All descendants (including self) with `data-uix-role="<role>"`. */
24
+ /**
25
+ * The custom-element tag this element was declared with, lowercased, or
26
+ * undefined for a plain built-in element.
27
+ */
28
+ export declare function tagOf(element: UixElement): string | undefined;
29
+ /** All descendants (including self) declared as `<uix-{role}>`. */
18
30
  export declare function findRoles(root: UixElement, role: string): UixElement[];
19
31
  export declare function findRole(root: UixElement, role: string): UixElement | undefined;
20
32
  export declare function attrString(element: UixElement, key: string): string | undefined;
@@ -5,11 +5,21 @@ export function walk(element, visit) {
5
5
  walk(child, visit);
6
6
  }
7
7
  }
8
- /** All descendants (including self) with `data-uix-role="<role>"`. */
8
+ /**
9
+ * The custom-element tag this element was declared with, lowercased, or
10
+ * undefined for a plain built-in element.
11
+ */
12
+ export function tagOf(element) {
13
+ const custom = element.userData?.['customElement'];
14
+ const name = custom?.componentName;
15
+ return typeof name === 'string' && name.length > 0 ? name.toLowerCase() : undefined;
16
+ }
17
+ /** All descendants (including self) declared as `<uix-{role}>`. */
9
18
  export function findRoles(root, role) {
19
+ const tag = `uix-${role}`;
10
20
  const found = [];
11
21
  walk(root, (element) => {
12
- if (element.userData?.['uixRole'] === role) {
22
+ if (tagOf(element) === tag) {
13
23
  found.push(element);
14
24
  }
15
25
  });
@@ -1 +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"]}
1
+ {"version":3,"file":"element.js","sourceRoot":"","sources":["../../src/controls/element.ts"],"names":[],"mappings":"AAsBA,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;;;GAGG;AACH,MAAM,UAAU,KAAK,CAAC,OAAmB;IACvC,MAAM,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,eAAe,CAEpC,CAAC;IACd,MAAM,IAAI,GAAG,MAAM,EAAE,aAAa,CAAC;IACnC,OAAO,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;AACtF,CAAC;AAED,mEAAmE;AACnE,MAAM,UAAU,SAAS,CAAC,IAAgB,EAAE,IAAY;IACtD,MAAM,GAAG,GAAG,OAAO,IAAI,EAAE,CAAC;IAC1B,MAAM,KAAK,GAAiB,EAAE,CAAC;IAC/B,IAAI,CAAC,IAAI,EAAE,CAAC,OAAO,EAAE,EAAE;QACrB,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,GAAG,EAAE,CAAC;YAC3B,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 * Controls and their parts are declared as CUSTOM ELEMENTS - `<uix-stepper>`,\n * `<uix-value>` - rather than as attributes on a `<div>`. That is what makes\n * one markup file portable across every adapter: the IWSDK 0.5 parser rejects\n * unknown attributes on built-in tags, but accepts a custom tag declared in a\n * component set, and the three.js / XR Blocks parser accepts custom tags with\n * no registration at all.\n *\n * Both paths expose the declared tag the same way, on\n * `userData.customElement.componentName`. Parameters stay `data-uix-*`\n * attributes, which land in `userData` camelCased with `data-` stripped:\n * `data-uix-min=\"0\"` → `userData.uixMin`.\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/**\n * The custom-element tag this element was declared with, lowercased, or\n * undefined for a plain built-in element.\n */\nexport function tagOf(element: UixElement): string | undefined {\n const custom = element.userData?.['customElement'] as\n | { componentName?: unknown }\n | undefined;\n const name = custom?.componentName;\n return typeof name === 'string' && name.length > 0 ? name.toLowerCase() : undefined;\n}\n\n/** All descendants (including self) declared as `<uix-{role}>`. */\nexport function findRoles(root: UixElement, role: string): UixElement[] {\n const tag = `uix-${role}`;\n const found: UixElement[] = [];\n walk(root, (element) => {\n if (tagOf(element) === tag) {\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"]}
@@ -1,16 +1,16 @@
1
1
  /**
2
- * Expandable label - `data-uix="expandable-label"`.
2
+ * Expandable label - `<uix-expandable-label>`.
3
3
  *
4
4
  * A multi-line label that collapses to a character budget with an ellipsis
5
5
  * and an inline more/less affordance.
6
6
  *
7
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>
8
+ * <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
+ * <uix-text>.</uix-text>
12
+ * <uix-more>more</uix-more>
13
+ * </uix-expandable-label>
14
14
  * ```
15
15
  */
16
16
  import { Emitter } from '../core/events.js';
@@ -1,16 +1,16 @@
1
1
  /**
2
- * Expandable label - `data-uix="expandable-label"`.
2
+ * Expandable label - `<uix-expandable-label>`.
3
3
  *
4
4
  * A multi-line label that collapses to a character budget with an ellipsis
5
5
  * and an inline more/less affordance.
6
6
  *
7
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>
8
+ * <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
+ * <uix-text>.</uix-text>
12
+ * <uix-more>more</uix-more>
13
+ * </uix-expandable-label>
14
14
  * ```
15
15
  */
16
16
  import { Emitter } from '../core/events.js';
@@ -55,7 +55,7 @@ export function upgradeExpandableLabel(root) {
55
55
  ...(lines !== undefined && { collapsedLines: lines }),
56
56
  ...(charsPerLine !== undefined && { charsPerLine }),
57
57
  });
58
- const toggle = findRole(root, 'toggle');
58
+ const toggle = findRole(root, 'more');
59
59
  const handle = new ExpandableLabelHandle(model, findRole(root, 'text'), toggle);
60
60
  toggle?.addEventListener('click', () => handle.toggle());
61
61
  return handle;
@@ -1 +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"]}
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,MAAM,CAAC,CAAC;IACtC,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 - `<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 * <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 * <uix-text>.</uix-text>\n * <uix-more>more</uix-more>\n * </uix-expandable-label>\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, 'more');\n const handle = new ExpandableLabelHandle(model, findRole(root, 'text'), toggle);\n toggle?.addEventListener('click', () => handle.toggle());\n return handle;\n}\n"]}
@@ -1,25 +1,25 @@
1
1
  /**
2
- * Log view - `data-uix="log-view"`.
2
+ * Log view - `<uix-log-view>`.
3
3
  *
4
4
  * A console-style scrolling list rendered into a fixed pool of row elements
5
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.
6
+ * are the `<uix-line>` descendants, in document order; their count is the
7
+ * viewport height.
8
8
  *
9
9
  * ```html
10
- * <div data-uix="log-view" data-uix-id="combat" data-uix-capacity="200">
10
+ * <uix-log-view data-uix-id="combat" data-uix-capacity="200">
11
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>
12
+ * <uix-line>.</uix-line>
13
+ * <uix-line>.</uix-line>
14
+ * <uix-line>.</uix-line>
15
15
  * </div>
16
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>
17
+ * <uix-up>▲</uix-up>
18
+ * <uix-down>▼</uix-down>
19
+ * <uix-clear>clear</uix-clear>
20
20
  * </div>
21
- * <span data-uix-role="status">.</span>
22
- * </div>
21
+ * <uix-status>.</uix-status>
22
+ * </uix-log-view>
23
23
  * ```
24
24
  */
25
25
  import { LogModel } from '../core/log-model.js';
@@ -1,25 +1,25 @@
1
1
  /**
2
- * Log view - `data-uix="log-view"`.
2
+ * Log view - `<uix-log-view>`.
3
3
  *
4
4
  * A console-style scrolling list rendered into a fixed pool of row elements
5
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.
6
+ * are the `<uix-line>` descendants, in document order; their count is the
7
+ * viewport height.
8
8
  *
9
9
  * ```html
10
- * <div data-uix="log-view" data-uix-id="combat" data-uix-capacity="200">
10
+ * <uix-log-view data-uix-id="combat" data-uix-capacity="200">
11
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>
12
+ * <uix-line>.</uix-line>
13
+ * <uix-line>.</uix-line>
14
+ * <uix-line>.</uix-line>
15
15
  * </div>
16
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>
17
+ * <uix-up>▲</uix-up>
18
+ * <uix-down>▼</uix-down>
19
+ * <uix-clear>clear</uix-clear>
20
20
  * </div>
21
- * <span data-uix-role="status">.</span>
22
- * </div>
21
+ * <uix-status>.</uix-status>
22
+ * </uix-log-view>
23
23
  * ```
24
24
  */
25
25
  import { LogModel } from '../core/log-model.js';
@@ -1 +1 @@
1
- {"version":3,"file":"log-view.js","sourceRoot":"","sources":["../../src/controls/log-view.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Log view - `data-uix=\"log-view\"`.\n *\n * A console-style scrolling list rendered into a fixed pool of row elements\n * (UIKitML's text rule: each row needs a literal placeholder child). Rows\n * are the descendants with role `line`, in document order; their count is\n * the viewport height.\n *\n * ```html\n * <div data-uix=\"log-view\" data-uix-id=\"combat\" data-uix-capacity=\"200\">\n * <div class=\"rows\">\n * <span data-uix-role=\"line\">.</span>\n * <span data-uix-role=\"line\">.</span>\n * <span data-uix-role=\"line\">.</span>\n * </div>\n * <div class=\"log-buttons\">\n * <button data-uix-role=\"up\">▲</button>\n * <button data-uix-role=\"down\">▼</button>\n * <button data-uix-role=\"clear\">clear</button>\n * </div>\n * <span data-uix-role=\"status\">.</span>\n * </div>\n * ```\n */\nimport { LogModel } from '../core/log-model.js';\nimport { UixElement, attrNumber, findRole, findRoles } from './element.js';\n\nexport class LogViewHandle {\n constructor(\n readonly model: LogModel,\n private rows: UixElement[],\n private statusElement: UixElement | undefined,\n ) {\n this.render();\n }\n\n push(message: string, level = 'info'): void {\n this.model.push(message, level);\n this.render();\n }\n\n clear(): void {\n this.model.clear();\n this.render();\n }\n\n scrollUp(rows = 1): void {\n this.model.scrollUp(rows);\n this.render();\n }\n\n scrollDown(rows = 1): void {\n this.model.scrollDown(rows);\n this.render();\n }\n\n private render(): void {\n const visible = this.model.visible();\n this.rows.forEach((row, index) => {\n const entry = visible[index];\n row.setProperties({\n // UIKitML renders \"\" as empty text - exactly what vacant rows need.\n text: entry ? entry.message : '',\n });\n });\n if (this.statusElement) {\n const suffix = this.model.isFollowing ? '' : ' (scrolled)';\n this.statusElement.setProperties({\n text: `${this.model.count} entries${suffix}`,\n });\n }\n }\n}\n\nexport function upgradeLogView(root: UixElement): LogViewHandle {\n const rows = findRoles(root, 'line');\n const capacity = attrNumber(root, 'uixCapacity');\n const model = new LogModel({\n visibleRows: Math.max(1, rows.length),\n ...(capacity !== undefined && { capacity }),\n });\n const handle = new LogViewHandle(model, rows, findRole(root, 'status'));\n findRole(root, 'up')?.addEventListener('click', () => handle.scrollUp());\n findRole(root, 'down')?.addEventListener('click', () => handle.scrollDown());\n findRole(root, 'clear')?.addEventListener('click', () => handle.clear());\n return handle;\n}\n"]}
1
+ {"version":3,"file":"log-view.js","sourceRoot":"","sources":["../../src/controls/log-view.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Log view - `<uix-log-view>`.\n *\n * A console-style scrolling list rendered into a fixed pool of row elements\n * (UIKitML's text rule: each row needs a literal placeholder child). Rows\n * are the `<uix-line>` descendants, in document order; their count is the\n * viewport height.\n *\n * ```html\n * <uix-log-view data-uix-id=\"combat\" data-uix-capacity=\"200\">\n * <div class=\"rows\">\n * <uix-line>.</uix-line>\n * <uix-line>.</uix-line>\n * <uix-line>.</uix-line>\n * </div>\n * <div class=\"log-buttons\">\n * <uix-up>▲</uix-up>\n * <uix-down>▼</uix-down>\n * <uix-clear>clear</uix-clear>\n * </div>\n * <uix-status>.</uix-status>\n * </uix-log-view>\n * ```\n */\nimport { LogModel } from '../core/log-model.js';\nimport { UixElement, attrNumber, findRole, findRoles } from './element.js';\n\nexport class LogViewHandle {\n constructor(\n readonly model: LogModel,\n private rows: UixElement[],\n private statusElement: UixElement | undefined,\n ) {\n this.render();\n }\n\n push(message: string, level = 'info'): void {\n this.model.push(message, level);\n this.render();\n }\n\n clear(): void {\n this.model.clear();\n this.render();\n }\n\n scrollUp(rows = 1): void {\n this.model.scrollUp(rows);\n this.render();\n }\n\n scrollDown(rows = 1): void {\n this.model.scrollDown(rows);\n this.render();\n }\n\n private render(): void {\n const visible = this.model.visible();\n this.rows.forEach((row, index) => {\n const entry = visible[index];\n row.setProperties({\n // UIKitML renders \"\" as empty text - exactly what vacant rows need.\n text: entry ? entry.message : '',\n });\n });\n if (this.statusElement) {\n const suffix = this.model.isFollowing ? '' : ' (scrolled)';\n this.statusElement.setProperties({\n text: `${this.model.count} entries${suffix}`,\n });\n }\n }\n}\n\nexport function upgradeLogView(root: UixElement): LogViewHandle {\n const rows = findRoles(root, 'line');\n const capacity = attrNumber(root, 'uixCapacity');\n const model = new LogModel({\n visibleRows: Math.max(1, rows.length),\n ...(capacity !== undefined && { capacity }),\n });\n const handle = new LogViewHandle(model, rows, findRole(root, 'status'));\n findRole(root, 'up')?.addEventListener('click', () => handle.scrollUp());\n findRole(root, 'down')?.addEventListener('click', () => handle.scrollDown());\n findRole(root, 'clear')?.addEventListener('click', () => handle.clear());\n return handle;\n}\n"]}
@@ -1,14 +1,14 @@
1
1
  /**
2
- * Stepper control - `data-uix="stepper"`.
2
+ * Stepper control - `<uix-stepper>`.
3
3
  *
4
- * Markup contract (roles are `data-uix-role` attributes on descendants):
4
+ * Markup contract (parts are `<uix-*>` elements; params are `data-uix-*`):
5
5
  * ```html
6
- * <div data-uix="stepper" data-uix-id="count" data-uix-min="0" data-uix-max="10"
7
- * data-uix-step="1" data-uix-value="5" class="my-stepper">
8
- * <button data-uix-role="decrement">-</button>
9
- * <span data-uix-role="value">.</span>
10
- * <button data-uix-role="increment">+</button>
11
- * </div>
6
+ * <uix-stepper data-uix-id="count" data-uix-min="0" data-uix-max="10"
7
+ * data-uix-step="1" data-uix-value="5" class="my-stepper">
8
+ * <uix-decrement>-</uix-decrement>
9
+ * <uix-value>.</uix-value>
10
+ * <uix-increment>+</uix-increment>
11
+ * </uix-stepper>
12
12
  * ```
13
13
  */
14
14
  import { Emitter } from '../core/events.js';
@@ -1,14 +1,14 @@
1
1
  /**
2
- * Stepper control - `data-uix="stepper"`.
2
+ * Stepper control - `<uix-stepper>`.
3
3
  *
4
- * Markup contract (roles are `data-uix-role` attributes on descendants):
4
+ * Markup contract (parts are `<uix-*>` elements; params are `data-uix-*`):
5
5
  * ```html
6
- * <div data-uix="stepper" data-uix-id="count" data-uix-min="0" data-uix-max="10"
7
- * data-uix-step="1" data-uix-value="5" class="my-stepper">
8
- * <button data-uix-role="decrement">-</button>
9
- * <span data-uix-role="value">.</span>
10
- * <button data-uix-role="increment">+</button>
11
- * </div>
6
+ * <uix-stepper data-uix-id="count" data-uix-min="0" data-uix-max="10"
7
+ * data-uix-step="1" data-uix-value="5" class="my-stepper">
8
+ * <uix-decrement>-</uix-decrement>
9
+ * <uix-value>.</uix-value>
10
+ * <uix-increment>+</uix-increment>
11
+ * </uix-stepper>
12
12
  * ```
13
13
  */
14
14
  import { Emitter } from '../core/events.js';
@@ -1 +1 @@
1
- {"version":3,"file":"stepper.js","sourceRoot":"","sources":["../../src/controls/stepper.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Stepper control - `data-uix=\"stepper\"`.\n *\n * Markup contract (roles are `data-uix-role` attributes on descendants):\n * ```html\n * <div data-uix=\"stepper\" data-uix-id=\"count\" data-uix-min=\"0\" data-uix-max=\"10\"\n * data-uix-step=\"1\" data-uix-value=\"5\" class=\"my-stepper\">\n * <button data-uix-role=\"decrement\">-</button>\n * <span data-uix-role=\"value\">.</span>\n * <button data-uix-role=\"increment\">+</button>\n * </div>\n * ```\n */\nimport { Emitter } from '../core/events.js';\nimport { StepperModel, type StepperOptions } from '../core/stepper-model.js';\nimport { UixElement, attrNumber, findRole } from './element.js';\n\nconst DISABLED_OPACITY = 0.35;\n\nexport class StepperHandle {\n readonly events = new Emitter<{ change: number }>();\n\n constructor(\n readonly model: StepperModel,\n private valueElement: UixElement | undefined,\n private decrementElement: UixElement | undefined,\n private incrementElement: UixElement | undefined,\n ) {\n this.render();\n }\n\n get value(): number {\n return this.model.value;\n }\n\n set(value: number): void {\n if (this.model.set(value)) {\n this.render();\n this.events.emit('change', this.model.value);\n }\n }\n\n increment(): void {\n this.set(this.model.value + this.model.step);\n }\n\n decrement(): void {\n this.set(this.model.value - this.model.step);\n }\n\n private render(): void {\n this.valueElement?.setProperties({ text: String(this.model.value) });\n this.decrementElement?.setProperties({\n opacity: this.model.canDecrement ? 1 : DISABLED_OPACITY,\n });\n this.incrementElement?.setProperties({\n opacity: this.model.canIncrement ? 1 : DISABLED_OPACITY,\n });\n }\n}\n\nexport function upgradeStepper(root: UixElement): StepperHandle {\n const options: StepperOptions = {};\n const min = attrNumber(root, 'uixMin');\n const max = attrNumber(root, 'uixMax');\n const step = attrNumber(root, 'uixStep');\n const value = attrNumber(root, 'uixValue');\n if (min !== undefined) options.min = min;\n if (max !== undefined) options.max = max;\n if (step !== undefined) options.step = step;\n if (value !== undefined) options.value = value;\n const model = new StepperModel(options);\n const decrement = findRole(root, 'decrement');\n const increment = findRole(root, 'increment');\n const handle = new StepperHandle(model, findRole(root, 'value'), decrement, increment);\n decrement?.addEventListener('click', () => handle.decrement());\n increment?.addEventListener('click', () => handle.increment());\n return handle;\n}\n"]}
1
+ {"version":3,"file":"stepper.js","sourceRoot":"","sources":["../../src/controls/stepper.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Stepper control - `<uix-stepper>`.\n *\n * Markup contract (parts are `<uix-*>` elements; params are `data-uix-*`):\n * ```html\n * <uix-stepper data-uix-id=\"count\" data-uix-min=\"0\" data-uix-max=\"10\"\n * data-uix-step=\"1\" data-uix-value=\"5\" class=\"my-stepper\">\n * <uix-decrement>-</uix-decrement>\n * <uix-value>.</uix-value>\n * <uix-increment>+</uix-increment>\n * </uix-stepper>\n * ```\n */\nimport { Emitter } from '../core/events.js';\nimport { StepperModel, type StepperOptions } from '../core/stepper-model.js';\nimport { UixElement, attrNumber, findRole } from './element.js';\n\nconst DISABLED_OPACITY = 0.35;\n\nexport class StepperHandle {\n readonly events = new Emitter<{ change: number }>();\n\n constructor(\n readonly model: StepperModel,\n private valueElement: UixElement | undefined,\n private decrementElement: UixElement | undefined,\n private incrementElement: UixElement | undefined,\n ) {\n this.render();\n }\n\n get value(): number {\n return this.model.value;\n }\n\n set(value: number): void {\n if (this.model.set(value)) {\n this.render();\n this.events.emit('change', this.model.value);\n }\n }\n\n increment(): void {\n this.set(this.model.value + this.model.step);\n }\n\n decrement(): void {\n this.set(this.model.value - this.model.step);\n }\n\n private render(): void {\n this.valueElement?.setProperties({ text: String(this.model.value) });\n this.decrementElement?.setProperties({\n opacity: this.model.canDecrement ? 1 : DISABLED_OPACITY,\n });\n this.incrementElement?.setProperties({\n opacity: this.model.canIncrement ? 1 : DISABLED_OPACITY,\n });\n }\n}\n\nexport function upgradeStepper(root: UixElement): StepperHandle {\n const options: StepperOptions = {};\n const min = attrNumber(root, 'uixMin');\n const max = attrNumber(root, 'uixMax');\n const step = attrNumber(root, 'uixStep');\n const value = attrNumber(root, 'uixValue');\n if (min !== undefined) options.min = min;\n if (max !== undefined) options.max = max;\n if (step !== undefined) options.step = step;\n if (value !== undefined) options.value = value;\n const model = new StepperModel(options);\n const decrement = findRole(root, 'decrement');\n const increment = findRole(root, 'increment');\n const handle = new StepperHandle(model, findRole(root, 'value'), decrement, increment);\n decrement?.addEventListener('click', () => handle.decrement());\n increment?.addEventListener('click', () => handle.increment());\n return handle;\n}\n"]}
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Toggle control - `data-uix="toggle"`.
2
+ * Toggle control - `<uix-toggle>`.
3
3
  *
4
4
  * The whole control is the click target. Optional attributes:
5
5
  * `data-uix-on` (initial state), `data-uix-on-text` / `data-uix-off-text`
@@ -7,11 +7,11 @@
7
7
  * `data-uix-off-color` (background color applied to the control root).
8
8
  *
9
9
  * ```html
10
- * <div data-uix="toggle" data-uix-id="shield" data-uix-on="true"
11
- * data-uix-on-text="SHIELD ON" data-uix-off-text="SHIELD OFF"
12
- * data-uix-on-color="#2e7d32" data-uix-off-color="#5d4037" class="my-toggle">
13
- * <span data-uix-role="label">.</span>
14
- * </div>
10
+ * <uix-toggle data-uix-id="shield" data-uix-on="true"
11
+ * data-uix-on-text="SHIELD ON" data-uix-off-text="SHIELD OFF"
12
+ * data-uix-on-color="#2e7d32" data-uix-off-color="#5d4037" class="my-toggle">
13
+ * <uix-label>.</uix-label>
14
+ * </uix-toggle>
15
15
  * ```
16
16
  */
17
17
  import { Emitter } from '../core/events.js';
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Toggle control - `data-uix="toggle"`.
2
+ * Toggle control - `<uix-toggle>`.
3
3
  *
4
4
  * The whole control is the click target. Optional attributes:
5
5
  * `data-uix-on` (initial state), `data-uix-on-text` / `data-uix-off-text`
@@ -7,11 +7,11 @@
7
7
  * `data-uix-off-color` (background color applied to the control root).
8
8
  *
9
9
  * ```html
10
- * <div data-uix="toggle" data-uix-id="shield" data-uix-on="true"
11
- * data-uix-on-text="SHIELD ON" data-uix-off-text="SHIELD OFF"
12
- * data-uix-on-color="#2e7d32" data-uix-off-color="#5d4037" class="my-toggle">
13
- * <span data-uix-role="label">.</span>
14
- * </div>
10
+ * <uix-toggle data-uix-id="shield" data-uix-on="true"
11
+ * data-uix-on-text="SHIELD ON" data-uix-off-text="SHIELD OFF"
12
+ * data-uix-on-color="#2e7d32" data-uix-off-color="#5d4037" class="my-toggle">
13
+ * <uix-label>.</uix-label>
14
+ * </uix-toggle>
15
15
  * ```
16
16
  */
17
17
  import { Emitter } from '../core/events.js';
@@ -1 +1 @@
1
- {"version":3,"file":"toggle.js","sourceRoot":"","sources":["../../src/controls/toggle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,EAAc,WAAW,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAE7E,MAAM,OAAO,YAAY;IAIZ;IACD;IACA;IALD,MAAM,GAAG,IAAI,OAAO,EAAuB,CAAC;IAErD,YACW,KAAkB,EACnB,IAAgB,EAChB,YAAoC;QAFnC,UAAK,GAAL,KAAK,CAAa;QACnB,SAAI,GAAJ,IAAI,CAAY;QAChB,iBAAY,GAAZ,YAAY,CAAwB;QAE5C,IAAI,CAAC,MAAM,EAAE,CAAC;IAChB,CAAC;IAED,IAAI,KAAK;QACP,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC;IAC1B,CAAC;IAED,GAAG,CAAC,KAAc;QAChB,IAAI,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;YAC1B,IAAI,CAAC,MAAM,EAAE,CAAC;YACd,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAC/C,CAAC;IACH,CAAC;IAED,MAAM;QACJ,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAC9B,CAAC;IAEO,MAAM;QACZ,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC;QAC5B,MAAM,IAAI,GAAG,EAAE;YACb,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,EAAE,WAAW,CAAC,IAAI,IAAI,CAAC;YAC9C,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,EAAE,YAAY,CAAC,IAAI,KAAK,CAAC,CAAC;QACnD,IAAI,CAAC,YAAY,EAAE,aAAa,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC;QAC3C,MAAM,KAAK,GAAG,EAAE;YACd,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,EAAE,YAAY,CAAC;YACrC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,EAAE,aAAa,CAAC,CAAC;QACzC,IAAI,KAAK,EAAE,CAAC;YACV,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,EAAE,eAAe,EAAE,KAAK,EAAE,CAAC,CAAC;QACtD,CAAC;IACH,CAAC;CACF;AAED,MAAM,UAAU,aAAa,CAAC,IAAgB;IAC5C,MAAM,KAAK,GAAG,IAAI,WAAW,CAAC,WAAW,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,KAAK,CAAC,CAAC;IACnE,MAAM,MAAM,GAAG,IAAI,YAAY,CAAC,KAAK,EAAE,IAAI,EAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC;IACtE,IAAI,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;IACtD,OAAO,MAAM,CAAC;AAChB,CAAC","sourcesContent":["/**\n * Toggle control - `data-uix=\"toggle\"`.\n *\n * The whole control is the click target. Optional attributes:\n * `data-uix-on` (initial state), `data-uix-on-text` / `data-uix-off-text`\n * (label written into the `label` role), `data-uix-on-color` /\n * `data-uix-off-color` (background color applied to the control root).\n *\n * ```html\n * <div data-uix=\"toggle\" data-uix-id=\"shield\" data-uix-on=\"true\"\n * data-uix-on-text=\"SHIELD ON\" data-uix-off-text=\"SHIELD OFF\"\n * data-uix-on-color=\"#2e7d32\" data-uix-off-color=\"#5d4037\" class=\"my-toggle\">\n * <span data-uix-role=\"label\">.</span>\n * </div>\n * ```\n */\nimport { Emitter } from '../core/events.js';\nimport { ToggleModel } from '../core/toggle-model.js';\nimport { UixElement, attrBoolean, attrString, findRole } from './element.js';\n\nexport class ToggleHandle {\n readonly events = new Emitter<{ change: boolean }>();\n\n constructor(\n readonly model: ToggleModel,\n private root: UixElement,\n private labelElement: UixElement | undefined,\n ) {\n this.render();\n }\n\n get value(): boolean {\n return this.model.value;\n }\n\n set(value: boolean): void {\n if (this.model.set(value)) {\n this.render();\n this.events.emit('change', this.model.value);\n }\n }\n\n toggle(): void {\n this.set(!this.model.value);\n }\n\n private render(): void {\n const on = this.model.value;\n const text = on\n ? (attrString(this.root, 'uixOnText') ?? 'ON')\n : (attrString(this.root, 'uixOffText') ?? 'OFF');\n this.labelElement?.setProperties({ text });\n const color = on\n ? attrString(this.root, 'uixOnColor')\n : attrString(this.root, 'uixOffColor');\n if (color) {\n this.root.setProperties({ backgroundColor: color });\n }\n }\n}\n\nexport function upgradeToggle(root: UixElement): ToggleHandle {\n const model = new ToggleModel(attrBoolean(root, 'uixOn') ?? false);\n const handle = new ToggleHandle(model, root, findRole(root, 'label'));\n root.addEventListener('click', () => handle.toggle());\n return handle;\n}\n"]}
1
+ {"version":3,"file":"toggle.js","sourceRoot":"","sources":["../../src/controls/toggle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,EAAc,WAAW,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAE7E,MAAM,OAAO,YAAY;IAIZ;IACD;IACA;IALD,MAAM,GAAG,IAAI,OAAO,EAAuB,CAAC;IAErD,YACW,KAAkB,EACnB,IAAgB,EAChB,YAAoC;QAFnC,UAAK,GAAL,KAAK,CAAa;QACnB,SAAI,GAAJ,IAAI,CAAY;QAChB,iBAAY,GAAZ,YAAY,CAAwB;QAE5C,IAAI,CAAC,MAAM,EAAE,CAAC;IAChB,CAAC;IAED,IAAI,KAAK;QACP,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC;IAC1B,CAAC;IAED,GAAG,CAAC,KAAc;QAChB,IAAI,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;YAC1B,IAAI,CAAC,MAAM,EAAE,CAAC;YACd,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAC/C,CAAC;IACH,CAAC;IAED,MAAM;QACJ,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAC9B,CAAC;IAEO,MAAM;QACZ,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC;QAC5B,MAAM,IAAI,GAAG,EAAE;YACb,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,EAAE,WAAW,CAAC,IAAI,IAAI,CAAC;YAC9C,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,EAAE,YAAY,CAAC,IAAI,KAAK,CAAC,CAAC;QACnD,IAAI,CAAC,YAAY,EAAE,aAAa,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC;QAC3C,MAAM,KAAK,GAAG,EAAE;YACd,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,EAAE,YAAY,CAAC;YACrC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,EAAE,aAAa,CAAC,CAAC;QACzC,IAAI,KAAK,EAAE,CAAC;YACV,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,EAAE,eAAe,EAAE,KAAK,EAAE,CAAC,CAAC;QACtD,CAAC;IACH,CAAC;CACF;AAED,MAAM,UAAU,aAAa,CAAC,IAAgB;IAC5C,MAAM,KAAK,GAAG,IAAI,WAAW,CAAC,WAAW,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,KAAK,CAAC,CAAC;IACnE,MAAM,MAAM,GAAG,IAAI,YAAY,CAAC,KAAK,EAAE,IAAI,EAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC;IACtE,IAAI,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;IACtD,OAAO,MAAM,CAAC;AAChB,CAAC","sourcesContent":["/**\n * Toggle control - `<uix-toggle>`.\n *\n * The whole control is the click target. Optional attributes:\n * `data-uix-on` (initial state), `data-uix-on-text` / `data-uix-off-text`\n * (label written into the `label` role), `data-uix-on-color` /\n * `data-uix-off-color` (background color applied to the control root).\n *\n * ```html\n * <uix-toggle data-uix-id=\"shield\" data-uix-on=\"true\"\n * data-uix-on-text=\"SHIELD ON\" data-uix-off-text=\"SHIELD OFF\"\n * data-uix-on-color=\"#2e7d32\" data-uix-off-color=\"#5d4037\" class=\"my-toggle\">\n * <uix-label>.</uix-label>\n * </uix-toggle>\n * ```\n */\nimport { Emitter } from '../core/events.js';\nimport { ToggleModel } from '../core/toggle-model.js';\nimport { UixElement, attrBoolean, attrString, findRole } from './element.js';\n\nexport class ToggleHandle {\n readonly events = new Emitter<{ change: boolean }>();\n\n constructor(\n readonly model: ToggleModel,\n private root: UixElement,\n private labelElement: UixElement | undefined,\n ) {\n this.render();\n }\n\n get value(): boolean {\n return this.model.value;\n }\n\n set(value: boolean): void {\n if (this.model.set(value)) {\n this.render();\n this.events.emit('change', this.model.value);\n }\n }\n\n toggle(): void {\n this.set(!this.model.value);\n }\n\n private render(): void {\n const on = this.model.value;\n const text = on\n ? (attrString(this.root, 'uixOnText') ?? 'ON')\n : (attrString(this.root, 'uixOffText') ?? 'OFF');\n this.labelElement?.setProperties({ text });\n const color = on\n ? attrString(this.root, 'uixOnColor')\n : attrString(this.root, 'uixOffColor');\n if (color) {\n this.root.setProperties({ backgroundColor: color });\n }\n }\n}\n\nexport function upgradeToggle(root: UixElement): ToggleHandle {\n const model = new ToggleModel(attrBoolean(root, 'uixOn') ?? false);\n const handle = new ToggleHandle(model, root, findRole(root, 'label'));\n root.addEventListener('click', () => handle.toggle());\n return handle;\n}\n"]}
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Panel upgrade - scans a loaded panel for `data-uix` markers and wires the
3
- * matching control behaviour. App code retrieves the resulting handles via
2
+ * Panel upgrade - scans a loaded panel for `<uix-*>` control elements and
3
+ * wires the matching behaviour. App code retrieves the resulting handles via
4
4
  * `panelControlsFor(document)` and the control's `data-uix-id`.
5
5
  */
6
6
  import { UixElement } from './element.js';
@@ -1,9 +1,9 @@
1
1
  /**
2
- * Panel upgrade - scans a loaded panel for `data-uix` markers and wires the
3
- * matching control behaviour. App code retrieves the resulting handles via
2
+ * Panel upgrade - scans a loaded panel for `<uix-*>` control elements and
3
+ * wires the matching behaviour. App code retrieves the resulting handles via
4
4
  * `panelControlsFor(document)` and the control's `data-uix-id`.
5
5
  */
6
- import { attrString, walk } from './element.js';
6
+ import { attrString, tagOf, walk } from './element.js';
7
7
  import { StepperHandle, upgradeStepper } from './stepper.js';
8
8
  import { ToggleHandle, upgradeToggle } from './toggle.js';
9
9
  import { ExpandableLabelHandle, upgradeExpandableLabel } from './expandable-label.js';
@@ -48,12 +48,35 @@ export class PanelControls {
48
48
  return handle;
49
49
  }
50
50
  }
51
+ /**
52
+ * Control tags. These are the custom elements an author writes; every part
53
+ * inside them (`<uix-value>`, `<uix-line>`, …) is a role, resolved by the
54
+ * individual upgraders through `findRole`.
55
+ */
51
56
  const UPGRADERS = {
52
- stepper: upgradeStepper,
53
- toggle: upgradeToggle,
54
- 'expandable-label': upgradeExpandableLabel,
55
- 'log-view': upgradeLogView,
57
+ 'uix-stepper': upgradeStepper,
58
+ 'uix-toggle': upgradeToggle,
59
+ 'uix-expandable-label': upgradeExpandableLabel,
60
+ 'uix-log-view': upgradeLogView,
56
61
  };
62
+ /**
63
+ * Parts claimed by the control that owns them, resolved through `findRole`.
64
+ * Listed here only so a genuine typo in a control tag still gets a warning
65
+ * instead of being silently mistaken for a role.
66
+ */
67
+ const ROLE_TAGS = new Set([
68
+ 'uix-decrement',
69
+ 'uix-value',
70
+ 'uix-increment',
71
+ 'uix-label',
72
+ 'uix-line',
73
+ 'uix-up',
74
+ 'uix-down',
75
+ 'uix-clear',
76
+ 'uix-status',
77
+ 'uix-text',
78
+ 'uix-more',
79
+ ]);
57
80
  const registry = new WeakMap();
58
81
  /**
59
82
  * Upgrade every `data-uix` control under `root` and remember the result
@@ -66,13 +89,15 @@ export function upgradePanel(documentKey, root) {
66
89
  }
67
90
  const controls = new PanelControls();
68
91
  walk(root, (element) => {
69
- const marker = attrString(element, 'uix');
70
- if (marker === undefined) {
92
+ const tag = tagOf(element);
93
+ if (tag === undefined || !tag.startsWith('uix-')) {
71
94
  return;
72
95
  }
73
- const upgrader = UPGRADERS[marker];
96
+ const upgrader = UPGRADERS[tag];
74
97
  if (!upgrader) {
75
- console.warn(`[uix] unknown control type data-uix="${marker}" - skipped`);
98
+ if (!ROLE_TAGS.has(tag)) {
99
+ console.warn(`[uix] unknown control element <${tag}> - skipped`);
100
+ }
76
101
  return;
77
102
  }
78
103
  controls.add(attrString(element, 'uixId'), upgrader(element));
@@ -1 +1 @@
1
- {"version":3,"file":"upgrade.js","sourceRoot":"","sources":["../../src/controls/upgrade.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Panel upgrade - scans a loaded panel for `data-uix` markers and wires the\n * matching control behaviour. App code retrieves the resulting handles via\n * `panelControlsFor(document)` and the control's `data-uix-id`.\n */\nimport { UixElement, attrString, walk } from './element.js';\nimport { StepperHandle, upgradeStepper } from './stepper.js';\nimport { ToggleHandle, upgradeToggle } from './toggle.js';\nimport { ExpandableLabelHandle, upgradeExpandableLabel } from './expandable-label.js';\nimport { LogViewHandle, upgradeLogView } from './log-view.js';\n\nexport type ControlHandle =\n | StepperHandle\n | ToggleHandle\n | ExpandableLabelHandle\n | LogViewHandle;\n\nexport class PanelControls {\n private handles = new Map<string, ControlHandle>();\n private anonymous = 0;\n\n add(id: string | undefined, handle: ControlHandle): void {\n const key = id ?? `uix-control-${this.anonymous++}`;\n if (this.handles.has(key)) {\n throw new Error(`[uix] duplicate control id \"${key}\" in panel`);\n }\n this.handles.set(key, handle);\n }\n\n /** Typed accessors - throw on wrong id so mistakes surface immediately. */\n stepper(id: string): StepperHandle {\n return this.expect(id, StepperHandle);\n }\n\n toggle(id: string): ToggleHandle {\n return this.expect(id, ToggleHandle);\n }\n\n expandableLabel(id: string): ExpandableLabelHandle {\n return this.expect(id, ExpandableLabelHandle);\n }\n\n logView(id: string): LogViewHandle {\n return this.expect(id, LogViewHandle);\n }\n\n get(id: string): ControlHandle | undefined {\n return this.handles.get(id);\n }\n\n ids(): string[] {\n return [...this.handles.keys()];\n }\n\n private expect<T extends ControlHandle>(\n id: string,\n kind: abstract new (...args: never[]) => T,\n ): T {\n const handle = this.handles.get(id);\n if (!handle) {\n throw new Error(`[uix] no control \"${id}\" in panel (have: ${this.ids().join(', ') || 'none'})`);\n }\n if (!(handle instanceof kind)) {\n throw new Error(`[uix] control \"${id}\" is not a ${kind.name}`);\n }\n return handle;\n }\n}\n\nconst UPGRADERS: Record<string, (root: UixElement) => ControlHandle> = {\n stepper: upgradeStepper,\n toggle: upgradeToggle,\n 'expandable-label': upgradeExpandableLabel,\n 'log-view': upgradeLogView,\n};\n\nconst registry = new WeakMap<object, PanelControls>();\n\n/**\n * Upgrade every `data-uix` control under `root` and remember the result\n * against `documentKey` (the panel's `UIKitDocument`). Idempotent per key.\n */\nexport function upgradePanel(documentKey: object, root: UixElement): PanelControls {\n const existing = registry.get(documentKey);\n if (existing) {\n return existing;\n }\n const controls = new PanelControls();\n walk(root, (element) => {\n const marker = attrString(element, 'uix');\n if (marker === undefined) {\n return;\n }\n const upgrader = UPGRADERS[marker];\n if (!upgrader) {\n console.warn(`[uix] unknown control type data-uix=\"${marker}\" - skipped`);\n return;\n }\n controls.add(attrString(element, 'uixId'), upgrader(element));\n });\n registry.set(documentKey, controls);\n return controls;\n}\n\n/** Controls of an upgraded panel; undefined before `UIControlsSystem` ran. */\nexport function panelControlsFor(documentKey: object): PanelControls | undefined {\n return registry.get(documentKey);\n}\n"]}
1
+ {"version":3,"file":"upgrade.js","sourceRoot":"","sources":["../../src/controls/upgrade.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Panel upgrade - scans a loaded panel for `<uix-*>` control elements and\n * wires the matching behaviour. App code retrieves the resulting handles via\n * `panelControlsFor(document)` and the control's `data-uix-id`.\n */\nimport { UixElement, attrString, tagOf, walk } from './element.js';\nimport { StepperHandle, upgradeStepper } from './stepper.js';\nimport { ToggleHandle, upgradeToggle } from './toggle.js';\nimport { ExpandableLabelHandle, upgradeExpandableLabel } from './expandable-label.js';\nimport { LogViewHandle, upgradeLogView } from './log-view.js';\n\nexport type ControlHandle =\n | StepperHandle\n | ToggleHandle\n | ExpandableLabelHandle\n | LogViewHandle;\n\nexport class PanelControls {\n private handles = new Map<string, ControlHandle>();\n private anonymous = 0;\n\n add(id: string | undefined, handle: ControlHandle): void {\n const key = id ?? `uix-control-${this.anonymous++}`;\n if (this.handles.has(key)) {\n throw new Error(`[uix] duplicate control id \"${key}\" in panel`);\n }\n this.handles.set(key, handle);\n }\n\n /** Typed accessors - throw on wrong id so mistakes surface immediately. */\n stepper(id: string): StepperHandle {\n return this.expect(id, StepperHandle);\n }\n\n toggle(id: string): ToggleHandle {\n return this.expect(id, ToggleHandle);\n }\n\n expandableLabel(id: string): ExpandableLabelHandle {\n return this.expect(id, ExpandableLabelHandle);\n }\n\n logView(id: string): LogViewHandle {\n return this.expect(id, LogViewHandle);\n }\n\n get(id: string): ControlHandle | undefined {\n return this.handles.get(id);\n }\n\n ids(): string[] {\n return [...this.handles.keys()];\n }\n\n private expect<T extends ControlHandle>(\n id: string,\n kind: abstract new (...args: never[]) => T,\n ): T {\n const handle = this.handles.get(id);\n if (!handle) {\n throw new Error(`[uix] no control \"${id}\" in panel (have: ${this.ids().join(', ') || 'none'})`);\n }\n if (!(handle instanceof kind)) {\n throw new Error(`[uix] control \"${id}\" is not a ${kind.name}`);\n }\n return handle;\n }\n}\n\n/**\n * Control tags. These are the custom elements an author writes; every part\n * inside them (`<uix-value>`, `<uix-line>`, …) is a role, resolved by the\n * individual upgraders through `findRole`.\n */\nconst UPGRADERS: Record<string, (root: UixElement) => ControlHandle> = {\n 'uix-stepper': upgradeStepper,\n 'uix-toggle': upgradeToggle,\n 'uix-expandable-label': upgradeExpandableLabel,\n 'uix-log-view': upgradeLogView,\n};\n\n/**\n * Parts claimed by the control that owns them, resolved through `findRole`.\n * Listed here only so a genuine typo in a control tag still gets a warning\n * instead of being silently mistaken for a role.\n */\nconst ROLE_TAGS = new Set([\n 'uix-decrement',\n 'uix-value',\n 'uix-increment',\n 'uix-label',\n 'uix-line',\n 'uix-up',\n 'uix-down',\n 'uix-clear',\n 'uix-status',\n 'uix-text',\n 'uix-more',\n]);\n\nconst registry = new WeakMap<object, PanelControls>();\n\n/**\n * Upgrade every `data-uix` control under `root` and remember the result\n * against `documentKey` (the panel's `UIKitDocument`). Idempotent per key.\n */\nexport function upgradePanel(documentKey: object, root: UixElement): PanelControls {\n const existing = registry.get(documentKey);\n if (existing) {\n return existing;\n }\n const controls = new PanelControls();\n walk(root, (element) => {\n const tag = tagOf(element);\n if (tag === undefined || !tag.startsWith('uix-')) {\n return;\n }\n const upgrader = UPGRADERS[tag];\n if (!upgrader) {\n if (!ROLE_TAGS.has(tag)) {\n console.warn(`[uix] unknown control element <${tag}> - skipped`);\n }\n return;\n }\n controls.add(attrString(element, 'uixId'), upgrader(element));\n });\n registry.set(documentKey, controls);\n return controls;\n}\n\n/** Controls of an upgraded panel; undefined before `UIControlsSystem` ran. */\nexport function panelControlsFor(documentKey: object): PanelControls | undefined {\n return registry.get(documentKey);\n}\n"]}
package/dist/scene.d.ts CHANGED
@@ -9,9 +9,11 @@
9
9
  * applyScene(iwsdkTarget, PLAYGROUND); // ECS entities
10
10
  * applyScene(vanillaHost, PLAYGROUND); // three.js groups
11
11
  *
12
- * Panel config paths stay strings (`./ui/foo.json`) because every adapter
13
- * resolves them the same way - IWSDK fetches them itself, the vanilla host
14
- * is handed the parsed JSON by `applyScene`'s loader.
12
+ * Panel config paths stay opaque strings (`./ui/foo.uikitml`) because each
13
+ * adapter resolves them its own way. IWSDK 0.5 fetches the source and parses
14
+ * it inside PanelUI; the vanilla and XR Blocks hosts parse it through the
15
+ * `loadConfig` resolver and are handed the resulting AST. The descriptor makes
16
+ * no claim about the artefact behind the path.
15
17
  */
16
18
  import type { DockModeValue } from './core/dock-state.js';
17
19
  import type { RegionFlow, Vec3 } from './core/region-layout.js';
@@ -19,7 +21,7 @@ import type { RegionFlow, Vec3 } from './core/region-layout.js';
19
21
  export interface SceneWindow {
20
22
  id: string;
21
23
  title: string;
22
- /** Path to the compiled UIKitML JSON, e.g. `./ui/clicker.json`. */
24
+ /** Path to the UIKitML source, e.g. `./ui/clicker.uikitml`. */
23
25
  config: string;
24
26
  /** World position for world-locked windows. */
25
27
  position?: Vec3;
package/dist/scene.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"scene.js","sourceRoot":"","sources":["../src/scene.ts"],"names":[],"mappings":"AAuEA;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,MAAmB,EAAE,KAAsB;IACpE,KAAK,MAAM,MAAM,IAAI,KAAK,CAAC,OAAO,IAAI,EAAE,EAAE,CAAC;QACzC,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;IAC7B,CAAC;IACD,KAAK,MAAM,MAAM,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;QACnC,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;IAC7B,CAAC;AACH,CAAC;AAED,oEAAoE;AACpE,MAAM,UAAU,gBAAgB,CAAC,KAAsB;IACrD,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;AACpE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,KAAsB;IAClD,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,MAAM,SAAS,GAAG,IAAI,GAAG,EAAU,CAAC;IACpC,KAAK,MAAM,MAAM,IAAI,KAAK,CAAC,OAAO,IAAI,EAAE,EAAE,CAAC;QACzC,IAAI,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC;YAC7B,QAAQ,CAAC,IAAI,CAAC,wBAAwB,MAAM,CAAC,EAAE,GAAG,CAAC,CAAC;QACtD,CAAC;QACD,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAC3B,CAAC;IACD,MAAM,SAAS,GAAG,IAAI,GAAG,EAAU,CAAC;IACpC,KAAK,MAAM,MAAM,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;QACnC,IAAI,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC;YAC7B,QAAQ,CAAC,IAAI,CAAC,wBAAwB,MAAM,CAAC,EAAE,GAAG,CAAC,CAAC;QACtD,CAAC;QACD,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QACzB,IAAI,MAAM,CAAC,MAAM,KAAK,SAAS,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;YACjE,QAAQ,CAAC,IAAI,CACX,WAAW,MAAM,CAAC,EAAE,gCAAgC,MAAM,CAAC,MAAM,GAAG,CACrE,CAAC;QACJ,CAAC;IACH,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC","sourcesContent":["/**\n * Portable scene descriptors.\n *\n * A scene is plain DATA - which windows exist, where they sit, which dock\n * regions they can drop into - with no engine types anywhere. Each adapter\n * knows how to apply the same descriptor through its own factories, so one\n * playground definition drives IWSDK, XR Blocks and plain three.js alike:\n *\n * applyScene(iwsdkTarget, PLAYGROUND); // ECS entities\n * applyScene(vanillaHost, PLAYGROUND); // three.js groups\n *\n * Panel config paths stay strings (`./ui/foo.json`) because every adapter\n * resolves them the same way - IWSDK fetches them itself, the vanilla host\n * is handed the parsed JSON by `applyScene`'s loader.\n */\nimport type { DockModeValue } from './core/dock-state.js';\nimport type { RegionFlow, Vec3 } from './core/region-layout.js';\n\n/** One window in a scene. */\nexport interface SceneWindow {\n id: string;\n title: string;\n /** Path to the compiled UIKitML JSON, e.g. `./ui/clicker.json`. */\n config: string;\n /** World position for world-locked windows. */\n position?: Vec3;\n maxWidth?: number;\n maxHeight?: number;\n dockMode?: DockModeValue;\n /** Dock straight into this region on spawn. */\n region?: string;\n followOffset?: Vec3;\n followSpeed?: number;\n followTolerance?: number;\n movable?: boolean;\n closable?: boolean;\n minimizable?: boolean;\n pinnable?: boolean;\n}\n\n/** One dock region in a scene. */\nexport interface SceneRegion {\n id: string;\n flow?: RegionFlow;\n pitch?: number;\n columns?: number;\n capacity?: number;\n snapRadius?: number;\n position?: Vec3;\n /** Body-lock the region so it follows the viewer. */\n follow?: boolean;\n followOffset?: Vec3;\n}\n\n/** A complete, engine-free scene definition. */\nexport interface SceneDescriptor {\n name?: string;\n regions?: readonly SceneRegion[];\n windows: readonly SceneWindow[];\n}\n\n/**\n * What an adapter must provide for {@link applyScene} to build a scene.\n * Both shipped adapters implement this; a new adapter only needs these two\n * methods to gain full scene portability.\n */\nexport interface SceneTarget {\n spawnRegion(region: SceneRegion): void;\n spawnWindow(window: SceneWindow): void;\n}\n\n/**\n * Apply a descriptor to an adapter. Regions are created before windows so a\n * window that spawns docked (`region: 'x'`) always finds its region.\n */\nexport function applyScene(target: SceneTarget, scene: SceneDescriptor): void {\n for (const region of scene.regions ?? []) {\n target.spawnRegion(region);\n }\n for (const window of scene.windows) {\n target.spawnWindow(window);\n }\n}\n\n/** Every distinct panel config path in a scene (for preloading). */\nexport function sceneConfigPaths(scene: SceneDescriptor): string[] {\n return [...new Set(scene.windows.map((window) => window.config))];\n}\n\n/**\n * Validate a descriptor: unique window/region ids, and every `region`\n * reference resolvable. Returns the problems found (empty = valid) rather\n * than throwing, so callers can report them all at once.\n */\nexport function validateScene(scene: SceneDescriptor): string[] {\n const problems: string[] = [];\n const regionIds = new Set<string>();\n for (const region of scene.regions ?? []) {\n if (regionIds.has(region.id)) {\n problems.push(`duplicate region id \"${region.id}\"`);\n }\n regionIds.add(region.id);\n }\n const windowIds = new Set<string>();\n for (const window of scene.windows) {\n if (windowIds.has(window.id)) {\n problems.push(`duplicate window id \"${window.id}\"`);\n }\n windowIds.add(window.id);\n if (window.region !== undefined && !regionIds.has(window.region)) {\n problems.push(\n `window \"${window.id}\" docks into unknown region \"${window.region}\"`,\n );\n }\n }\n return problems;\n}\n"]}
1
+ {"version":3,"file":"scene.js","sourceRoot":"","sources":["../src/scene.ts"],"names":[],"mappings":"AAyEA;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,MAAmB,EAAE,KAAsB;IACpE,KAAK,MAAM,MAAM,IAAI,KAAK,CAAC,OAAO,IAAI,EAAE,EAAE,CAAC;QACzC,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;IAC7B,CAAC;IACD,KAAK,MAAM,MAAM,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;QACnC,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;IAC7B,CAAC;AACH,CAAC;AAED,oEAAoE;AACpE,MAAM,UAAU,gBAAgB,CAAC,KAAsB;IACrD,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;AACpE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,KAAsB;IAClD,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,MAAM,SAAS,GAAG,IAAI,GAAG,EAAU,CAAC;IACpC,KAAK,MAAM,MAAM,IAAI,KAAK,CAAC,OAAO,IAAI,EAAE,EAAE,CAAC;QACzC,IAAI,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC;YAC7B,QAAQ,CAAC,IAAI,CAAC,wBAAwB,MAAM,CAAC,EAAE,GAAG,CAAC,CAAC;QACtD,CAAC;QACD,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAC3B,CAAC;IACD,MAAM,SAAS,GAAG,IAAI,GAAG,EAAU,CAAC;IACpC,KAAK,MAAM,MAAM,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;QACnC,IAAI,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC;YAC7B,QAAQ,CAAC,IAAI,CAAC,wBAAwB,MAAM,CAAC,EAAE,GAAG,CAAC,CAAC;QACtD,CAAC;QACD,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QACzB,IAAI,MAAM,CAAC,MAAM,KAAK,SAAS,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;YACjE,QAAQ,CAAC,IAAI,CACX,WAAW,MAAM,CAAC,EAAE,gCAAgC,MAAM,CAAC,MAAM,GAAG,CACrE,CAAC;QACJ,CAAC;IACH,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC","sourcesContent":["/**\n * Portable scene descriptors.\n *\n * A scene is plain DATA - which windows exist, where they sit, which dock\n * regions they can drop into - with no engine types anywhere. Each adapter\n * knows how to apply the same descriptor through its own factories, so one\n * playground definition drives IWSDK, XR Blocks and plain three.js alike:\n *\n * applyScene(iwsdkTarget, PLAYGROUND); // ECS entities\n * applyScene(vanillaHost, PLAYGROUND); // three.js groups\n *\n * Panel config paths stay opaque strings (`./ui/foo.uikitml`) because each\n * adapter resolves them its own way. IWSDK 0.5 fetches the source and parses\n * it inside PanelUI; the vanilla and XR Blocks hosts parse it through the\n * `loadConfig` resolver and are handed the resulting AST. The descriptor makes\n * no claim about the artefact behind the path.\n */\nimport type { DockModeValue } from './core/dock-state.js';\nimport type { RegionFlow, Vec3 } from './core/region-layout.js';\n\n/** One window in a scene. */\nexport interface SceneWindow {\n id: string;\n title: string;\n /** Path to the UIKitML source, e.g. `./ui/clicker.uikitml`. */\n config: string;\n /** World position for world-locked windows. */\n position?: Vec3;\n maxWidth?: number;\n maxHeight?: number;\n dockMode?: DockModeValue;\n /** Dock straight into this region on spawn. */\n region?: string;\n followOffset?: Vec3;\n followSpeed?: number;\n followTolerance?: number;\n movable?: boolean;\n closable?: boolean;\n minimizable?: boolean;\n pinnable?: boolean;\n}\n\n/** One dock region in a scene. */\nexport interface SceneRegion {\n id: string;\n flow?: RegionFlow;\n pitch?: number;\n columns?: number;\n capacity?: number;\n snapRadius?: number;\n position?: Vec3;\n /** Body-lock the region so it follows the viewer. */\n follow?: boolean;\n followOffset?: Vec3;\n}\n\n/** A complete, engine-free scene definition. */\nexport interface SceneDescriptor {\n name?: string;\n regions?: readonly SceneRegion[];\n windows: readonly SceneWindow[];\n}\n\n/**\n * What an adapter must provide for {@link applyScene} to build a scene.\n * Both shipped adapters implement this; a new adapter only needs these two\n * methods to gain full scene portability.\n */\nexport interface SceneTarget {\n spawnRegion(region: SceneRegion): void;\n spawnWindow(window: SceneWindow): void;\n}\n\n/**\n * Apply a descriptor to an adapter. Regions are created before windows so a\n * window that spawns docked (`region: 'x'`) always finds its region.\n */\nexport function applyScene(target: SceneTarget, scene: SceneDescriptor): void {\n for (const region of scene.regions ?? []) {\n target.spawnRegion(region);\n }\n for (const window of scene.windows) {\n target.spawnWindow(window);\n }\n}\n\n/** Every distinct panel config path in a scene (for preloading). */\nexport function sceneConfigPaths(scene: SceneDescriptor): string[] {\n return [...new Set(scene.windows.map((window) => window.config))];\n}\n\n/**\n * Validate a descriptor: unique window/region ids, and every `region`\n * reference resolvable. Returns the problems found (empty = valid) rather\n * than throwing, so callers can report them all at once.\n */\nexport function validateScene(scene: SceneDescriptor): string[] {\n const problems: string[] = [];\n const regionIds = new Set<string>();\n for (const region of scene.regions ?? []) {\n if (regionIds.has(region.id)) {\n problems.push(`duplicate region id \"${region.id}\"`);\n }\n regionIds.add(region.id);\n }\n const windowIds = new Set<string>();\n for (const window of scene.windows) {\n if (windowIds.has(window.id)) {\n problems.push(`duplicate window id \"${window.id}\"`);\n }\n windowIds.add(window.id);\n if (window.region !== undefined && !regionIds.has(window.region)) {\n problems.push(\n `window \"${window.id}\" docks into unknown region \"${window.region}\"`,\n );\n }\n }\n return problems;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@realitycollective/webxr-uiextensions",
3
- "version": "0.1.0-preview.0",
3
+ "version": "0.1.0-preview.2",
4
4
  "description": "Engine-free core of the Reality Collective UI Extensions: windowing, docking, layout regions and control models for WebXR spatial UI, driven through platform-adapter interfaces. Pair with an engine adapter - @realitycollective/iwsdk-uiextensions (Meta IWSDK) or @realitycollective/xrblocks-uiextensions (Google XR Blocks).",
5
5
  "keywords": [
6
6
  "realitycollective",