@realitycollective/iwsdk-uiextensions 0.1.0-preview.4
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 +40 -0
- package/Examples/README.md +13 -0
- package/Examples/basic-window/main.ts +32 -0
- package/Examples/basic-window/window.uikitml +58 -0
- package/Examples/controls/controls.uikitml +49 -0
- package/Examples/controls/main.ts +48 -0
- package/Examples/dock-regions/main.ts +55 -0
- package/LICENSE +21 -0
- package/README.md +155 -0
- package/dist/component-set.d.ts +28 -0
- package/dist/component-set.js +108 -0
- package/dist/component-set.js.map +1 -0
- package/dist/components.d.ts +189 -0
- package/dist/components.js +97 -0
- package/dist/components.js.map +1 -0
- package/dist/factory.d.ts +44 -0
- package/dist/factory.js +63 -0
- package/dist/factory.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/manager-registry.d.ts +8 -0
- package/dist/manager-registry.js +11 -0
- package/dist/manager-registry.js.map +1 -0
- package/dist/region-registry-store.d.ts +7 -0
- package/dist/region-registry-store.js +11 -0
- package/dist/region-registry-store.js.map +1 -0
- package/dist/register.d.ts +19 -0
- package/dist/register.js +20 -0
- package/dist/register.js.map +1 -0
- package/dist/scene-host.d.ts +44 -0
- package/dist/scene-host.js +190 -0
- package/dist/scene-host.js.map +1 -0
- package/dist/systems/controls-system.d.ts +24 -0
- package/dist/systems/controls-system.js +24 -0
- package/dist/systems/controls-system.js.map +1 -0
- package/dist/systems/dock-region-system.d.ts +230 -0
- package/dist/systems/dock-region-system.js +102 -0
- package/dist/systems/dock-region-system.js.map +1 -0
- package/dist/systems/dock-system.d.ts +205 -0
- package/dist/systems/dock-system.js +135 -0
- package/dist/systems/dock-system.js.map +1 -0
- package/dist/systems/drag-system.d.ts +280 -0
- package/dist/systems/drag-system.js +167 -0
- package/dist/systems/drag-system.js.map +1 -0
- package/dist/systems/window-system.d.ts +439 -0
- package/dist/systems/window-system.js +244 -0
- package/dist/systems/window-system.js.map +1 -0
- package/package.json +67 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Change log for the Reality Collective WebXR UI Extensions packages. All four packages are versioned and released together; the version below is the one carried by the `v<version>` release tag.
|
|
4
|
+
|
|
5
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Preview builds are not listed separately. The entry for a version accumulates while its previews are published, and is dated when that version is released.
|
|
6
|
+
|
|
7
|
+
## [0.1.0]
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `@realitycollective/webxr-uiextensions` - engine-free core: window manager, dock state and regions, drag maths, hold-to-drag, control models (stepper/toggle/expandable/log), the `SceneDescriptor` scene format, window chrome conventions and the platform-adapter contract.
|
|
12
|
+
- `@realitycollective/iwsdk-uiextensions` - Meta IWSDK adapter binding the core onto IWSDK's ECS, UIKitML and interaction systems, with shipped examples.
|
|
13
|
+
- `@realitycollective/xrblocks-uiextensions` - EXPERIMENTAL Google XR Blocks / plain three.js adapter: panel document, window host, follow and scale maths, desktop controls and locomotion, pointer forwarding.
|
|
14
|
+
- `@realitycollective/uix-devtools` - dev-only tooling: edit-session launch gate, runtime UIKitML compilation, and the `uix-dev` CLI (Cloudflare quick tunnel, QR onboarding, environment doctor).
|
|
15
|
+
- Demo clients: the IWSDK showcase, the devtools playground, and the multiplatform lab that picks its pipeline from the hardware.
|
|
16
|
+
- `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
|
+
- `windowHostContractCases()` in `@realitycollective/webxr-uiextensions`, with `WindowHostContractCase` and `WindowHostContractSetup` - the `WindowHost` conformance suite as data rather than as tests. Each case is a `name` plus a `run(setup)` that throws an `Error` on failure, so an adapter runs the suite in its own test runner and needs nothing from this repository's `test/` folder. It was previously an in-repo vitest helper that only the two shipped adapters could reach, which left an adapter author outside this repository with no way to prove conformance. Both adapters now run the shipped cases through a three-line wrapper.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- **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.
|
|
27
|
+
- The expandable label's `toggle` part is now `<uix-more>`. As an element the old name would have collided with the `<uix-toggle>` control.
|
|
28
|
+
- `@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`.
|
|
29
|
+
- `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.
|
|
30
|
+
- `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.
|
|
31
|
+
- 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.
|
|
32
|
+
- 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.
|
|
33
|
+
- `@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.
|
|
34
|
+
- `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`.
|
|
35
|
+
- `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.
|
|
36
|
+
- 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.
|
|
37
|
+
- 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.
|
|
38
|
+
- `@realitycollective/webxr-uiextensions` takes its geometry vocabulary from `@realitycollective/webxr-input` at `^0.1.1` rather than redeclaring it. `Vec3Tuple`, `QuatTuple`, `HeadPose`, `HeadPoseSource` and `PointerSample` are now that package's types, re-exported under the same names, so no import changes for a consumer. `PointerSample` is its `RayTuple`, which is what lets an input provider written against the shared contracts feed this contract unchanged. It is the core's only runtime dependency: the contracts package is engine-free and carries none of its own, and `test/architecture.test.ts` now allows exactly that one name and fails on any other.
|
|
39
|
+
|
|
40
|
+
[0.1.0]: https://github.com/realitycollective/WebXR-UIExtensions/commits/main
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Examples
|
|
2
|
+
|
|
3
|
+
Copy-paste starting points, shipped inside the npm package (Unity-style `Examples/` convention, as used across Reality Collective packages).
|
|
4
|
+
|
|
5
|
+
| Example | Shows |
|
|
6
|
+
| --- | --- |
|
|
7
|
+
| [`basic-window/`](./basic-window) | A managed window with chrome, pin/minimize/close and body-follow |
|
|
8
|
+
| [`dock-regions/`](./dock-regions) | Layout regions, drop-to-dock, a follow-region "toolbar" |
|
|
9
|
+
| [`controls/`](./controls) | `data-uix` stepper, toggle, expandable label and log view |
|
|
10
|
+
|
|
11
|
+
Each example assumes an IWSDK app created with `npm create @iwsdk@latest` (so the UIKitML Vite plugin is already wired: `.uikitml` files in `ui/` compile to `public/ui/*.json`).
|
|
12
|
+
|
|
13
|
+
For a complete integrated application - five windows, two regions, a registration form, event log, and horizon-kit Slider - see the `showcase/` client in the repository, which deploys to Cloudflare Pages from CI.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Basic window - chrome, focus, docking and drag with one call.
|
|
3
|
+
* Put window.uikitml in your app's public/ui/ folder - IWSDK 0.5 fetches and
|
|
4
|
+
* parses the source at runtime, so there is no compile step.
|
|
5
|
+
*/
|
|
6
|
+
import { World } from '@iwsdk/core';
|
|
7
|
+
import {
|
|
8
|
+
DockMode,
|
|
9
|
+
createUIWindow,
|
|
10
|
+
registerUIExtensions,
|
|
11
|
+
} from '@realitycollective/iwsdk-uiextensions';
|
|
12
|
+
|
|
13
|
+
export async function start(container: HTMLDivElement) {
|
|
14
|
+
const world = await World.create(container, {
|
|
15
|
+
features: { spatialUI: true },
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const windows = registerUIExtensions(world);
|
|
19
|
+
|
|
20
|
+
createUIWindow(world, {
|
|
21
|
+
id: 'hello',
|
|
22
|
+
title: 'Hello Window',
|
|
23
|
+
config: './ui/window.uikitml',
|
|
24
|
+
dockMode: DockMode.BodyFollow, // follows until pinned or dragged
|
|
25
|
+
followOffset: [0, -0.15, -1.2],
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
windows.events.on('dockChanged', ({ window, previous }) => {
|
|
29
|
+
console.log(`[uix] ${window.title}: ${previous} → ${window.dockMode}`);
|
|
30
|
+
});
|
|
31
|
+
windows.events.on('closed', (w) => console.log(`[uix] ${w.title} closed`));
|
|
32
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<style>
|
|
2
|
+
.uix-window {
|
|
3
|
+
display: flex;
|
|
4
|
+
flex-direction: column;
|
|
5
|
+
width: 300;
|
|
6
|
+
background-color: rgba(16, 26, 38, 0.92);
|
|
7
|
+
border-radius: 12.5;
|
|
8
|
+
border-color: #2e4a66;
|
|
9
|
+
border-width: 0.75;
|
|
10
|
+
}
|
|
11
|
+
.uix-titlebar {
|
|
12
|
+
display: flex;
|
|
13
|
+
flex-direction: row;
|
|
14
|
+
align-items: center;
|
|
15
|
+
justify-content: space-between;
|
|
16
|
+
width: 100%;
|
|
17
|
+
padding: 8;
|
|
18
|
+
background-color: #1b2c40;
|
|
19
|
+
border-top-left-radius: 12.5;
|
|
20
|
+
border-top-right-radius: 12.5;
|
|
21
|
+
cursor: pointer;
|
|
22
|
+
}
|
|
23
|
+
.uix-title { flex-shrink: 1; font-size: 12; font-weight: bold; color: #dce9f7; }
|
|
24
|
+
.uix-titlebar-buttons { display: flex; flex-direction: row; flex-shrink: 0; }
|
|
25
|
+
.uix-titlebar-button {
|
|
26
|
+
width: 34;
|
|
27
|
+
height: 20;
|
|
28
|
+
align-items: center;
|
|
29
|
+
justify-content: center;
|
|
30
|
+
text-align: center;
|
|
31
|
+
margin-left: 4;
|
|
32
|
+
font-size: 9;
|
|
33
|
+
color: #9fb8d4;
|
|
34
|
+
background-color: #223850;
|
|
35
|
+
border-radius: 6;
|
|
36
|
+
cursor: pointer;
|
|
37
|
+
}
|
|
38
|
+
.uix-titlebar-button:hover { background-color: #33547a; color: #ffffff; }
|
|
39
|
+
.uix-content { display: flex; flex-direction: column; width: 100%; padding: 10; }
|
|
40
|
+
.body-text { font-size: 11; color: #c4d6ea; }
|
|
41
|
+
</style>
|
|
42
|
+
<div id="uix-window" class="uix-window">
|
|
43
|
+
<div id="uix-titlebar" class="uix-titlebar">
|
|
44
|
+
<span id="uix-title" class="uix-title">.</span>
|
|
45
|
+
<div class="uix-titlebar-buttons">
|
|
46
|
+
<div id="uix-pin" class="uix-titlebar-button">PIN</div>
|
|
47
|
+
<div id="uix-dock" class="uix-titlebar-button">DOCK</div>
|
|
48
|
+
<div id="uix-minimize" class="uix-titlebar-button">MIN</div>
|
|
49
|
+
<div id="uix-close" class="uix-titlebar-button">X</div>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
<div id="uix-content" class="uix-content">
|
|
53
|
+
<span class="body-text">
|
|
54
|
+
Drag my title bar to move me. PIN toggles between following you and
|
|
55
|
+
staying put. MIN collapses me to the title bar.
|
|
56
|
+
</span>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<style>
|
|
2
|
+
.panel {
|
|
3
|
+
display: flex;
|
|
4
|
+
flex-direction: column;
|
|
5
|
+
width: 300;
|
|
6
|
+
padding: 10;
|
|
7
|
+
background-color: rgba(16, 26, 38, 0.92);
|
|
8
|
+
border-radius: 12.5;
|
|
9
|
+
}
|
|
10
|
+
.row { display: flex; flex-direction: row; align-items: center; justify-content: space-between; width: 100%; margin-top: 7; }
|
|
11
|
+
.label { font-size: 11; color: #9fb8d4; }
|
|
12
|
+
.stepper { display: flex; flex-direction: row; align-items: center; }
|
|
13
|
+
.stepper-button { padding: 4; font-size: 13; color: #eaf6ff; background-color: #2c4a6b; border-radius: 6; cursor: pointer; }
|
|
14
|
+
.stepper-value { font-size: 13; color: #ffffff; margin-left: 7; margin-right: 7; }
|
|
15
|
+
.toggle { padding: 5.5; border-radius: 7; cursor: pointer; }
|
|
16
|
+
.toggle-label { font-size: 11; font-weight: bold; color: #ffffff; }
|
|
17
|
+
.long { display: flex; flex-direction: column; width: 100%; margin-top: 8; }
|
|
18
|
+
.long-text { font-size: 9.5; color: #c4d6ea; }
|
|
19
|
+
.long-toggle { margin-top: 3; font-size: 9.5; color: #6fb4ff; cursor: pointer; }
|
|
20
|
+
.log { display: flex; flex-direction: column; width: 100%; margin-top: 8; }
|
|
21
|
+
.log-line { font-size: 9; color: #9fe0a8; margin-top: 2; }
|
|
22
|
+
</style>
|
|
23
|
+
<div class="panel">
|
|
24
|
+
<div class="row">
|
|
25
|
+
<span class="label">Volume</span>
|
|
26
|
+
<div data-uix="stepper" data-uix-id="volume" data-uix-min="0" data-uix-max="10" class="stepper">
|
|
27
|
+
<div data-uix-role="decrement" class="stepper-button">-</div>
|
|
28
|
+
<span data-uix-role="value" class="stepper-value">.</span>
|
|
29
|
+
<div data-uix-role="increment" class="stepper-button">+</div>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
<div class="row">
|
|
33
|
+
<span class="label">Music</span>
|
|
34
|
+
<div data-uix="toggle" data-uix-id="music" data-uix-on-color="#1f6d3d" data-uix-off-color="#5a3037" class="toggle">
|
|
35
|
+
<span data-uix-role="label" class="toggle-label">.</span>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
<div data-uix="expandable-label" data-uix-id="details" data-uix-lines="2" data-uix-chars-per-line="40"
|
|
39
|
+
data-uix-text="Long descriptive text collapses to two lines with an ellipsis, and the more/less affordance expands it in place - no scrolling, no clipping."
|
|
40
|
+
class="long">
|
|
41
|
+
<span data-uix-role="text" class="long-text">.</span>
|
|
42
|
+
<div data-uix-role="toggle" class="long-toggle">more</div>
|
|
43
|
+
</div>
|
|
44
|
+
<div data-uix="log-view" data-uix-id="log" class="log">
|
|
45
|
+
<span data-uix-role="line" class="log-line">.</span>
|
|
46
|
+
<span data-uix-role="line" class="log-line">.</span>
|
|
47
|
+
<span data-uix-role="line" class="log-line">.</span>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Controls - data-uix markup upgrades on any IWSDK panel.
|
|
3
|
+
*
|
|
4
|
+
* The controls work in plain panels too (no window required): the
|
|
5
|
+
* UIControlsSystem upgrades every loaded PanelDocument.
|
|
6
|
+
*/
|
|
7
|
+
import { PanelDocument, PanelUI, UIKitDocument, World, createSystem, eq } from '@iwsdk/core';
|
|
8
|
+
import {
|
|
9
|
+
registerUIExtensions,
|
|
10
|
+
upgradePanel,
|
|
11
|
+
type UixElement,
|
|
12
|
+
} from '@realitycollective/iwsdk-uiextensions';
|
|
13
|
+
|
|
14
|
+
export async function start(container: HTMLDivElement) {
|
|
15
|
+
const world = await World.create(container, {
|
|
16
|
+
features: { spatialUI: true },
|
|
17
|
+
});
|
|
18
|
+
registerUIExtensions(world);
|
|
19
|
+
|
|
20
|
+
// `config` is the only field PanelUI carries in IWSDK 0.5. maxWidth/maxHeight
|
|
21
|
+
// were removed from the schema; ECS ignores unknown fields silently, so
|
|
22
|
+
// writing them here would mis-size the panel with no error. Size a panel
|
|
23
|
+
// through createUIWindow instead, which routes to UIWindow.targetWidth/Height.
|
|
24
|
+
const panel = world
|
|
25
|
+
.createTransformEntity()
|
|
26
|
+
.addComponent(PanelUI, { config: './ui/controls.uikitml' });
|
|
27
|
+
panel.object3D!.position.set(0, 1.5, -1.2);
|
|
28
|
+
|
|
29
|
+
// Wire the handles once the panel loads.
|
|
30
|
+
class WireSystem extends createSystem({
|
|
31
|
+
panel: {
|
|
32
|
+
required: [PanelUI, PanelDocument],
|
|
33
|
+
where: [eq(PanelUI, 'config', './ui/controls.uikitml')],
|
|
34
|
+
},
|
|
35
|
+
}) {
|
|
36
|
+
override init(): void {
|
|
37
|
+
this.queries.panel.subscribe('qualify', (entity) => {
|
|
38
|
+
const document = PanelDocument.data.document[entity.index] as UIKitDocument;
|
|
39
|
+
// upgradePanel is idempotent - safe regardless of system order.
|
|
40
|
+
const controls = upgradePanel(document, document.rootElement as unknown as UixElement);
|
|
41
|
+
const log = controls.logView('log');
|
|
42
|
+
controls.stepper('volume').events.on('change', (v) => log.push(`volume ${v}`));
|
|
43
|
+
controls.toggle('music').events.on('change', (on) => log.push(`music ${on ? 'on' : 'off'}`));
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
world.registerSystem(WireSystem);
|
|
48
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dock regions - snap windows into named layout regions.
|
|
3
|
+
*
|
|
4
|
+
* A region is an anchor entity with slot layout (row / column / grid).
|
|
5
|
+
* Drop a window inside its snap radius to dock it; drag it out to undock.
|
|
6
|
+
* Regions can follow the player, turning their contents into a toolbar.
|
|
7
|
+
*/
|
|
8
|
+
import { World } from '@iwsdk/core';
|
|
9
|
+
import {
|
|
10
|
+
createDockRegion,
|
|
11
|
+
createUIWindow,
|
|
12
|
+
registerUIExtensions,
|
|
13
|
+
} from '@realitycollective/iwsdk-uiextensions';
|
|
14
|
+
|
|
15
|
+
export async function start(container: HTMLDivElement) {
|
|
16
|
+
const world = await World.create(container, {
|
|
17
|
+
features: { spatialUI: true },
|
|
18
|
+
});
|
|
19
|
+
registerUIExtensions(world);
|
|
20
|
+
|
|
21
|
+
// A world-locked column on the wall - drop windows to stack them.
|
|
22
|
+
createDockRegion(world, {
|
|
23
|
+
id: 'wall',
|
|
24
|
+
flow: 'column',
|
|
25
|
+
pitch: 0.6,
|
|
26
|
+
capacity: 3,
|
|
27
|
+
snapRadius: 0.65,
|
|
28
|
+
position: [1.6, 1.9, -1.5],
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// A row that follows the player - a floating toolbar.
|
|
32
|
+
createDockRegion(world, {
|
|
33
|
+
id: 'toolbar',
|
|
34
|
+
flow: 'row',
|
|
35
|
+
pitch: 0.55,
|
|
36
|
+
follow: true,
|
|
37
|
+
followOffset: [0, -0.7, -1.1],
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Spawn a window straight into a region…
|
|
41
|
+
createUIWindow(world, {
|
|
42
|
+
id: 'docked',
|
|
43
|
+
title: 'Docked Window',
|
|
44
|
+
config: './ui/window.uikitml',
|
|
45
|
+
region: 'wall',
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// …and one free-floating to drag into either region.
|
|
49
|
+
createUIWindow(world, {
|
|
50
|
+
id: 'floating',
|
|
51
|
+
title: 'Drag Me Into A Region',
|
|
52
|
+
config: './ui/window.uikitml',
|
|
53
|
+
position: [0, 1.5, -1.4],
|
|
54
|
+
});
|
|
55
|
+
}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Reality Collective
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# @realitycollective/iwsdk-uiextensions
|
|
2
|
+
|
|
3
|
+
Windowing, docking, layout regions and extra controls for [Meta's Immersive Web SDK](https://iwsdk.dev) (`@iwsdk/core`).
|
|
4
|
+
|
|
5
|
+
**This is the Meta IWSDK adapter** - and the reference implementation - for the engine-free [`@realitycollective/webxr-uiextensions`](../webxr-uiextensions/README.md) core, which it re-exports in full: one dependency gets IWSDK apps the whole surface. (A sibling [`@realitycollective/xrblocks-uiextensions`](../xrblocks-uiextensions/README.md) adapter binds the same core to Google XR Blocks, experimentally.)
|
|
6
|
+
|
|
7
|
+
**Reuse, not recreation.** The IWSDK already ships an excellent spatial UI stack - UIKitML markup, `@pmndrs/uikit` rendering, `Follower`/`ScreenSpace` anchoring, grab/ray/poke interaction. This package adds the missing layer above it:
|
|
8
|
+
|
|
9
|
+
| Feature | What you get |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| **Windows** | Title-bar chrome (pin / minimize / close), focus & z-ordering, a per-world `WindowManager` with typed events |
|
|
12
|
+
| **Dock states** | `world-locked` (place in space) ⇄ `body-follow` (lazy follow) ⇄ `head-locked`, realised with the IWSDK's own `Follower`/`ScreenSpace` |
|
|
13
|
+
| **Manipulation** | Drag windows by the title bar (powered by `@pmndrs/handle`, the same library behind IWSDK grabbing), billboard-while-dragging, drop-to-dock |
|
|
14
|
+
| **Layout regions** | Named regions (row / column / grid slots) windows snap into; regions can themselves follow the player |
|
|
15
|
+
| **Controls** | `data-uix` markup upgrades: **stepper**, **toggle**, **expandable multi-line label**, **log/list view** - plus everything UIKitML already has (buttons, inputs, textareas, images, and the horizon kit's Slider/Checkbox/…) |
|
|
16
|
+
|
|
17
|
+
Everything is authored in plain UIKitML (HTML/CSS-like) - no new markup language, no custom renderer, no wrapper widgets around things the IWSDK already does.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install @realitycollective/iwsdk-uiextensions
|
|
23
|
+
# peer: @iwsdk/core >=0.5.0 <0.6.0
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Quick start
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { World } from '@iwsdk/core';
|
|
30
|
+
import {
|
|
31
|
+
registerUIExtensions,
|
|
32
|
+
createUIWindow,
|
|
33
|
+
createDockRegion,
|
|
34
|
+
DockMode,
|
|
35
|
+
} from '@realitycollective/iwsdk-uiextensions';
|
|
36
|
+
|
|
37
|
+
const world = await World.create(container, {
|
|
38
|
+
features: { spatialUI: true },
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const windows = registerUIExtensions(world); // registers all systems, returns the WindowManager
|
|
42
|
+
|
|
43
|
+
createDockRegion(world, { id: 'wall', flow: 'column', position: [1.5, 1.8, -1.5] });
|
|
44
|
+
|
|
45
|
+
createUIWindow(world, {
|
|
46
|
+
id: 'status',
|
|
47
|
+
title: 'Player Status',
|
|
48
|
+
config: './ui/status.uikitml', // UIKitML source; IWSDK 0.5 parses it at runtime
|
|
49
|
+
dockMode: DockMode.BodyFollow, // follows until the user pins it
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
windows.events.on('closed', (w) => console.log(`${w.title} closed`));
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Window markup
|
|
56
|
+
|
|
57
|
+
Windows are ordinary UIKitML panels; the chrome is discovered by well-known element ids (only the ids are contractual - restyle freely):
|
|
58
|
+
|
|
59
|
+
```html
|
|
60
|
+
<div id="uix-window" class="my-window">
|
|
61
|
+
<div id="uix-titlebar" class="my-titlebar"> <!-- drag surface -->
|
|
62
|
+
<span id="uix-title" class="my-title">.</span>
|
|
63
|
+
<div id="uix-pin">PIN</div> <!-- follow ⇄ placed; label auto-syncs to PIN/UNPIN -->
|
|
64
|
+
<div id="uix-dock">DOCK</div> <!-- return to home (spawn region / placement) -->
|
|
65
|
+
<div id="uix-minimize">MIN</div>
|
|
66
|
+
<div id="uix-close">X</div>
|
|
67
|
+
</div>
|
|
68
|
+
<div id="uix-content">
|
|
69
|
+
<!-- window body -->
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
> Use `<div>`s (not `<button>`s) for chrome buttons: with a component kit registered, lowercase `<button>` resolves to the kit's Button component, whose intrinsic sizing fights compact title-bar chrome.
|
|
75
|
+
|
|
76
|
+
### Controls markup
|
|
77
|
+
|
|
78
|
+
Annotate any element with `data-uix` and the `UIControlsSystem` upgrades it - in any panel, not just windows:
|
|
79
|
+
|
|
80
|
+
```html
|
|
81
|
+
<div data-uix="stepper" data-uix-id="health" data-uix-min="0" data-uix-max="100" data-uix-step="10">
|
|
82
|
+
<button data-uix-role="decrement">-</button>
|
|
83
|
+
<span data-uix-role="value">.</span>
|
|
84
|
+
<button data-uix-role="increment">+</button>
|
|
85
|
+
</div>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
import { panelControlsFor } from '@realitycollective/iwsdk-uiextensions';
|
|
90
|
+
|
|
91
|
+
const controls = panelControlsFor(document); // the panel's UIKitDocument
|
|
92
|
+
controls.stepper('health').events.on('change', (hp) => setHealth(hp));
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
> UIKitML note: every dynamic-text element needs a literal placeholder child (`<span data-uix-role="value">.</span>`) or no Text node is created.
|
|
96
|
+
|
|
97
|
+
### Interaction model
|
|
98
|
+
|
|
99
|
+
- **Drag** the title bar with the ray (or mouse on desktop) to move a window; it billboards toward you while dragging and settles facing you when released. A press only becomes a drag after `dragDelay` seconds (default 0.3, per-window on `UIWindow`) - shorter presses stay clicks, and the chrome buttons swallow their presses entirely, so PIN/DOCK/MIN/X never fight the drag gesture.
|
|
100
|
+
- **Drop** a window inside a region's snap radius to dock it into the next slot; drag it out again to undock.
|
|
101
|
+
- **Pin** toggles `body-follow` ⇄ `world-locked` ("place in space").
|
|
102
|
+
- Dragging a following window implicitly places it - pin re-attaches it.
|
|
103
|
+
|
|
104
|
+
See `Examples/` (shipped in this package) and the deployable showcase client in the repository for complete, working demonstrations of every feature.
|
|
105
|
+
|
|
106
|
+
## Windows and panel readiness
|
|
107
|
+
|
|
108
|
+
`createUIWindow` returns the ECS entity, which is what you want when you are going to add components to it. When you want the PANEL, use the scene host: `createWindow` gives you a handle that resolves itself.
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
import { createSceneHost, getPanelHandle } from '@realitycollective/iwsdk-uiextensions';
|
|
112
|
+
|
|
113
|
+
const host = createSceneHost(world); // call after registerUIExtensions(world)
|
|
114
|
+
|
|
115
|
+
const status = host.createWindow({
|
|
116
|
+
id: 'status', // optional - omit and you get uix-window-<n>
|
|
117
|
+
title: 'Player Status',
|
|
118
|
+
config: './ui/status.uikitml',
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
status.panel; // undefined until IWSDK attaches the document
|
|
122
|
+
status.onReady((panel) => { // runs once, immediately if it is already there
|
|
123
|
+
panel.getElementById('uix-title');
|
|
124
|
+
});
|
|
125
|
+
status.entity; // still the entity, for ECS work
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
`getPanelHandle(entity)` does the same lookup for an entity you already hold, and returns `undefined` while the document is still loading.
|
|
129
|
+
|
|
130
|
+
Across a whole scene, subscribe to the host instead:
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
host.onPanelReady(({ id, panel, kind }) => {
|
|
134
|
+
if (kind === 'panel') {
|
|
135
|
+
// A bare PanelUI entity with no UIWindow: `id` is its config path.
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
wireMyWindow(id, panel);
|
|
139
|
+
});
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Bare panels are announced as well as managed windows, which is how devtools and hand-built `PanelUI` entities show up in the same stream. `supportsStandalonePanels` is `false` on this host: IWSDK owns panel lifecycles through the ECS, so `createPanel()` throws rather than half-working. Spawn a window instead.
|
|
143
|
+
|
|
144
|
+
## Headless core
|
|
145
|
+
|
|
146
|
+
All decision logic (window manager, dock state machine, region slot math, drag math, control models) lives in `@realitycollective/webxr-uiextensions` - pure TypeScript with no engine imports, tested at 100% coverage. The ECS systems in this package are thin appliers of that core onto `@iwsdk/core` components.
|
|
147
|
+
|
|
148
|
+
## Live demos
|
|
149
|
+
|
|
150
|
+
- Showcase: **[webxr-uiextensions.pages.dev](https://webxr-uiextensions.pages.dev)**
|
|
151
|
+
- Multiplatform lab: **[webxr-uix-lab.pages.dev](https://webxr-uix-lab.pages.dev)**
|
|
152
|
+
|
|
153
|
+
## License
|
|
154
|
+
|
|
155
|
+
MIT © Reality Collective
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The UIKitML component set for `<uix-*>` control elements.
|
|
3
|
+
*
|
|
4
|
+
* IWSDK 0.5 parses panels with `@drawcall/uikitml`, which validates every tag
|
|
5
|
+
* against a component schema and rejects anything it does not know - including
|
|
6
|
+
* every `data-*` attribute on a built-in tag. Declaring the control elements
|
|
7
|
+
* here is the supported way to extend that vocabulary.
|
|
8
|
+
*
|
|
9
|
+
* The three.js and XR Blocks adapters need no equivalent: their parser accepts
|
|
10
|
+
* custom tags unregistered, exposing the tag on
|
|
11
|
+
* `userData.customElement.componentName`. This set reproduces that convention
|
|
12
|
+
* on the IWSDK side - the constructor below stamps the same key and lifts
|
|
13
|
+
* `data-uix-*` attributes into `userData` - so ONE markup file drives every
|
|
14
|
+
* adapter and the engine-free upgraders read it identically everywhere.
|
|
15
|
+
*
|
|
16
|
+
* Register it when creating the world:
|
|
17
|
+
* ```ts
|
|
18
|
+
* const world = await World.create(container, {
|
|
19
|
+
* features: { spatialUI: { kit: 'horizon', componentSets: [uixComponentSet] } },
|
|
20
|
+
* });
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
import { type ComponentSet } from '@drawcall/uikitml';
|
|
24
|
+
/**
|
|
25
|
+
* Pass to `spatialUI.componentSets` so `<uix-*>` elements parse on IWSDK 0.5.
|
|
26
|
+
* Without it, a panel using any control fails to parse and never attaches.
|
|
27
|
+
*/
|
|
28
|
+
export declare const uixComponentSet: ComponentSet;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The UIKitML component set for `<uix-*>` control elements.
|
|
3
|
+
*
|
|
4
|
+
* IWSDK 0.5 parses panels with `@drawcall/uikitml`, which validates every tag
|
|
5
|
+
* against a component schema and rejects anything it does not know - including
|
|
6
|
+
* every `data-*` attribute on a built-in tag. Declaring the control elements
|
|
7
|
+
* here is the supported way to extend that vocabulary.
|
|
8
|
+
*
|
|
9
|
+
* The three.js and XR Blocks adapters need no equivalent: their parser accepts
|
|
10
|
+
* custom tags unregistered, exposing the tag on
|
|
11
|
+
* `userData.customElement.componentName`. This set reproduces that convention
|
|
12
|
+
* on the IWSDK side - the constructor below stamps the same key and lifts
|
|
13
|
+
* `data-uix-*` attributes into `userData` - so ONE markup file drives every
|
|
14
|
+
* adapter and the engine-free upgraders read it identically everywhere.
|
|
15
|
+
*
|
|
16
|
+
* Register it when creating the world:
|
|
17
|
+
* ```ts
|
|
18
|
+
* const world = await World.create(container, {
|
|
19
|
+
* features: { spatialUI: { kit: 'horizon', componentSets: [uixComponentSet] } },
|
|
20
|
+
* });
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
import { htmlComponentSet } from '@drawcall/uikitml';
|
|
24
|
+
import { z } from 'zod';
|
|
25
|
+
/**
|
|
26
|
+
* Base container, taken from the parser's own `<div>` definition rather than
|
|
27
|
+
* imported from `@pmndrs/uikit` directly - that keeps a single copy of uikit
|
|
28
|
+
* in the tree, whichever version `@iwsdk/core` resolves.
|
|
29
|
+
*/
|
|
30
|
+
const BaseContainer = htmlComponentSet['div']
|
|
31
|
+
?.component;
|
|
32
|
+
/** Control elements an author writes, and the parts that live inside them. */
|
|
33
|
+
const CONTROL_TAGS = [
|
|
34
|
+
'uix-stepper',
|
|
35
|
+
'uix-toggle',
|
|
36
|
+
'uix-expandable-label',
|
|
37
|
+
'uix-log-view',
|
|
38
|
+
];
|
|
39
|
+
const ROLE_TAGS = [
|
|
40
|
+
'uix-decrement',
|
|
41
|
+
'uix-value',
|
|
42
|
+
'uix-increment',
|
|
43
|
+
'uix-label',
|
|
44
|
+
'uix-line',
|
|
45
|
+
'uix-up',
|
|
46
|
+
'uix-down',
|
|
47
|
+
'uix-clear',
|
|
48
|
+
'uix-status',
|
|
49
|
+
'uix-text',
|
|
50
|
+
'uix-more',
|
|
51
|
+
];
|
|
52
|
+
/**
|
|
53
|
+
* `data-uix-min` → `uixMin`, matching how the other parser fills userData.
|
|
54
|
+
*
|
|
55
|
+
* The parser hands properties over already camelCased, so `data-uix-min`
|
|
56
|
+
* arrives as `dataUixMin`; both spellings are normalised here.
|
|
57
|
+
*/
|
|
58
|
+
function toUserDataKey(attribute) {
|
|
59
|
+
if (/^data[A-Z]/.test(attribute)) {
|
|
60
|
+
const rest = attribute.slice(4);
|
|
61
|
+
return rest.charAt(0).toLowerCase() + rest.slice(1);
|
|
62
|
+
}
|
|
63
|
+
return attribute
|
|
64
|
+
.replace(/^data-/, '')
|
|
65
|
+
.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* A plain uikit Container that records what it was declared as.
|
|
69
|
+
*
|
|
70
|
+
* Anything the schema let through that is not a uikit property - the
|
|
71
|
+
* `data-uix-*` parameters - is moved into `userData` rather than handed to
|
|
72
|
+
* uikit, which would warn about properties it does not recognise.
|
|
73
|
+
*/
|
|
74
|
+
function controlComponent(tag) {
|
|
75
|
+
return class UixControlContainer extends BaseContainer {
|
|
76
|
+
constructor(properties = {}) {
|
|
77
|
+
const uikitProps = {};
|
|
78
|
+
const userData = {};
|
|
79
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
80
|
+
if (key.startsWith('data-') || /^data[A-Z]/.test(key)) {
|
|
81
|
+
userData[toUserDataKey(key)] = value;
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
uikitProps[key] = value;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
super(uikitProps);
|
|
88
|
+
Object.assign(this.userData, userData, {
|
|
89
|
+
customElement: { componentName: tag, sourceTag: tag },
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/** Permissive: the parameters are ours, and the upgraders validate them. */
|
|
95
|
+
const schema = z.object({}).passthrough();
|
|
96
|
+
function define(tag) {
|
|
97
|
+
return {
|
|
98
|
+
component: controlComponent(tag),
|
|
99
|
+
schema,
|
|
100
|
+
canHaveChildren: true,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Pass to `spatialUI.componentSets` so `<uix-*>` elements parse on IWSDK 0.5.
|
|
105
|
+
* Without it, a panel using any control fails to parse and never attaches.
|
|
106
|
+
*/
|
|
107
|
+
export const uixComponentSet = Object.fromEntries([...CONTROL_TAGS, ...ROLE_TAGS].map((tag) => [tag, define(tag)]));
|
|
108
|
+
//# sourceMappingURL=component-set.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"component-set.js","sourceRoot":"","sources":["../src/component-set.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,EAAE,gBAAgB,EAA+C,MAAM,mBAAmB,CAAC;AAClG,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;GAIG;AACH,MAAM,aAAa,GAAG,gBAAgB,CAAC,KAAK,CAAC;IAC3C,EAAE,SAEH,CAAC;AAEF,8EAA8E;AAC9E,MAAM,YAAY,GAAG;IACnB,aAAa;IACb,YAAY;IACZ,sBAAsB;IACtB,cAAc;CACN,CAAC;AAEX,MAAM,SAAS,GAAG;IAChB,eAAe;IACf,WAAW;IACX,eAAe;IACf,WAAW;IACX,UAAU;IACV,QAAQ;IACR,UAAU;IACV,WAAW;IACX,YAAY;IACZ,UAAU;IACV,UAAU;CACF,CAAC;AAEX;;;;;GAKG;AACH,SAAS,aAAa,CAAC,SAAiB;IACtC,IAAI,YAAY,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC;QACjC,MAAM,IAAI,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QAChC,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACtD,CAAC;IACD,OAAO,SAAS;SACb,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC;SACrB,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC,EAAE,CAAS,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC;AAChE,CAAC;AAED;;;;;;GAMG;AACH,SAAS,gBAAgB,CAAC,GAAW;IACnC,OAAO,MAAM,mBAAoB,SAAQ,aAAa;QACpD,YAAY,aAAsC,EAAE;YAClD,MAAM,UAAU,GAA4B,EAAE,CAAC;YAC/C,MAAM,QAAQ,GAA4B,EAAE,CAAC;YAC7C,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC;gBACtD,IAAI,GAAG,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;oBACtD,QAAQ,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,GAAG,KAAK,CAAC;gBACvC,CAAC;qBAAM,CAAC;oBACN,UAAU,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;gBAC1B,CAAC;YACH,CAAC;YACD,KAAK,CAAC,UAAU,CAAC,CAAC;YAClB,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,EAAE;gBACrC,aAAa,EAAE,EAAE,aAAa,EAAE,GAAG,EAAE,SAAS,EAAE,GAAG,EAAE;aACtD,CAAC,CAAC;QACL,CAAC;KACF,CAAC;AACJ,CAAC;AAED,4EAA4E;AAC5E,MAAM,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;AAE1C,SAAS,MAAM,CAAC,GAAW;IACzB,OAAO;QACL,SAAS,EAAE,gBAAgB,CAAC,GAAG,CAAgD;QAC/E,MAAM;QACN,eAAe,EAAE,IAAI;KACtB,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,MAAM,eAAe,GAAiB,MAAM,CAAC,WAAW,CAC7D,CAAC,GAAG,YAAY,EAAE,GAAG,SAAS,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CACjE,CAAC","sourcesContent":["/**\n * The UIKitML component set for `<uix-*>` control elements.\n *\n * IWSDK 0.5 parses panels with `@drawcall/uikitml`, which validates every tag\n * against a component schema and rejects anything it does not know - including\n * every `data-*` attribute on a built-in tag. Declaring the control elements\n * here is the supported way to extend that vocabulary.\n *\n * The three.js and XR Blocks adapters need no equivalent: their parser accepts\n * custom tags unregistered, exposing the tag on\n * `userData.customElement.componentName`. This set reproduces that convention\n * on the IWSDK side - the constructor below stamps the same key and lifts\n * `data-uix-*` attributes into `userData` - so ONE markup file drives every\n * adapter and the engine-free upgraders read it identically everywhere.\n *\n * Register it when creating the world:\n * ```ts\n * const world = await World.create(container, {\n * features: { spatialUI: { kit: 'horizon', componentSets: [uixComponentSet] } },\n * });\n * ```\n */\nimport { htmlComponentSet, type ComponentSet, type ComponentDefinition } from '@drawcall/uikitml';\nimport { z } from 'zod';\n\n/**\n * Base container, taken from the parser's own `<div>` definition rather than\n * imported from `@pmndrs/uikit` directly - that keeps a single copy of uikit\n * in the tree, whichever version `@iwsdk/core` resolves.\n */\nconst BaseContainer = htmlComponentSet['div']\n ?.component as unknown as new (properties?: Record<string, unknown>) => {\n userData: Record<string, unknown>;\n};\n\n/** Control elements an author writes, and the parts that live inside them. */\nconst CONTROL_TAGS = [\n 'uix-stepper',\n 'uix-toggle',\n 'uix-expandable-label',\n 'uix-log-view',\n] as const;\n\nconst ROLE_TAGS = [\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] as const;\n\n/**\n * `data-uix-min` → `uixMin`, matching how the other parser fills userData.\n *\n * The parser hands properties over already camelCased, so `data-uix-min`\n * arrives as `dataUixMin`; both spellings are normalised here.\n */\nfunction toUserDataKey(attribute: string): string {\n if (/^data[A-Z]/.test(attribute)) {\n const rest = attribute.slice(4);\n return rest.charAt(0).toLowerCase() + rest.slice(1);\n }\n return attribute\n .replace(/^data-/, '')\n .replace(/-([a-z0-9])/g, (_, c: string) => c.toUpperCase());\n}\n\n/**\n * A plain uikit Container that records what it was declared as.\n *\n * Anything the schema let through that is not a uikit property - the\n * `data-uix-*` parameters - is moved into `userData` rather than handed to\n * uikit, which would warn about properties it does not recognise.\n */\nfunction controlComponent(tag: string): typeof BaseContainer {\n return class UixControlContainer extends BaseContainer {\n constructor(properties: Record<string, unknown> = {}) {\n const uikitProps: Record<string, unknown> = {};\n const userData: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(properties)) {\n if (key.startsWith('data-') || /^data[A-Z]/.test(key)) {\n userData[toUserDataKey(key)] = value;\n } else {\n uikitProps[key] = value;\n }\n }\n super(uikitProps);\n Object.assign(this.userData, userData, {\n customElement: { componentName: tag, sourceTag: tag },\n });\n }\n };\n}\n\n/** Permissive: the parameters are ours, and the upgraders validate them. */\nconst schema = z.object({}).passthrough();\n\nfunction define(tag: string): ComponentDefinition {\n return {\n component: controlComponent(tag) as unknown as ComponentDefinition['component'],\n schema,\n canHaveChildren: true,\n };\n}\n\n/**\n * Pass to `spatialUI.componentSets` so `<uix-*>` elements parse on IWSDK 0.5.\n * Without it, a panel using any control fails to parse and never attaches.\n */\nexport const uixComponentSet: ComponentSet = Object.fromEntries(\n [...CONTROL_TAGS, ...ROLE_TAGS].map((tag) => [tag, define(tag)]),\n);\n"]}
|