@realitycollective/webxr-uiextensions 0.1.0-preview.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/LICENSE +21 -0
  3. package/README.md +61 -0
  4. package/dist/adapter.d.ts +105 -0
  5. package/dist/adapter.js +2 -0
  6. package/dist/adapter.js.map +1 -0
  7. package/dist/chrome/markup.d.ts +40 -0
  8. package/dist/chrome/markup.js +56 -0
  9. package/dist/chrome/markup.js.map +1 -0
  10. package/dist/controls/element.d.ts +22 -0
  11. package/dist/controls/element.js +40 -0
  12. package/dist/controls/element.js.map +1 -0
  13. package/dist/controls/expandable-label.d.ts +32 -0
  14. package/dist/controls/expandable-label.js +63 -0
  15. package/dist/controls/expandable-label.js.map +1 -0
  16. package/dist/controls/log-view.d.ts +38 -0
  17. package/dist/controls/log-view.js +83 -0
  18. package/dist/controls/log-view.js.map +1 -0
  19. package/dist/controls/stepper.d.ts +32 -0
  20. package/dist/controls/stepper.js +78 -0
  21. package/dist/controls/stepper.js.map +1 -0
  22. package/dist/controls/toggle.d.ts +33 -0
  23. package/dist/controls/toggle.js +63 -0
  24. package/dist/controls/toggle.js.map +1 -0
  25. package/dist/controls/upgrade.d.ts +31 -0
  26. package/dist/controls/upgrade.js +87 -0
  27. package/dist/controls/upgrade.js.map +1 -0
  28. package/dist/core/dock-state.d.ts +58 -0
  29. package/dist/core/dock-state.js +64 -0
  30. package/dist/core/dock-state.js.map +1 -0
  31. package/dist/core/drag-math.d.ts +34 -0
  32. package/dist/core/drag-math.js +44 -0
  33. package/dist/core/drag-math.js.map +1 -0
  34. package/dist/core/events.d.ts +17 -0
  35. package/dist/core/events.js +40 -0
  36. package/dist/core/events.js.map +1 -0
  37. package/dist/core/expandable-model.d.ts +35 -0
  38. package/dist/core/expandable-model.js +57 -0
  39. package/dist/core/expandable-model.js.map +1 -0
  40. package/dist/core/hold-to-drag.d.ts +24 -0
  41. package/dist/core/hold-to-drag.js +34 -0
  42. package/dist/core/hold-to-drag.js.map +1 -0
  43. package/dist/core/log-model.d.ts +42 -0
  44. package/dist/core/log-model.js +69 -0
  45. package/dist/core/log-model.js.map +1 -0
  46. package/dist/core/region-layout.d.ts +41 -0
  47. package/dist/core/region-layout.js +79 -0
  48. package/dist/core/region-layout.js.map +1 -0
  49. package/dist/core/region-registry.d.ts +35 -0
  50. package/dist/core/region-registry.js +97 -0
  51. package/dist/core/region-registry.js.map +1 -0
  52. package/dist/core/stepper-model.d.ts +24 -0
  53. package/dist/core/stepper-model.js +46 -0
  54. package/dist/core/stepper-model.js.map +1 -0
  55. package/dist/core/toggle-model.d.ts +11 -0
  56. package/dist/core/toggle-model.js +25 -0
  57. package/dist/core/toggle-model.js.map +1 -0
  58. package/dist/core/window-manager.d.ts +81 -0
  59. package/dist/core/window-manager.js +172 -0
  60. package/dist/core/window-manager.js.map +1 -0
  61. package/dist/index.d.ts +29 -0
  62. package/dist/index.js +34 -0
  63. package/dist/index.js.map +1 -0
  64. package/dist/scene.d.ts +79 -0
  65. package/dist/scene.js +43 -0
  66. package/dist/scene.js.map +1 -0
  67. package/package.json +51 -0
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Stepper model - pure numeric state for the `data-uix="stepper"` control.
3
+ */
4
+ export interface StepperOptions {
5
+ min?: number;
6
+ max?: number;
7
+ step?: number;
8
+ value?: number;
9
+ }
10
+ export declare class StepperModel {
11
+ readonly min: number;
12
+ readonly max: number;
13
+ readonly step: number;
14
+ private current;
15
+ constructor(options?: StepperOptions);
16
+ get value(): number;
17
+ /** Set (clamped); returns true when the value actually changed. */
18
+ set(value: number): boolean;
19
+ increment(): boolean;
20
+ decrement(): boolean;
21
+ get canIncrement(): boolean;
22
+ get canDecrement(): boolean;
23
+ private clamp;
24
+ }
@@ -0,0 +1,46 @@
1
+ export class StepperModel {
2
+ min;
3
+ max;
4
+ step;
5
+ current;
6
+ constructor(options = {}) {
7
+ this.min = options.min ?? 0;
8
+ this.max = options.max ?? 10;
9
+ this.step = options.step ?? 1;
10
+ if (this.max < this.min) {
11
+ throw new Error(`[uix] stepper max (${this.max}) < min (${this.min})`);
12
+ }
13
+ if (!(this.step > 0)) {
14
+ throw new Error(`[uix] stepper step must be > 0 (got ${this.step})`);
15
+ }
16
+ this.current = this.clamp(options.value ?? this.min);
17
+ }
18
+ get value() {
19
+ return this.current;
20
+ }
21
+ /** Set (clamped); returns true when the value actually changed. */
22
+ set(value) {
23
+ const next = this.clamp(value);
24
+ if (next === this.current) {
25
+ return false;
26
+ }
27
+ this.current = next;
28
+ return true;
29
+ }
30
+ increment() {
31
+ return this.set(this.current + this.step);
32
+ }
33
+ decrement() {
34
+ return this.set(this.current - this.step);
35
+ }
36
+ get canIncrement() {
37
+ return this.current < this.max;
38
+ }
39
+ get canDecrement() {
40
+ return this.current > this.min;
41
+ }
42
+ clamp(value) {
43
+ return Math.min(this.max, Math.max(this.min, value));
44
+ }
45
+ }
46
+ //# sourceMappingURL=stepper-model.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stepper-model.js","sourceRoot":"","sources":["../../src/core/stepper-model.ts"],"names":[],"mappings":"AAUA,MAAM,OAAO,YAAY;IACd,GAAG,CAAS;IACZ,GAAG,CAAS;IACZ,IAAI,CAAS;IACd,OAAO,CAAS;IAExB,YAAY,UAA0B,EAAE;QACtC,IAAI,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC;QAC5B,IAAI,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,EAAE,CAAC;QAC7B,IAAI,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,CAAC,CAAC;QAC9B,IAAI,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YACxB,MAAM,IAAI,KAAK,CAAC,sBAAsB,IAAI,CAAC,GAAG,YAAY,IAAI,CAAC,GAAG,GAAG,CAAC,CAAC;QACzE,CAAC;QACD,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,EAAE,CAAC;YACrB,MAAM,IAAI,KAAK,CAAC,uCAAuC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC;QACvE,CAAC;QACD,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC;IACvD,CAAC;IAED,IAAI,KAAK;QACP,OAAO,IAAI,CAAC,OAAO,CAAC;IACtB,CAAC;IAED,mEAAmE;IACnE,GAAG,CAAC,KAAa;QACf,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAC/B,IAAI,IAAI,KAAK,IAAI,CAAC,OAAO,EAAE,CAAC;YAC1B,OAAO,KAAK,CAAC;QACf,CAAC;QACD,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QACpB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,SAAS;QACP,OAAO,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC;IAC5C,CAAC;IAED,SAAS;QACP,OAAO,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC;IAC5C,CAAC;IAED,IAAI,YAAY;QACd,OAAO,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC;IACjC,CAAC;IAED,IAAI,YAAY;QACd,OAAO,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC;IACjC,CAAC;IAEO,KAAK,CAAC,KAAa;QACzB,OAAO,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC;IACvD,CAAC;CACF","sourcesContent":["/**\n * Stepper model - pure numeric state for the `data-uix=\"stepper\"` control.\n */\nexport interface StepperOptions {\n min?: number;\n max?: number;\n step?: number;\n value?: number;\n}\n\nexport class StepperModel {\n readonly min: number;\n readonly max: number;\n readonly step: number;\n private current: number;\n\n constructor(options: StepperOptions = {}) {\n this.min = options.min ?? 0;\n this.max = options.max ?? 10;\n this.step = options.step ?? 1;\n if (this.max < this.min) {\n throw new Error(`[uix] stepper max (${this.max}) < min (${this.min})`);\n }\n if (!(this.step > 0)) {\n throw new Error(`[uix] stepper step must be > 0 (got ${this.step})`);\n }\n this.current = this.clamp(options.value ?? this.min);\n }\n\n get value(): number {\n return this.current;\n }\n\n /** Set (clamped); returns true when the value actually changed. */\n set(value: number): boolean {\n const next = this.clamp(value);\n if (next === this.current) {\n return false;\n }\n this.current = next;\n return true;\n }\n\n increment(): boolean {\n return this.set(this.current + this.step);\n }\n\n decrement(): boolean {\n return this.set(this.current - this.step);\n }\n\n get canIncrement(): boolean {\n return this.current < this.max;\n }\n\n get canDecrement(): boolean {\n return this.current > this.min;\n }\n\n private clamp(value: number): number {\n return Math.min(this.max, Math.max(this.min, value));\n }\n}\n"]}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Toggle model - pure boolean state for the `data-uix="toggle"` control.
3
+ */
4
+ export declare class ToggleModel {
5
+ private state;
6
+ constructor(initial?: boolean);
7
+ get value(): boolean;
8
+ /** Set the state; returns true when it actually changed. */
9
+ set(value: boolean): boolean;
10
+ toggle(): boolean;
11
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Toggle model - pure boolean state for the `data-uix="toggle"` control.
3
+ */
4
+ export class ToggleModel {
5
+ state;
6
+ constructor(initial = false) {
7
+ this.state = initial;
8
+ }
9
+ get value() {
10
+ return this.state;
11
+ }
12
+ /** Set the state; returns true when it actually changed. */
13
+ set(value) {
14
+ if (value === this.state) {
15
+ return false;
16
+ }
17
+ this.state = value;
18
+ return true;
19
+ }
20
+ toggle() {
21
+ this.state = !this.state;
22
+ return this.state;
23
+ }
24
+ }
25
+ //# sourceMappingURL=toggle-model.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toggle-model.js","sourceRoot":"","sources":["../../src/core/toggle-model.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,OAAO,WAAW;IACd,KAAK,CAAU;IAEvB,YAAY,OAAO,GAAG,KAAK;QACzB,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC;IACvB,CAAC;IAED,IAAI,KAAK;QACP,OAAO,IAAI,CAAC,KAAK,CAAC;IACpB,CAAC;IAED,4DAA4D;IAC5D,GAAG,CAAC,KAAc;QAChB,IAAI,KAAK,KAAK,IAAI,CAAC,KAAK,EAAE,CAAC;YACzB,OAAO,KAAK,CAAC;QACf,CAAC;QACD,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM;QACJ,IAAI,CAAC,KAAK,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC;QACzB,OAAO,IAAI,CAAC,KAAK,CAAC;IACpB,CAAC;CACF","sourcesContent":["/**\n * Toggle model - pure boolean state for the `data-uix=\"toggle\"` control.\n */\nexport class ToggleModel {\n private state: boolean;\n\n constructor(initial = false) {\n this.state = initial;\n }\n\n get value(): boolean {\n return this.state;\n }\n\n /** Set the state; returns true when it actually changed. */\n set(value: boolean): boolean {\n if (value === this.state) {\n return false;\n }\n this.state = value;\n return true;\n }\n\n toggle(): boolean {\n this.state = !this.state;\n return this.state;\n }\n}\n"]}
@@ -0,0 +1,81 @@
1
+ /**
2
+ * WindowManager - pure window registry, focus ordering and minimize state.
3
+ *
4
+ * The manager knows nothing about entities, three.js or uikit. It deals in
5
+ * opaque window ids and answers two questions the ECS layer applies each
6
+ * frame:
7
+ *
8
+ * 1. What is the focus (z) order? (`orderOf` → depth bias so the focused
9
+ * window renders nearest the user and receives pointer priority)
10
+ * 2. What state is a window in? (minimized / focused / dock mode)
11
+ */
12
+ import { Emitter } from './events.js';
13
+ import { DockModeValue } from './dock-state.js';
14
+ export interface WindowRecord {
15
+ id: string;
16
+ title: string;
17
+ dockMode: DockModeValue;
18
+ minimized: boolean;
19
+ /** True while the user is actively dragging the window by its title bar. */
20
+ dragging: boolean;
21
+ }
22
+ export interface WindowManagerEvents extends Record<string, unknown> {
23
+ opened: WindowRecord;
24
+ closed: WindowRecord;
25
+ focused: WindowRecord;
26
+ minimized: WindowRecord;
27
+ restored: WindowRecord;
28
+ dockChanged: {
29
+ window: WindowRecord;
30
+ previous: DockModeValue;
31
+ };
32
+ dragStarted: WindowRecord;
33
+ dragEnded: WindowRecord;
34
+ }
35
+ export interface OpenWindowOptions {
36
+ title?: string;
37
+ dockMode?: DockModeValue;
38
+ }
39
+ /**
40
+ * What the pin affordance should read for a window's current state:
41
+ * - dragging → "PIN" (the window is loose in your hand)
42
+ * - placed/world-locked → "UNPIN" (click releases it to follow)
43
+ * - following → "PIN" (click pins it where it is)
44
+ */
45
+ export declare function pinLabelFor(record: Pick<WindowRecord, 'dockMode' | 'dragging'>): 'PIN' | 'UNPIN';
46
+ /**
47
+ * What the minimize affordance should read for a window's current state -
48
+ * the label always names what the NEXT click does, matching `pinLabelFor`:
49
+ * - open → "MIN" (click collapses the content)
50
+ * - minimized → "MAX" (click restores it)
51
+ */
52
+ export declare function minimizeLabelFor(record: Pick<WindowRecord, 'minimized'>): 'MIN' | 'MAX';
53
+ export declare class WindowManager {
54
+ readonly events: Emitter<WindowManagerEvents>;
55
+ private windows;
56
+ /** Most-recently-focused last (top of the stack). */
57
+ private focusStack;
58
+ open(id: string, options?: OpenWindowOptions): WindowRecord;
59
+ close(id: string): void;
60
+ focus(id: string): void;
61
+ minimize(id: string): void;
62
+ restore(id: string): void;
63
+ toggleMinimized(id: string): void;
64
+ setDockMode(id: string, mode: DockModeValue): void;
65
+ /** Track an active title-bar drag; emits dragStarted/dragEnded on change. */
66
+ setDragging(id: string, dragging: boolean): void;
67
+ /** Title-bar pin button behaviour: place in space ↔ follow the player. */
68
+ togglePin(id: string): DockModeValue;
69
+ get(id: string): WindowRecord | undefined;
70
+ has(id: string): boolean;
71
+ get focused(): WindowRecord | undefined;
72
+ /**
73
+ * Focus depth of a window: 0 = focused (topmost), 1 = next, and so on.
74
+ * The ECS layer converts this into a small z bias toward the viewer so
75
+ * overlapping panels resolve in focus order.
76
+ */
77
+ orderOf(id: string): number;
78
+ get count(): number;
79
+ list(): WindowRecord[];
80
+ private require;
81
+ }
@@ -0,0 +1,172 @@
1
+ /**
2
+ * WindowManager - pure window registry, focus ordering and minimize state.
3
+ *
4
+ * The manager knows nothing about entities, three.js or uikit. It deals in
5
+ * opaque window ids and answers two questions the ECS layer applies each
6
+ * frame:
7
+ *
8
+ * 1. What is the focus (z) order? (`orderOf` → depth bias so the focused
9
+ * window renders nearest the user and receives pointer priority)
10
+ * 2. What state is a window in? (minimized / focused / dock mode)
11
+ */
12
+ import { Emitter } from './events.js';
13
+ import { DockMode, isDockMode, togglePinned } from './dock-state.js';
14
+ /**
15
+ * What the pin affordance should read for a window's current state:
16
+ * - dragging → "PIN" (the window is loose in your hand)
17
+ * - placed/world-locked → "UNPIN" (click releases it to follow)
18
+ * - following → "PIN" (click pins it where it is)
19
+ */
20
+ export function pinLabelFor(record) {
21
+ if (record.dragging) {
22
+ return 'PIN';
23
+ }
24
+ return record.dockMode === DockMode.WorldLocked ? 'UNPIN' : 'PIN';
25
+ }
26
+ /**
27
+ * What the minimize affordance should read for a window's current state -
28
+ * the label always names what the NEXT click does, matching `pinLabelFor`:
29
+ * - open → "MIN" (click collapses the content)
30
+ * - minimized → "MAX" (click restores it)
31
+ */
32
+ export function minimizeLabelFor(record) {
33
+ return record.minimized ? 'MAX' : 'MIN';
34
+ }
35
+ export class WindowManager {
36
+ events = new Emitter();
37
+ windows = new Map();
38
+ /** Most-recently-focused last (top of the stack). */
39
+ focusStack = [];
40
+ open(id, options = {}) {
41
+ if (this.windows.has(id)) {
42
+ throw new Error(`[uix] window "${id}" is already open`);
43
+ }
44
+ const dockMode = options.dockMode ?? DockMode.WorldLocked;
45
+ if (!isDockMode(dockMode)) {
46
+ throw new Error(`[uix] "${String(dockMode)}" is not a dock mode`);
47
+ }
48
+ const record = {
49
+ id,
50
+ title: options.title ?? id,
51
+ dockMode,
52
+ minimized: false,
53
+ dragging: false,
54
+ };
55
+ this.windows.set(id, record);
56
+ this.focusStack.push(id);
57
+ this.events.emit('opened', record);
58
+ this.events.emit('focused', record);
59
+ return record;
60
+ }
61
+ close(id) {
62
+ const record = this.require(id);
63
+ this.windows.delete(id);
64
+ this.focusStack = this.focusStack.filter((w) => w !== id);
65
+ this.events.emit('closed', record);
66
+ const top = this.focused;
67
+ if (top) {
68
+ this.events.emit('focused', top);
69
+ }
70
+ }
71
+ focus(id) {
72
+ const record = this.require(id);
73
+ const top = this.focusStack[this.focusStack.length - 1];
74
+ if (top === id) {
75
+ return;
76
+ }
77
+ this.focusStack = this.focusStack.filter((w) => w !== id);
78
+ this.focusStack.push(id);
79
+ this.events.emit('focused', record);
80
+ }
81
+ minimize(id) {
82
+ const record = this.require(id);
83
+ if (record.minimized) {
84
+ return;
85
+ }
86
+ record.minimized = true;
87
+ this.events.emit('minimized', record);
88
+ }
89
+ restore(id) {
90
+ const record = this.require(id);
91
+ if (!record.minimized) {
92
+ return;
93
+ }
94
+ record.minimized = false;
95
+ this.events.emit('restored', record);
96
+ this.focus(id);
97
+ }
98
+ toggleMinimized(id) {
99
+ if (this.require(id).minimized) {
100
+ this.restore(id);
101
+ }
102
+ else {
103
+ this.minimize(id);
104
+ }
105
+ }
106
+ setDockMode(id, mode) {
107
+ if (!isDockMode(mode)) {
108
+ throw new Error(`[uix] "${String(mode)}" is not a dock mode`);
109
+ }
110
+ const record = this.require(id);
111
+ if (record.dockMode === mode) {
112
+ return;
113
+ }
114
+ const previous = record.dockMode;
115
+ record.dockMode = mode;
116
+ this.events.emit('dockChanged', { window: record, previous });
117
+ }
118
+ /** Track an active title-bar drag; emits dragStarted/dragEnded on change. */
119
+ setDragging(id, dragging) {
120
+ const record = this.require(id);
121
+ if (record.dragging === dragging) {
122
+ return;
123
+ }
124
+ record.dragging = dragging;
125
+ this.events.emit(dragging ? 'dragStarted' : 'dragEnded', record);
126
+ }
127
+ /** Title-bar pin button behaviour: place in space ↔ follow the player. */
128
+ togglePin(id) {
129
+ const next = togglePinned(this.require(id).dockMode);
130
+ this.setDockMode(id, next);
131
+ return next;
132
+ }
133
+ get(id) {
134
+ return this.windows.get(id);
135
+ }
136
+ has(id) {
137
+ return this.windows.has(id);
138
+ }
139
+ get focused() {
140
+ const top = this.focusStack[this.focusStack.length - 1];
141
+ return top === undefined ? undefined : this.windows.get(top);
142
+ }
143
+ /**
144
+ * Focus depth of a window: 0 = focused (topmost), 1 = next, and so on.
145
+ * The ECS layer converts this into a small z bias toward the viewer so
146
+ * overlapping panels resolve in focus order.
147
+ */
148
+ orderOf(id) {
149
+ this.require(id);
150
+ // focusStack is bottom→top; depth counts down from the top.
151
+ const index = this.focusStack.lastIndexOf(id);
152
+ return this.focusStack.length - 1 - index;
153
+ }
154
+ get count() {
155
+ return this.windows.size;
156
+ }
157
+ list() {
158
+ // Top-of-stack first - the natural order for "window list" UIs.
159
+ return [...this.focusStack]
160
+ .reverse()
161
+ .map((id) => this.windows.get(id))
162
+ .filter((record) => record !== undefined);
163
+ }
164
+ require(id) {
165
+ const record = this.windows.get(id);
166
+ if (!record) {
167
+ throw new Error(`[uix] unknown window "${id}"`);
168
+ }
169
+ return record;
170
+ }
171
+ }
172
+ //# sourceMappingURL=window-manager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"window-manager.js","sourceRoot":"","sources":["../../src/core/window-manager.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * WindowManager - pure window registry, focus ordering and minimize state.\n *\n * The manager knows nothing about entities, three.js or uikit. It deals in\n * opaque window ids and answers two questions the ECS layer applies each\n * frame:\n *\n * 1. What is the focus (z) order? (`orderOf` → depth bias so the focused\n * window renders nearest the user and receives pointer priority)\n * 2. What state is a window in? (minimized / focused / dock mode)\n */\nimport { Emitter } from './events.js';\nimport { DockMode, DockModeValue, isDockMode, togglePinned } from './dock-state.js';\n\nexport interface WindowRecord {\n id: string;\n title: string;\n dockMode: DockModeValue;\n minimized: boolean;\n /** True while the user is actively dragging the window by its title bar. */\n dragging: boolean;\n}\n\nexport interface WindowManagerEvents extends Record<string, unknown> {\n opened: WindowRecord;\n closed: WindowRecord;\n focused: WindowRecord;\n minimized: WindowRecord;\n restored: WindowRecord;\n dockChanged: { window: WindowRecord; previous: DockModeValue };\n dragStarted: WindowRecord;\n dragEnded: WindowRecord;\n}\n\nexport interface OpenWindowOptions {\n title?: string;\n dockMode?: DockModeValue;\n}\n\n/**\n * What the pin affordance should read for a window's current state:\n * - dragging → \"PIN\" (the window is loose in your hand)\n * - placed/world-locked → \"UNPIN\" (click releases it to follow)\n * - following → \"PIN\" (click pins it where it is)\n */\nexport function pinLabelFor(record: Pick<WindowRecord, 'dockMode' | 'dragging'>): 'PIN' | 'UNPIN' {\n if (record.dragging) {\n return 'PIN';\n }\n return record.dockMode === DockMode.WorldLocked ? 'UNPIN' : 'PIN';\n}\n\n/**\n * What the minimize affordance should read for a window's current state -\n * the label always names what the NEXT click does, matching `pinLabelFor`:\n * - open → \"MIN\" (click collapses the content)\n * - minimized → \"MAX\" (click restores it)\n */\nexport function minimizeLabelFor(record: Pick<WindowRecord, 'minimized'>): 'MIN' | 'MAX' {\n return record.minimized ? 'MAX' : 'MIN';\n}\n\nexport class WindowManager {\n readonly events = new Emitter<WindowManagerEvents>();\n\n private windows = new Map<string, WindowRecord>();\n /** Most-recently-focused last (top of the stack). */\n private focusStack: string[] = [];\n\n open(id: string, options: OpenWindowOptions = {}): WindowRecord {\n if (this.windows.has(id)) {\n throw new Error(`[uix] window \"${id}\" is already open`);\n }\n const dockMode = options.dockMode ?? DockMode.WorldLocked;\n if (!isDockMode(dockMode)) {\n throw new Error(`[uix] \"${String(dockMode)}\" is not a dock mode`);\n }\n const record: WindowRecord = {\n id,\n title: options.title ?? id,\n dockMode,\n minimized: false,\n dragging: false,\n };\n this.windows.set(id, record);\n this.focusStack.push(id);\n this.events.emit('opened', record);\n this.events.emit('focused', record);\n return record;\n }\n\n close(id: string): void {\n const record = this.require(id);\n this.windows.delete(id);\n this.focusStack = this.focusStack.filter((w) => w !== id);\n this.events.emit('closed', record);\n const top = this.focused;\n if (top) {\n this.events.emit('focused', top);\n }\n }\n\n focus(id: string): void {\n const record = this.require(id);\n const top = this.focusStack[this.focusStack.length - 1];\n if (top === id) {\n return;\n }\n this.focusStack = this.focusStack.filter((w) => w !== id);\n this.focusStack.push(id);\n this.events.emit('focused', record);\n }\n\n minimize(id: string): void {\n const record = this.require(id);\n if (record.minimized) {\n return;\n }\n record.minimized = true;\n this.events.emit('minimized', record);\n }\n\n restore(id: string): void {\n const record = this.require(id);\n if (!record.minimized) {\n return;\n }\n record.minimized = false;\n this.events.emit('restored', record);\n this.focus(id);\n }\n\n toggleMinimized(id: string): void {\n if (this.require(id).minimized) {\n this.restore(id);\n } else {\n this.minimize(id);\n }\n }\n\n setDockMode(id: string, mode: DockModeValue): void {\n if (!isDockMode(mode)) {\n throw new Error(`[uix] \"${String(mode)}\" is not a dock mode`);\n }\n const record = this.require(id);\n if (record.dockMode === mode) {\n return;\n }\n const previous = record.dockMode;\n record.dockMode = mode;\n this.events.emit('dockChanged', { window: record, previous });\n }\n\n /** Track an active title-bar drag; emits dragStarted/dragEnded on change. */\n setDragging(id: string, dragging: boolean): void {\n const record = this.require(id);\n if (record.dragging === dragging) {\n return;\n }\n record.dragging = dragging;\n this.events.emit(dragging ? 'dragStarted' : 'dragEnded', record);\n }\n\n /** Title-bar pin button behaviour: place in space ↔ follow the player. */\n togglePin(id: string): DockModeValue {\n const next = togglePinned(this.require(id).dockMode);\n this.setDockMode(id, next);\n return next;\n }\n\n get(id: string): WindowRecord | undefined {\n return this.windows.get(id);\n }\n\n has(id: string): boolean {\n return this.windows.has(id);\n }\n\n get focused(): WindowRecord | undefined {\n const top = this.focusStack[this.focusStack.length - 1];\n return top === undefined ? undefined : this.windows.get(top);\n }\n\n /**\n * Focus depth of a window: 0 = focused (topmost), 1 = next, and so on.\n * The ECS layer converts this into a small z bias toward the viewer so\n * overlapping panels resolve in focus order.\n */\n orderOf(id: string): number {\n this.require(id);\n // focusStack is bottom→top; depth counts down from the top.\n const index = this.focusStack.lastIndexOf(id);\n return this.focusStack.length - 1 - index;\n }\n\n get count(): number {\n return this.windows.size;\n }\n\n list(): WindowRecord[] {\n // Top-of-stack first - the natural order for \"window list\" UIs.\n return [...this.focusStack]\n .reverse()\n .map((id) => this.windows.get(id))\n .filter((record): record is WindowRecord => record !== undefined);\n }\n\n private require(id: string): WindowRecord {\n const record = this.windows.get(id);\n if (!record) {\n throw new Error(`[uix] unknown window \"${id}\"`);\n }\n return record;\n }\n}\n"]}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * @realitycollective/webxr-uiextensions - the engine-free core.
3
+ *
4
+ * Everything exported here is pure TypeScript with no engine imports
5
+ * (enforced by test/architecture.test.ts): window/dock/region/drag logic,
6
+ * control models, the `data-uix` markup upgraders, the window chrome
7
+ * conventions, and the platform-adapter interfaces engine packages
8
+ * implement.
9
+ */
10
+ export * from './core/events.js';
11
+ export * from './core/dock-state.js';
12
+ export * from './core/window-manager.js';
13
+ export * from './core/region-layout.js';
14
+ export * from './core/region-registry.js';
15
+ export * from './core/drag-math.js';
16
+ export * from './core/hold-to-drag.js';
17
+ export * from './core/stepper-model.js';
18
+ export * from './core/toggle-model.js';
19
+ export * from './core/expandable-model.js';
20
+ export * from './core/log-model.js';
21
+ export * from './chrome/markup.js';
22
+ export * from './controls/element.js';
23
+ export * from './controls/stepper.js';
24
+ export * from './controls/toggle.js';
25
+ export * from './controls/expandable-label.js';
26
+ export * from './controls/log-view.js';
27
+ export * from './controls/upgrade.js';
28
+ export * from './adapter.js';
29
+ export * from './scene.js';
package/dist/index.js ADDED
@@ -0,0 +1,34 @@
1
+ /**
2
+ * @realitycollective/webxr-uiextensions - the engine-free core.
3
+ *
4
+ * Everything exported here is pure TypeScript with no engine imports
5
+ * (enforced by test/architecture.test.ts): window/dock/region/drag logic,
6
+ * control models, the `data-uix` markup upgraders, the window chrome
7
+ * conventions, and the platform-adapter interfaces engine packages
8
+ * implement.
9
+ */
10
+ // Pure logic
11
+ export * from './core/events.js';
12
+ export * from './core/dock-state.js';
13
+ export * from './core/window-manager.js';
14
+ export * from './core/region-layout.js';
15
+ export * from './core/region-registry.js';
16
+ export * from './core/drag-math.js';
17
+ export * from './core/hold-to-drag.js';
18
+ export * from './core/stepper-model.js';
19
+ export * from './core/toggle-model.js';
20
+ export * from './core/expandable-model.js';
21
+ export * from './core/log-model.js';
22
+ // Chrome conventions (markup ids + reference snippet)
23
+ export * from './chrome/markup.js';
24
+ // Interface-driven control upgraders
25
+ export * from './controls/element.js';
26
+ export * from './controls/stepper.js';
27
+ export * from './controls/toggle.js';
28
+ export * from './controls/expandable-label.js';
29
+ export * from './controls/log-view.js';
30
+ export * from './controls/upgrade.js';
31
+ // Platform-adapter contract + portable scene descriptors
32
+ export * from './adapter.js';
33
+ export * from './scene.js';
34
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,aAAa;AACb,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,0BAA0B,CAAC;AACzC,cAAc,yBAAyB,CAAC;AACxC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,qBAAqB,CAAC;AACpC,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC;AACxC,cAAc,wBAAwB,CAAC;AACvC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,qBAAqB,CAAC;AAEpC,sDAAsD;AACtD,cAAc,oBAAoB,CAAC;AAEnC,qCAAqC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,sBAAsB,CAAC;AACrC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,wBAAwB,CAAC;AACvC,cAAc,uBAAuB,CAAC;AAEtC,yDAAyD;AACzD,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC","sourcesContent":["/**\n * @realitycollective/webxr-uiextensions - the engine-free core.\n *\n * Everything exported here is pure TypeScript with no engine imports\n * (enforced by test/architecture.test.ts): window/dock/region/drag logic,\n * control models, the `data-uix` markup upgraders, the window chrome\n * conventions, and the platform-adapter interfaces engine packages\n * implement.\n */\n// Pure logic\nexport * from './core/events.js';\nexport * from './core/dock-state.js';\nexport * from './core/window-manager.js';\nexport * from './core/region-layout.js';\nexport * from './core/region-registry.js';\nexport * from './core/drag-math.js';\nexport * from './core/hold-to-drag.js';\nexport * from './core/stepper-model.js';\nexport * from './core/toggle-model.js';\nexport * from './core/expandable-model.js';\nexport * from './core/log-model.js';\n\n// Chrome conventions (markup ids + reference snippet)\nexport * from './chrome/markup.js';\n\n// Interface-driven control upgraders\nexport * from './controls/element.js';\nexport * from './controls/stepper.js';\nexport * from './controls/toggle.js';\nexport * from './controls/expandable-label.js';\nexport * from './controls/log-view.js';\nexport * from './controls/upgrade.js';\n\n// Platform-adapter contract + portable scene descriptors\nexport * from './adapter.js';\nexport * from './scene.js';\n"]}
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Portable scene descriptors.
3
+ *
4
+ * A scene is plain DATA - which windows exist, where they sit, which dock
5
+ * regions they can drop into - with no engine types anywhere. Each adapter
6
+ * knows how to apply the same descriptor through its own factories, so one
7
+ * playground definition drives IWSDK, XR Blocks and plain three.js alike:
8
+ *
9
+ * applyScene(iwsdkTarget, PLAYGROUND); // ECS entities
10
+ * applyScene(vanillaHost, PLAYGROUND); // three.js groups
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.
15
+ */
16
+ import type { DockModeValue } from './core/dock-state.js';
17
+ import type { RegionFlow, Vec3 } from './core/region-layout.js';
18
+ /** One window in a scene. */
19
+ export interface SceneWindow {
20
+ id: string;
21
+ title: string;
22
+ /** Path to the compiled UIKitML JSON, e.g. `./ui/clicker.json`. */
23
+ config: string;
24
+ /** World position for world-locked windows. */
25
+ position?: Vec3;
26
+ maxWidth?: number;
27
+ maxHeight?: number;
28
+ dockMode?: DockModeValue;
29
+ /** Dock straight into this region on spawn. */
30
+ region?: string;
31
+ followOffset?: Vec3;
32
+ followSpeed?: number;
33
+ followTolerance?: number;
34
+ movable?: boolean;
35
+ closable?: boolean;
36
+ minimizable?: boolean;
37
+ pinnable?: boolean;
38
+ }
39
+ /** One dock region in a scene. */
40
+ export interface SceneRegion {
41
+ id: string;
42
+ flow?: RegionFlow;
43
+ pitch?: number;
44
+ columns?: number;
45
+ capacity?: number;
46
+ snapRadius?: number;
47
+ position?: Vec3;
48
+ /** Body-lock the region so it follows the viewer. */
49
+ follow?: boolean;
50
+ followOffset?: Vec3;
51
+ }
52
+ /** A complete, engine-free scene definition. */
53
+ export interface SceneDescriptor {
54
+ name?: string;
55
+ regions?: readonly SceneRegion[];
56
+ windows: readonly SceneWindow[];
57
+ }
58
+ /**
59
+ * What an adapter must provide for {@link applyScene} to build a scene.
60
+ * Both shipped adapters implement this; a new adapter only needs these two
61
+ * methods to gain full scene portability.
62
+ */
63
+ export interface SceneTarget {
64
+ spawnRegion(region: SceneRegion): void;
65
+ spawnWindow(window: SceneWindow): void;
66
+ }
67
+ /**
68
+ * Apply a descriptor to an adapter. Regions are created before windows so a
69
+ * window that spawns docked (`region: 'x'`) always finds its region.
70
+ */
71
+ export declare function applyScene(target: SceneTarget, scene: SceneDescriptor): void;
72
+ /** Every distinct panel config path in a scene (for preloading). */
73
+ export declare function sceneConfigPaths(scene: SceneDescriptor): string[];
74
+ /**
75
+ * Validate a descriptor: unique window/region ids, and every `region`
76
+ * reference resolvable. Returns the problems found (empty = valid) rather
77
+ * than throwing, so callers can report them all at once.
78
+ */
79
+ export declare function validateScene(scene: SceneDescriptor): string[];
package/dist/scene.js ADDED
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Apply a descriptor to an adapter. Regions are created before windows so a
3
+ * window that spawns docked (`region: 'x'`) always finds its region.
4
+ */
5
+ export function applyScene(target, scene) {
6
+ for (const region of scene.regions ?? []) {
7
+ target.spawnRegion(region);
8
+ }
9
+ for (const window of scene.windows) {
10
+ target.spawnWindow(window);
11
+ }
12
+ }
13
+ /** Every distinct panel config path in a scene (for preloading). */
14
+ export function sceneConfigPaths(scene) {
15
+ return [...new Set(scene.windows.map((window) => window.config))];
16
+ }
17
+ /**
18
+ * Validate a descriptor: unique window/region ids, and every `region`
19
+ * reference resolvable. Returns the problems found (empty = valid) rather
20
+ * than throwing, so callers can report them all at once.
21
+ */
22
+ export function validateScene(scene) {
23
+ const problems = [];
24
+ const regionIds = new Set();
25
+ for (const region of scene.regions ?? []) {
26
+ if (regionIds.has(region.id)) {
27
+ problems.push(`duplicate region id "${region.id}"`);
28
+ }
29
+ regionIds.add(region.id);
30
+ }
31
+ const windowIds = new Set();
32
+ for (const window of scene.windows) {
33
+ if (windowIds.has(window.id)) {
34
+ problems.push(`duplicate window id "${window.id}"`);
35
+ }
36
+ windowIds.add(window.id);
37
+ if (window.region !== undefined && !regionIds.has(window.region)) {
38
+ problems.push(`window "${window.id}" docks into unknown region "${window.region}"`);
39
+ }
40
+ }
41
+ return problems;
42
+ }
43
+ //# sourceMappingURL=scene.js.map
@@ -0,0 +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"]}