@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.
- package/LICENSE +21 -0
- package/dist/Avatar.svelte +136 -0
- package/dist/Avatar.svelte.d.ts +13 -0
- package/dist/Button.svelte +29 -0
- package/dist/Button.svelte.d.ts +4 -0
- package/dist/Button2D.svelte +77 -0
- package/dist/Button2D.svelte.d.ts +4 -0
- package/dist/Button3D.svelte +98 -0
- package/dist/Button3D.svelte.d.ts +4 -0
- package/dist/ButtonGroup.svelte +59 -0
- package/dist/ButtonGroup.svelte.d.ts +17 -0
- package/dist/Checkbox.svelte +209 -0
- package/dist/Checkbox.svelte.d.ts +14 -0
- package/dist/DimensionScope.svelte +7 -0
- package/dist/DimensionScope.svelte.d.ts +9 -0
- package/dist/Input.svelte +293 -0
- package/dist/Input.svelte.d.ts +23 -0
- package/dist/Key3D.svelte +67 -0
- package/dist/Key3D.svelte.d.ts +13 -0
- package/dist/Keyboard.svelte +167 -0
- package/dist/Keyboard.svelte.d.ts +9 -0
- package/dist/Menu.svelte +193 -0
- package/dist/Menu.svelte.d.ts +23 -0
- package/dist/Modal.svelte +103 -0
- package/dist/Modal.svelte.d.ts +16 -0
- package/dist/Panel.svelte +132 -0
- package/dist/Panel.svelte.d.ts +19 -0
- package/dist/Pressable3D.svelte +82 -0
- package/dist/Pressable3D.svelte.d.ts +28 -0
- package/dist/ProgressBar.svelte +153 -0
- package/dist/ProgressBar.svelte.d.ts +16 -0
- package/dist/RadialMenu.svelte +322 -0
- package/dist/RadialMenu.svelte.d.ts +28 -0
- package/dist/Rule3D.svelte +17 -0
- package/dist/Rule3D.svelte.d.ts +9 -0
- package/dist/Scene3D.svelte +24 -0
- package/dist/Scene3D.svelte.d.ts +11 -0
- package/dist/Slider.svelte +209 -0
- package/dist/Slider.svelte.d.ts +17 -0
- package/dist/Space3D.svelte +6 -0
- package/dist/Space3D.svelte.d.ts +7 -0
- package/dist/Spinner.svelte +64 -0
- package/dist/Spinner.svelte.d.ts +10 -0
- package/dist/Stack.svelte +41 -0
- package/dist/Stack.svelte.d.ts +15 -0
- package/dist/StackScope.svelte +7 -0
- package/dist/StackScope.svelte.d.ts +9 -0
- package/dist/Surface.svelte +152 -0
- package/dist/Surface.svelte.d.ts +30 -0
- package/dist/Tab.svelte +40 -0
- package/dist/Tab.svelte.d.ts +10 -0
- package/dist/Tabs.svelte +145 -0
- package/dist/Tabs.svelte.d.ts +13 -0
- package/dist/Tag.svelte +71 -0
- package/dist/Tag.svelte.d.ts +12 -0
- package/dist/Text.svelte +75 -0
- package/dist/Text.svelte.d.ts +4 -0
- package/dist/TextLine3D.svelte +84 -0
- package/dist/TextLine3D.svelte.d.ts +19 -0
- package/dist/ThemeProvider.svelte +7 -0
- package/dist/ThemeProvider.svelte.d.ts +9 -0
- package/dist/Toaster.svelte +104 -0
- package/dist/Toaster.svelte.d.ts +11 -0
- package/dist/Tooltip.svelte +174 -0
- package/dist/Tooltip.svelte.d.ts +14 -0
- package/dist/aframe/frame.d.ts +1 -0
- package/dist/aframe/frame.js +37 -0
- package/dist/aframe/index.d.ts +1 -0
- package/dist/aframe/index.js +19 -0
- package/dist/aframe/measure.d.ts +3 -0
- package/dist/aframe/measure.js +42 -0
- package/dist/aframe/placement.d.ts +88 -0
- package/dist/aframe/placement.js +163 -0
- package/dist/aframe/rounded.d.ts +61 -0
- package/dist/aframe/rounded.js +121 -0
- package/dist/aframe/scene.d.ts +9 -0
- package/dist/aframe/scene.js +101 -0
- package/dist/assets/Geist-OFL.txt +93 -0
- package/dist/assets/Geist-Regular.ttf +0 -0
- package/dist/button-style.d.ts +22 -0
- package/dist/button-style.js +34 -0
- package/dist/component-style.d.ts +79 -0
- package/dist/component-style.js +129 -0
- package/dist/context.d.ts +16 -0
- package/dist/context.js +26 -0
- package/dist/fonts.css +7 -0
- package/dist/fonts.d.ts +2 -0
- package/dist/fonts.js +7 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.js +40 -0
- package/dist/keyboard-layout.d.ts +28 -0
- package/dist/keyboard-layout.js +77 -0
- package/dist/layout-math.d.ts +20 -0
- package/dist/layout-math.js +22 -0
- package/dist/layout.svelte.d.ts +17 -0
- package/dist/layout.svelte.js +58 -0
- package/dist/radial-math.d.ts +33 -0
- package/dist/radial-math.js +90 -0
- package/dist/tabs-context.d.ts +13 -0
- package/dist/tabs-context.js +6 -0
- package/dist/text-edit.d.ts +46 -0
- package/dist/text-edit.js +133 -0
- package/dist/text-focus.svelte.d.ts +24 -0
- package/dist/text-focus.svelte.js +102 -0
- package/dist/theme.d.ts +335 -0
- package/dist/theme.js +312 -0
- package/dist/toast.svelte.d.ts +24 -0
- package/dist/toast.svelte.js +30 -0
- package/dist/types.d.ts +25 -0
- package/dist/types.js +1 -0
- package/dist/xr.svelte.d.ts +9 -0
- package/dist/xr.svelte.js +24 -0
- package/package.json +44 -3
- 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,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,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;
|
package/dist/Tab.svelte
ADDED
|
@@ -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>
|