@gadxr/ui 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/LICENSE +21 -0
  2. package/dist/Avatar.svelte +136 -0
  3. package/dist/Avatar.svelte.d.ts +13 -0
  4. package/dist/Button.svelte +29 -0
  5. package/dist/Button.svelte.d.ts +4 -0
  6. package/dist/Button2D.svelte +77 -0
  7. package/dist/Button2D.svelte.d.ts +4 -0
  8. package/dist/Button3D.svelte +98 -0
  9. package/dist/Button3D.svelte.d.ts +4 -0
  10. package/dist/ButtonGroup.svelte +59 -0
  11. package/dist/ButtonGroup.svelte.d.ts +17 -0
  12. package/dist/Checkbox.svelte +209 -0
  13. package/dist/Checkbox.svelte.d.ts +14 -0
  14. package/dist/DimensionScope.svelte +7 -0
  15. package/dist/DimensionScope.svelte.d.ts +9 -0
  16. package/dist/Input.svelte +293 -0
  17. package/dist/Input.svelte.d.ts +23 -0
  18. package/dist/Key3D.svelte +67 -0
  19. package/dist/Key3D.svelte.d.ts +13 -0
  20. package/dist/Keyboard.svelte +167 -0
  21. package/dist/Keyboard.svelte.d.ts +9 -0
  22. package/dist/Menu.svelte +193 -0
  23. package/dist/Menu.svelte.d.ts +23 -0
  24. package/dist/Modal.svelte +103 -0
  25. package/dist/Modal.svelte.d.ts +16 -0
  26. package/dist/Panel.svelte +132 -0
  27. package/dist/Panel.svelte.d.ts +19 -0
  28. package/dist/Pressable3D.svelte +82 -0
  29. package/dist/Pressable3D.svelte.d.ts +28 -0
  30. package/dist/ProgressBar.svelte +153 -0
  31. package/dist/ProgressBar.svelte.d.ts +16 -0
  32. package/dist/RadialMenu.svelte +322 -0
  33. package/dist/RadialMenu.svelte.d.ts +28 -0
  34. package/dist/Rule3D.svelte +17 -0
  35. package/dist/Rule3D.svelte.d.ts +9 -0
  36. package/dist/Scene3D.svelte +24 -0
  37. package/dist/Scene3D.svelte.d.ts +11 -0
  38. package/dist/Slider.svelte +209 -0
  39. package/dist/Slider.svelte.d.ts +17 -0
  40. package/dist/Space3D.svelte +6 -0
  41. package/dist/Space3D.svelte.d.ts +7 -0
  42. package/dist/Spinner.svelte +64 -0
  43. package/dist/Spinner.svelte.d.ts +10 -0
  44. package/dist/Stack.svelte +41 -0
  45. package/dist/Stack.svelte.d.ts +15 -0
  46. package/dist/StackScope.svelte +7 -0
  47. package/dist/StackScope.svelte.d.ts +9 -0
  48. package/dist/Surface.svelte +152 -0
  49. package/dist/Surface.svelte.d.ts +30 -0
  50. package/dist/Tab.svelte +40 -0
  51. package/dist/Tab.svelte.d.ts +10 -0
  52. package/dist/Tabs.svelte +145 -0
  53. package/dist/Tabs.svelte.d.ts +13 -0
  54. package/dist/Tag.svelte +71 -0
  55. package/dist/Tag.svelte.d.ts +12 -0
  56. package/dist/Text.svelte +75 -0
  57. package/dist/Text.svelte.d.ts +4 -0
  58. package/dist/TextLine3D.svelte +84 -0
  59. package/dist/TextLine3D.svelte.d.ts +19 -0
  60. package/dist/ThemeProvider.svelte +7 -0
  61. package/dist/ThemeProvider.svelte.d.ts +9 -0
  62. package/dist/Toaster.svelte +104 -0
  63. package/dist/Toaster.svelte.d.ts +11 -0
  64. package/dist/Tooltip.svelte +174 -0
  65. package/dist/Tooltip.svelte.d.ts +14 -0
  66. package/dist/aframe/frame.d.ts +1 -0
  67. package/dist/aframe/frame.js +37 -0
  68. package/dist/aframe/index.d.ts +1 -0
  69. package/dist/aframe/index.js +19 -0
  70. package/dist/aframe/measure.d.ts +3 -0
  71. package/dist/aframe/measure.js +42 -0
  72. package/dist/aframe/placement.d.ts +88 -0
  73. package/dist/aframe/placement.js +163 -0
  74. package/dist/aframe/rounded.d.ts +61 -0
  75. package/dist/aframe/rounded.js +121 -0
  76. package/dist/aframe/scene.d.ts +9 -0
  77. package/dist/aframe/scene.js +101 -0
  78. package/dist/assets/Geist-OFL.txt +93 -0
  79. package/dist/assets/Geist-Regular.ttf +0 -0
  80. package/dist/button-style.d.ts +22 -0
  81. package/dist/button-style.js +34 -0
  82. package/dist/component-style.d.ts +79 -0
  83. package/dist/component-style.js +129 -0
  84. package/dist/context.d.ts +16 -0
  85. package/dist/context.js +26 -0
  86. package/dist/fonts.css +7 -0
  87. package/dist/fonts.d.ts +2 -0
  88. package/dist/fonts.js +7 -0
  89. package/dist/index.d.ts +40 -0
  90. package/dist/index.js +40 -0
  91. package/dist/keyboard-layout.d.ts +28 -0
  92. package/dist/keyboard-layout.js +77 -0
  93. package/dist/layout-math.d.ts +20 -0
  94. package/dist/layout-math.js +22 -0
  95. package/dist/layout.svelte.d.ts +17 -0
  96. package/dist/layout.svelte.js +58 -0
  97. package/dist/radial-math.d.ts +33 -0
  98. package/dist/radial-math.js +90 -0
  99. package/dist/tabs-context.d.ts +13 -0
  100. package/dist/tabs-context.js +6 -0
  101. package/dist/text-edit.d.ts +46 -0
  102. package/dist/text-edit.js +133 -0
  103. package/dist/text-focus.svelte.d.ts +24 -0
  104. package/dist/text-focus.svelte.js +102 -0
  105. package/dist/theme.d.ts +335 -0
  106. package/dist/theme.js +312 -0
  107. package/dist/toast.svelte.d.ts +24 -0
  108. package/dist/toast.svelte.js +30 -0
  109. package/dist/types.d.ts +25 -0
  110. package/dist/types.js +1 -0
  111. package/dist/xr.svelte.d.ts +9 -0
  112. package/dist/xr.svelte.js +24 -0
  113. package/package.json +44 -3
  114. package/README.md +0 -3
@@ -0,0 +1,209 @@
1
+ <script lang="ts">// Base UI-style Slider (single value). 2D is a native <input type="range">.
2
+ // 3D: press on the track or thumb, then the value follows the cursor ray where it
3
+ // crosses the track's plane until release — so the drag survives drifting off the
4
+ // track, and works the same for the mouse cursor and a VR laser.
5
+ import { ensure3d } from "./aframe";
6
+ import { everyFrame } from "./aframe/frame";
7
+ import { sliderFraction, sliderValueAt } from "./component-style";
8
+ import { getDimension, getTheme } from "./context";
9
+ import { setStack, useSlot } from "./layout.svelte";
10
+ import Text from "./Text.svelte";
11
+ const theme = getTheme();
12
+ let { value = $bindable(0), min = 0, max = 100, step = 1, label = "", showValue = true, disabled = false, width = theme().slider.width, onchange, dimension, style: userStyle = "", ...rest } = $props();
13
+ const t = $derived(theme());
14
+ const c = $derived(t.colors.slider);
15
+ const fraction = $derived(sliderFraction(value, min, max));
16
+ const font = $derived(t.typography[t.slider.typography]);
17
+ const hasHeader = $derived(!!label || showValue);
18
+ const contextDimension = getDimension();
19
+ // svelte-ignore state_referenced_locally
20
+ const is3d = (dimension ?? contextDimension) === "3d";
21
+ function setValue(next) {
22
+ if (next === value) return;
23
+ value = next;
24
+ onchange?.(value);
25
+ }
26
+ // ---- 3D ----
27
+ const m = (px) => px * t.space3d.metersPerPx;
28
+ if (is3d) ensure3d();
29
+ const W = $derived(m(width));
30
+ const trackH = $derived(m(t.slider.trackHeight));
31
+ const thumb = $derived(m(t.slider.thumb));
32
+ const headerH = $derived(hasHeader ? m(font.lineHeight) * t.space3d.textScale : 0);
33
+ const headerGap = $derived(hasHeader ? m(t.slider.gap) : 0);
34
+ const size = $derived({
35
+ width: W + thumb,
36
+ height: headerH + headerGap + thumb
37
+ });
38
+ const slot = is3d ? useSlot(() => size) : () => ({
39
+ x: 0,
40
+ y: 0
41
+ });
42
+ // The header Text inside is placed by hand, not by a parent Panel's stack.
43
+ setStack(undefined);
44
+ // Track runs through y = trackY; the header sits above it.
45
+ const trackY = $derived(-size.height / 2 + thumb / 2);
46
+ const headerY = $derived(size.height / 2 - headerH / 2);
47
+ const depth = .01;
48
+ const disabledColor = $derived(t.colors.buttonDisabledText);
49
+ let trackEl = $state();
50
+ let dragging = $state(false);
51
+ let hovered = $state(false);
52
+ const thumbScale = $derived((hovered || dragging) && !disabled ? 1.2 : 1);
53
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
54
+ function valueFromRay(ray) {
55
+ const THREE = globalThis.AFRAME.THREE;
56
+ const obj = trackEl.object3D;
57
+ const normal = new THREE.Vector3(0, 0, 1).transformDirection(obj.matrixWorld);
58
+ const plane = new THREE.Plane().setFromNormalAndCoplanarPoint(normal, obj.getWorldPosition(new THREE.Vector3()));
59
+ const hit = ray.intersectPlane(plane, new THREE.Vector3());
60
+ if (!hit) return;
61
+ const x = obj.worldToLocal(hit).x;
62
+ setValue(sliderValueAt((x + W / 2) / W, min, max, step));
63
+ }
64
+ function startDrag(event) {
65
+ const detail = event.detail;
66
+ const cursorEl = detail?.cursorEl;
67
+ const raycaster = cursorEl?.components?.raycaster;
68
+ if (disabled || !raycaster || !trackEl) return;
69
+ dragging = true;
70
+ valueFromRay(raycaster.raycaster.ray);
71
+ // Not window.requestAnimationFrame: that's paused in an immersive session.
72
+ const stopFollowing = everyFrame(cursorEl.sceneEl, () => valueFromRay(raycaster.raycaster.ray));
73
+ const stop = () => {
74
+ dragging = false;
75
+ stopFollowing();
76
+ cursorEl.removeEventListener("mouseup", stop);
77
+ window.removeEventListener("mouseup", stop);
78
+ };
79
+ // The cursor emits mouseup on itself; the window listener covers a mouse released off-canvas.
80
+ cursorEl.addEventListener("mouseup", stop);
81
+ window.addEventListener("mouseup", stop);
82
+ }
83
+ </script>
84
+
85
+ {#if is3d}
86
+ <!-- svelte-ignore a11y_no_static_element_interactions, a11y_mouse_events_have_key_events -->
87
+ <a-entity
88
+ {...rest}
89
+ onmousedown={startDrag}
90
+ onmouseenter={() => (hovered = true)}
91
+ onmouseleave={() => (hovered = false)}
92
+ >
93
+ <a-entity position="{slot().x} {slot().y} 0">
94
+ {#if hasHeader}
95
+ <a-entity position="0 {headerY} 0">
96
+ {#if label}
97
+ <Text variant={t.slider.typography} text={label} align="left" color={disabled ? disabledColor : undefined} position="{-W / 2} 0 0" />
98
+ {/if}
99
+ {#if showValue}
100
+ <Text variant={t.slider.typography} text={String(value)} align="right" color={t.colors.contentSecondary} position="{W / 2} 0 0" />
101
+ {/if}
102
+ </a-entity>
103
+ {/if}
104
+ <a-entity bind:this={trackEl} position="0 {trackY} 0">
105
+ <!-- Hit area: the track plus the thumb's reach, a bit taller than the thumb. -->
106
+ <a-entity
107
+ class={disabled ? '' : 'raycastable'}
108
+ geometry="primitive: plane; width: {W + thumb}; height: {thumb * 1.6}"
109
+ material="visible: false"
110
+ position="0 0 {depth + 0.004}"
111
+ ></a-entity>
112
+ <a-entity
113
+ rounded="width: {W}; height: {trackH}; depth: {depth}; radius: {trackH / 2}; color: {c.track}"
114
+ position="{-W / 2} {-trackH / 2} 0"
115
+ ></a-entity>
116
+ <a-entity
117
+ rounded="width: {Math.max(W * fraction, 0.0001)}; height: {trackH}; depth: {depth}; radius: {trackH / 2}; color: {disabled ? disabledColor : c.fill}; enabled: {fraction > 0}"
118
+ position="{-W / 2} {-trackH / 2} 0.001"
119
+ ></a-entity>
120
+ <a-entity
121
+ geometry="primitive: cylinder; radius: {thumb / 2}; height: 0.008; segmentsRadial: 32"
122
+ material="color: {disabled ? disabledColor : c.thumb}; roughness: 0.4"
123
+ rotation="90 0 0"
124
+ position="{-W / 2 + W * fraction} 0 {depth + t.space3d.raise / 2}"
125
+ scale="{thumbScale} 1 {thumbScale}"
126
+ ></a-entity>
127
+ </a-entity>
128
+ </a-entity>
129
+ </a-entity>
130
+ {:else}
131
+ <label
132
+ class="ui-slider"
133
+ class:disabled
134
+ style="--ui-width: {width}px; --ui-track: {c.track}; --ui-fill: {disabled ? t.colors.buttonDisabledFill : c.fill}; --ui-thumb: {disabled ? disabledColor : c.thumb}; --ui-thumb-size: {t.slider.thumb}px; --ui-track-height: {t.slider.trackHeight}px; --ui-pct: {fraction * 100}%; --ui-gap: {t.slider.gap}px; --ui-focus: {t.colors.accent}; --ui-font: {font.fontWeight} {font.fontSize}px/{font.lineHeight}px {t.typography.fontFamily}; --ui-label: {disabled ? disabledColor : t.colors.contentPrimary}; --ui-value: {t.colors.contentSecondary}; {userStyle}"
135
+ {...rest}
136
+ >
137
+ {#if hasHeader}
138
+ <span class="header">
139
+ <span>{label}</span>
140
+ {#if showValue}<span class="value">{value}</span>{/if}
141
+ </span>
142
+ {/if}
143
+ <input type="range" {min} {max} {step} {disabled} bind:value oninput={() => onchange?.(value)} />
144
+ </label>
145
+ {/if}
146
+
147
+ <style>
148
+ .ui-slider {
149
+ display: inline-flex;
150
+ flex-direction: column;
151
+ gap: var(--ui-gap);
152
+ width: var(--ui-width);
153
+ font: var(--ui-font);
154
+ color: var(--ui-label);
155
+ }
156
+ .header {
157
+ display: flex;
158
+ justify-content: space-between;
159
+ }
160
+ .value {
161
+ color: var(--ui-value);
162
+ font-variant-numeric: tabular-nums;
163
+ }
164
+ input {
165
+ appearance: none;
166
+ width: 100%;
167
+ height: var(--ui-thumb-size);
168
+ margin: 0;
169
+ background: transparent;
170
+ cursor: pointer;
171
+ }
172
+ .disabled input {
173
+ cursor: not-allowed;
174
+ }
175
+ input::-webkit-slider-runnable-track {
176
+ height: var(--ui-track-height);
177
+ border-radius: var(--ui-track-height);
178
+ background: linear-gradient(to right, var(--ui-fill) var(--ui-pct), var(--ui-track) var(--ui-pct));
179
+ }
180
+ input::-moz-range-track {
181
+ height: var(--ui-track-height);
182
+ border-radius: var(--ui-track-height);
183
+ background: linear-gradient(to right, var(--ui-fill) var(--ui-pct), var(--ui-track) var(--ui-pct));
184
+ }
185
+ input::-webkit-slider-thumb {
186
+ appearance: none;
187
+ width: var(--ui-thumb-size);
188
+ height: var(--ui-thumb-size);
189
+ margin-top: calc((var(--ui-track-height) - var(--ui-thumb-size)) / 2);
190
+ border-radius: 50%;
191
+ background: var(--ui-thumb);
192
+ border: 0;
193
+ }
194
+ input::-moz-range-thumb {
195
+ width: var(--ui-thumb-size);
196
+ height: var(--ui-thumb-size);
197
+ border-radius: 50%;
198
+ background: var(--ui-thumb);
199
+ border: 0;
200
+ }
201
+ input:focus-visible::-webkit-slider-thumb {
202
+ outline: 3px solid var(--ui-focus);
203
+ outline-offset: 2px;
204
+ }
205
+ input:focus-visible::-moz-range-thumb {
206
+ outline: 3px solid var(--ui-focus);
207
+ outline-offset: 2px;
208
+ }
209
+ </style>
@@ -0,0 +1,17 @@
1
+ import type { Dimension } from './theme';
2
+ type Props = {
3
+ value?: number;
4
+ min?: number;
5
+ max?: number;
6
+ step?: number;
7
+ label?: string;
8
+ showValue?: boolean;
9
+ disabled?: boolean;
10
+ width?: number;
11
+ onchange?: (value: number) => void;
12
+ dimension?: Dimension;
13
+ [attribute: string]: unknown;
14
+ };
15
+ declare const Slider: import("svelte").Component<Props, {}, "value">;
16
+ type Slider = ReturnType<typeof Slider>;
17
+ export default Slider;
@@ -0,0 +1,6 @@
1
+ <script lang="ts">import { setDimension } from "./context";
2
+ let { children } = $props();
3
+ setDimension("3d");
4
+ </script>
5
+
6
+ {@render children()}
@@ -0,0 +1,7 @@
1
+ import type { Snippet } from 'svelte';
2
+ type $$ComponentProps = {
3
+ children: Snippet;
4
+ };
5
+ declare const Space3D: import("svelte").Component<$$ComponentProps, {}, "">;
6
+ type Space3D = ReturnType<typeof Space3D>;
7
+ export default Space3D;
@@ -0,0 +1,64 @@
1
+ <script lang="ts">// Base UI-style Spinner: a rotating arc on a faint ring. 3D: a torus arc spinning in place.
2
+ import { ensure3d } from "./aframe";
3
+ import { getDimension, getTheme } from "./context";
4
+ import { useSlot } from "./layout.svelte";
5
+ let { size = "medium", color, dimension, style: userStyle = "", ...rest } = $props();
6
+ const theme = getTheme();
7
+ const t = $derived(theme());
8
+ const px = $derived(t.spinner.sizes[size]);
9
+ const thickness = $derived(Math.max(2, px * t.spinner.thickness));
10
+ const active = $derived(color ?? t.colors.spinner.active);
11
+ const contextDimension = getDimension();
12
+ // svelte-ignore state_referenced_locally
13
+ const is3d = (dimension ?? contextDimension) === "3d";
14
+ // ---- 3D ----
15
+ const m = (v) => v * t.space3d.metersPerPx;
16
+ if (is3d) ensure3d();
17
+ const d = $derived(m(px));
18
+ const slot = is3d ? useSlot(() => ({
19
+ width: d,
20
+ height: d
21
+ })) : () => ({
22
+ x: 0,
23
+ y: 0
24
+ });
25
+ const torus = $derived(`radius: ${(d - m(thickness)) / 2}; radiusTubular: ${m(thickness) / 2}; segmentsTubular: 48`);
26
+ const spin = $derived(`property: rotation; from: 0 0 0; to: 0 0 -360; dur: ${t.animation.timing500 * 2}; loop: true; easing: linear`);
27
+ </script>
28
+
29
+ {#if is3d}
30
+ <a-entity {...rest}>
31
+ <a-entity position="{slot().x} {slot().y} 0">
32
+ <a-entity geometry="primitive: torus; {torus}" material="color: {t.colors.spinner.track}; roughness: 0.6"></a-entity>
33
+ <a-entity
34
+ geometry="primitive: torus; {torus}; arc: 90"
35
+ material="color: {active}; emissive: {active}; emissiveIntensity: 0.4"
36
+ position="0 0 0.002"
37
+ animation__spin={spin}
38
+ ></a-entity>
39
+ </a-entity>
40
+ </a-entity>
41
+ {:else}
42
+ <span
43
+ class="ui-spinner"
44
+ role="progressbar"
45
+ aria-label="Loading"
46
+ style="width: {px}px; height: {px}px; border-width: {thickness}px; border-color: {t.colors.spinner.track}; border-top-color: {active}; {userStyle}"
47
+ {...rest}
48
+ ></span>
49
+ {/if}
50
+
51
+ <style>
52
+ .ui-spinner {
53
+ display: inline-block;
54
+ box-sizing: border-box;
55
+ border-style: solid;
56
+ border-radius: 50%;
57
+ animation: ui-spin 1s linear infinite;
58
+ }
59
+ @keyframes ui-spin {
60
+ to {
61
+ transform: rotate(360deg);
62
+ }
63
+ }
64
+ </style>
@@ -0,0 +1,10 @@
1
+ import type { Dimension, SpinnerSize } from './theme';
2
+ type Props = {
3
+ size?: SpinnerSize;
4
+ color?: string;
5
+ dimension?: Dimension;
6
+ [attribute: string]: unknown;
7
+ };
8
+ declare const Spinner: import("svelte").Component<Props, {}, "">;
9
+ type Spinner = ReturnType<typeof Spinner>;
10
+ export default Spinner;
@@ -0,0 +1,41 @@
1
+ <script lang="ts">import { getDimension, getTheme } from "./context";
2
+ import { createStack, setStack, useSlot } from "./layout.svelte";
3
+ const theme = getTheme();
4
+ let { children, direction = "row", gap = theme().sizing.scale300, align = "center", alignSelf, dimension, style: userStyle = "", ...rest } = $props();
5
+ const contextDimension = getDimension();
6
+ // svelte-ignore state_referenced_locally
7
+ const is3d = (dimension ?? contextDimension) === "3d";
8
+ const stack = is3d ? createStack(() => ({
9
+ direction,
10
+ gap: gap * theme().space3d.metersPerPx,
11
+ align
12
+ })) : undefined;
13
+ const slot = is3d ? useSlot(() => ({
14
+ ...stack.size,
15
+ align: alignSelf
16
+ })) : () => ({
17
+ x: 0,
18
+ y: 0
19
+ });
20
+ setStack(stack);
21
+ const flex = {
22
+ start: "flex-start",
23
+ center: "center",
24
+ end: "flex-end"
25
+ };
26
+ </script>
27
+
28
+ {#if is3d}
29
+ <a-entity {...rest}>
30
+ <a-entity position="{slot().x} {slot().y} 0">{@render children()}</a-entity>
31
+ </a-entity>
32
+ {:else}
33
+ <div
34
+ style="display: inline-flex; flex-direction: {direction}; gap: {gap}px; align-items: {flex[align]}; {alignSelf
35
+ ? `align-self: ${flex[alignSelf]};`
36
+ : ''} {userStyle}"
37
+ {...rest}
38
+ >
39
+ {@render children()}
40
+ </div>
41
+ {/if}
@@ -0,0 +1,15 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { Align } from './layout-math';
3
+ import type { Dimension } from './theme';
4
+ type Props = {
5
+ children: Snippet;
6
+ direction?: 'row' | 'column';
7
+ gap?: number;
8
+ align?: Align;
9
+ alignSelf?: Align;
10
+ dimension?: Dimension;
11
+ [attribute: string]: unknown;
12
+ };
13
+ declare const Stack: import("svelte").Component<Props, {}, "">;
14
+ type Stack = ReturnType<typeof Stack>;
15
+ export default Stack;
@@ -0,0 +1,7 @@
1
+ <script lang="ts">import { setStack } from "./layout.svelte";
2
+ let { stack, children } = $props();
3
+ // svelte-ignore state_referenced_locally
4
+ setStack(stack);
5
+ </script>
6
+
7
+ {@render children()}
@@ -0,0 +1,9 @@
1
+ import type { Snippet } from 'svelte';
2
+ import { type Stack } from './layout.svelte';
3
+ type $$ComponentProps = {
4
+ stack?: Stack;
5
+ children: Snippet;
6
+ };
7
+ declare const StackScope: import("svelte").Component<$$ComponentProps, {}, "">;
8
+ type StackScope = ReturnType<typeof StackScope>;
9
+ export default StackScope;
@@ -0,0 +1,152 @@
1
+ <script lang="ts" module>// One fixed region per screen spot, shared by every Surface placed there (they stack).
2
+ function region(screen) {
3
+ let el = document.querySelector(`.ui-surface-region[data-screen="${screen}"]`);
4
+ if (!el) {
5
+ el = document.createElement("div");
6
+ el.className = "ui-surface-region";
7
+ el.dataset.screen = screen;
8
+ document.body.appendChild(el);
9
+ }
10
+ return el;
11
+ }
12
+ // Moves the 2D content out of the <a-scene> into its screen region (or a target element).
13
+ function portal(node, where) {
14
+ const place = ({ screen, target }) => (target && document.querySelector(target) || region(screen)).appendChild(node);
15
+ place(where);
16
+ return {
17
+ update: place,
18
+ destroy: () => node.remove()
19
+ };
20
+ }
21
+ export {};
22
+ </script>
23
+
24
+ <script lang="ts">import { untrack } from "svelte";
25
+ import { fly } from "svelte/transition";
26
+ import { ensure3d } from "./aframe";
27
+ import { surfacePlacement } from "./aframe/placement";
28
+ import { getTheme } from "./context";
29
+ import DimensionScope from "./DimensionScope.svelte";
30
+ import StackScope from "./StackScope.svelte";
31
+ import { xr } from "./xr.svelte";
32
+ let { children, vr = "follow", desktop = "screen", screen = "bottom", target, mode = "auto", open = true, camera = "[camera]", offset = "0 -0.5 -1", distance = 1.1, height = -.25, side = 0, maxAngle = 30, hand = "#leftHand", handPosition = "0 0.06 -0.15", handRotation = "-60 0 0", scale, desktopScale = 1, depthScale, ...rest } = $props();
33
+ ensure3d();
34
+ const theme = getTheme();
35
+ let anchor = $state();
36
+ $effect(() => {
37
+ if (anchor) xr.watch(anchor.sceneEl ?? anchor.closest("a-scene"));
38
+ });
39
+ const placement = $derived(mode === "2d" ? "screen" : surfacePlacement({
40
+ vr,
41
+ desktop
42
+ }, {
43
+ immersive: xr.immersive || mode === "3d",
44
+ presenting: xr.presenting
45
+ }));
46
+ const is3d = $derived(placement !== "screen");
47
+ const size = $derived(placement === "camera" ? desktopScale : scale ?? (vr === "hand" ? .225 : .45));
48
+ const depth = $derived(size * (depthScale ?? (vr === "hand" && placement !== "camera" ? 2 : 1)));
49
+ // Simulated wrist menus sit off to the side instead of dead ahead.
50
+ const followSide = $derived(vr === "hand" && placement === "follow" ? side || -.45 : side);
51
+ const followAttr = $derived(`distance: ${distance}; height: ${height}; side: ${followSide}; maxAngle: ${maxAngle}`);
52
+ // 3D open/close: slide up into place / down out of view (meters), then unmount.
53
+ const slideMs = $derived(theme().animation.timing500);
54
+ const drop = $derived(placement === "camera" ? .5 : .3);
55
+ let shown = $state(untrack(() => open));
56
+ $effect(() => {
57
+ if (open) return void (shown = true);
58
+ if (!untrack(() => shown)) return;
59
+ const timer = setTimeout(() => shown = false, slideMs);
60
+ return () => clearTimeout(timer);
61
+ });
62
+ const slideAttr = $derived(`property: position; to: 0 ${open ? 0 : -drop} 0; dur: ${slideMs}; easing: ${open ? "easeOutCubic" : "easeInCubic"}`);
63
+ </script>
64
+
65
+ <a-entity bind:this={anchor} class="ui-surface-anchor">
66
+ {#if is3d}
67
+ {#if shown}
68
+ <!-- Recreated per placement, so each starts from a clean transform. -->
69
+ {#key placement}
70
+ {#if placement === 'world'}
71
+ <a-entity {...rest}>{@render content3d()}</a-entity>
72
+ {:else if placement === 'follow'}
73
+ <a-entity ui-follow={followAttr}>{@render content3d()}</a-entity>
74
+ {:else if placement === 'camera'}
75
+ <a-entity ui-attach="target: {camera}; position: {offset}; rotation: 0 0 0">{@render content3d()}</a-entity>
76
+ {:else}
77
+ <a-entity ui-attach="target: {hand}; position: {handPosition}; rotation: {handRotation}">
78
+ {@render content3d()}
79
+ </a-entity>
80
+ {/if}
81
+ {/key}
82
+ {/if}
83
+ {:else if open}
84
+ <div class="ui-surface" use:portal={{ screen, target }} transition:fly={{ y: 24, duration: slideMs }}>
85
+ <DimensionScope dimension="2d"><StackScope>{@render children()}</StackScope></DimensionScope>
86
+ </div>
87
+ {/if}
88
+ </a-entity>
89
+
90
+ {#snippet content3d()}
91
+ <!-- Starts below and slides up; `open` sends it back down. -->
92
+ <a-entity position="0 {-drop} 0" animation__slide={slideAttr}>
93
+ <a-entity scale="{size} {size} {depth}">
94
+ <DimensionScope dimension="3d"><StackScope>{@render children()}</StackScope></DimensionScope>
95
+ </a-entity>
96
+ </a-entity>
97
+ {/snippet}
98
+
99
+ <style>
100
+ :global(.ui-surface-region) {
101
+ position: fixed;
102
+ z-index: 900;
103
+ display: flex;
104
+ flex-direction: column;
105
+ gap: 12px;
106
+ padding: 16px;
107
+ pointer-events: none;
108
+ max-width: calc(100vw - 32px);
109
+ max-height: calc(100vh - 32px);
110
+ box-sizing: border-box;
111
+ }
112
+ :global(.ui-surface-region > *) {
113
+ pointer-events: auto;
114
+ }
115
+ :global(.ui-surface-region[data-screen^='top']) {
116
+ top: 0;
117
+ }
118
+ :global(.ui-surface-region[data-screen^='bottom']) {
119
+ bottom: 0;
120
+ flex-direction: column-reverse;
121
+ }
122
+ :global(.ui-surface-region[data-screen$='left']) {
123
+ left: 0;
124
+ align-items: flex-start;
125
+ }
126
+ :global(.ui-surface-region[data-screen$='right']) {
127
+ right: 0;
128
+ align-items: flex-end;
129
+ }
130
+ :global(.ui-surface-region[data-screen='top']),
131
+ :global(.ui-surface-region[data-screen='bottom']),
132
+ :global(.ui-surface-region[data-screen='center']) {
133
+ left: 50%;
134
+ transform: translateX(-50%);
135
+ align-items: center;
136
+ }
137
+ :global(.ui-surface-region[data-screen='left']),
138
+ :global(.ui-surface-region[data-screen='right']),
139
+ :global(.ui-surface-region[data-screen='center']) {
140
+ top: 50%;
141
+ transform: translateY(-50%);
142
+ }
143
+ :global(.ui-surface-region[data-screen='center']) {
144
+ transform: translate(-50%, -50%);
145
+ }
146
+ .ui-surface {
147
+ display: flex;
148
+ flex-direction: column;
149
+ align-items: inherit;
150
+ gap: inherit;
151
+ }
152
+ </style>
@@ -0,0 +1,30 @@
1
+ export type SurfacePlacement = 'world' | 'follow' | 'hand';
2
+ export type SurfaceDesktopPlacement = 'screen' | 'camera';
3
+ export type ScreenRegion = 'top-left' | 'top' | 'top-right' | 'left' | 'center' | 'right' | 'bottom-left' | 'bottom' | 'bottom-right';
4
+ import type { Snippet } from 'svelte';
5
+ import { type DesktopPlacement, type VRPlacement } from './aframe/placement';
6
+ type Props = {
7
+ children: Snippet;
8
+ vr?: VRPlacement;
9
+ desktop?: DesktopPlacement;
10
+ screen?: ScreenRegion;
11
+ target?: string;
12
+ mode?: 'auto' | '2d' | '3d';
13
+ open?: boolean;
14
+ camera?: string;
15
+ offset?: string;
16
+ distance?: number;
17
+ height?: number;
18
+ side?: number;
19
+ maxAngle?: number;
20
+ hand?: string;
21
+ handPosition?: string;
22
+ handRotation?: string;
23
+ scale?: number;
24
+ desktopScale?: number;
25
+ depthScale?: number;
26
+ [attribute: string]: unknown;
27
+ };
28
+ declare const Surface: import("svelte").Component<Props, {}, "">;
29
+ type Surface = ReturnType<typeof Surface>;
30
+ export default Surface;
@@ -0,0 +1,40 @@
1
+ <script lang="ts">import { getDimension } from "./context";
2
+ import { getTabs } from "./tabs-context";
3
+ let { key, title, disabled = false, children } = $props();
4
+ const tabs = getTabs();
5
+ if (!tabs) throw new Error("<Tab> must be inside <Tabs>");
6
+ const entry = {
7
+ get key() {
8
+ return key;
9
+ },
10
+ get title() {
11
+ return title;
12
+ },
13
+ get disabled() {
14
+ return disabled;
15
+ }
16
+ };
17
+ tabs.register(entry);
18
+ $effect(() => () => tabs.unregister(entry));
19
+ const is3d = getDimension() === "3d";
20
+ const active = $derived(tabs.active === key);
21
+ </script>
22
+
23
+ {#if active}
24
+ {#if is3d}
25
+ {@render children?.()}
26
+ {:else}
27
+ <div class="ui-tab-panel" role="tabpanel" id="{tabs.id}-panel-{key}" aria-labelledby="{tabs.id}-tab-{key}">
28
+ {@render children?.()}
29
+ </div>
30
+ {/if}
31
+ {/if}
32
+
33
+ <style>
34
+ .ui-tab-panel {
35
+ display: flex;
36
+ flex-direction: column;
37
+ align-items: flex-start;
38
+ gap: 12px;
39
+ }
40
+ </style>
@@ -0,0 +1,10 @@
1
+ import type { Snippet } from 'svelte';
2
+ type Props = {
3
+ key: string;
4
+ title: string;
5
+ disabled?: boolean;
6
+ children?: Snippet;
7
+ };
8
+ declare const Tab: import("svelte").Component<Props, {}, "">;
9
+ type Tab = ReturnType<typeof Tab>;
10
+ export default Tab;