@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
package/dist/Menu.svelte
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
<script lang="ts" module>export {};
|
|
2
|
+
</script>
|
|
3
|
+
|
|
4
|
+
<script lang="ts">// Base UI-style Menu: a list of selectable items, with optional end labels
|
|
5
|
+
// (shortcuts, counts), dividers and disabled items.
|
|
6
|
+
// <Menu items={[{ label: 'Wave', end: '1' }, { divider: true }, { label: 'Leave' }]} onselect={…} />
|
|
7
|
+
// Pass `selected` (an item's id, or its label) to make it a single-choice menu with a check.
|
|
8
|
+
// 2D: roving focus with arrow keys / Home / End. 3D: equal-width rows on a backing slab.
|
|
9
|
+
import { ensure3d } from "./aframe";
|
|
10
|
+
import { nextIndex } from "./component-style";
|
|
11
|
+
import { getDimension, getTheme } from "./context";
|
|
12
|
+
import { createStack, setStack, useSlot } from "./layout.svelte";
|
|
13
|
+
import Pressable3D from "./Pressable3D.svelte";
|
|
14
|
+
import Rule3D from "./Rule3D.svelte";
|
|
15
|
+
let { items, onselect, selected = $bindable(), width, dimension, style: userStyle = "", ...rest } = $props();
|
|
16
|
+
const theme = getTheme();
|
|
17
|
+
const t = $derived(theme());
|
|
18
|
+
const c = $derived(t.colors.menu);
|
|
19
|
+
const tk = $derived(t.menu);
|
|
20
|
+
const font = $derived(t.typography[tk.typography]);
|
|
21
|
+
const endFont = $derived(t.typography[tk.endTypography]);
|
|
22
|
+
const isDivider = (item) => "divider" in item;
|
|
23
|
+
const idOf = (item) => item.id ?? item.label;
|
|
24
|
+
const selectable = $derived(selected !== undefined);
|
|
25
|
+
function choose(item, index) {
|
|
26
|
+
if (item.disabled) return;
|
|
27
|
+
if (selectable) selected = idOf(item);
|
|
28
|
+
onselect?.(item, index);
|
|
29
|
+
}
|
|
30
|
+
// What goes on the right: the item's end label, else a check when it's the selected one.
|
|
31
|
+
const endOf = (item) => item.end ?? (selectable && selected === idOf(item) ? "✓" : undefined);
|
|
32
|
+
const textOf = (item) => item.disabled ? t.colors.buttonDisabledText : selectable && selected === idOf(item) ? c.selected : c.text;
|
|
33
|
+
const contextDimension = getDimension();
|
|
34
|
+
// svelte-ignore state_referenced_locally
|
|
35
|
+
const is3d = (dimension ?? contextDimension) === "3d";
|
|
36
|
+
// ---- 3D ----
|
|
37
|
+
const m = (px) => px * t.space3d.metersPerPx;
|
|
38
|
+
if (is3d) ensure3d();
|
|
39
|
+
const column = is3d ? createStack(() => ({
|
|
40
|
+
direction: "column",
|
|
41
|
+
gap: 0
|
|
42
|
+
})) : undefined;
|
|
43
|
+
const pad = $derived(m(tk.padding));
|
|
44
|
+
// Rows are drawn as wide as the widest one (or the menu's width).
|
|
45
|
+
const rowWidth = $derived(Math.max(column?.size.width ?? 0, m(width ?? tk.minWidth) - 2 * pad));
|
|
46
|
+
const outer = $derived({
|
|
47
|
+
width: rowWidth + 2 * pad,
|
|
48
|
+
height: (column?.size.height ?? 0) + 2 * pad
|
|
49
|
+
});
|
|
50
|
+
const slot = is3d ? useSlot(() => outer) : () => ({
|
|
51
|
+
x: 0,
|
|
52
|
+
y: 0
|
|
53
|
+
});
|
|
54
|
+
setStack(column);
|
|
55
|
+
const depth = .02;
|
|
56
|
+
// ---- 2D ----
|
|
57
|
+
let buttons = $state([]);
|
|
58
|
+
const usable = $derived(items.map((item) => !isDivider(item) && !item.disabled));
|
|
59
|
+
let focusIndex = $state(-1);
|
|
60
|
+
const tabStop = $derived(focusIndex >= 0 ? focusIndex : usable.indexOf(true));
|
|
61
|
+
function onkeydown(event) {
|
|
62
|
+
const from = tabStop;
|
|
63
|
+
const to = event.key === "ArrowDown" ? nextIndex(usable, from, 1) : event.key === "ArrowUp" ? nextIndex(usable, from, -1) : event.key === "Home" ? nextIndex(usable, -1, 1, false) : event.key === "End" ? nextIndex(usable, items.length, -1, false) : -1;
|
|
64
|
+
if (to < 0) return;
|
|
65
|
+
event.preventDefault();
|
|
66
|
+
focusIndex = to;
|
|
67
|
+
buttons[to]?.focus();
|
|
68
|
+
}
|
|
69
|
+
</script>
|
|
70
|
+
|
|
71
|
+
{#if is3d}
|
|
72
|
+
<a-entity {...rest}>
|
|
73
|
+
<a-entity position="{slot().x} {slot().y} 0">
|
|
74
|
+
<a-entity
|
|
75
|
+
rounded="width: {outer.width}; height: {outer.height}; depth: {depth}; radius: {Math.min(m(tk.radius), outer.height / 2)}; color: {c.fill}; opacity: {t.space3d.panelOpacity}"
|
|
76
|
+
position="{-outer.width / 2} {-outer.height / 2} {-depth}"
|
|
77
|
+
></a-entity>
|
|
78
|
+
<a-entity position="0 0 0.002">
|
|
79
|
+
{#each items as item, i}
|
|
80
|
+
{#if isDivider(item)}
|
|
81
|
+
<Rule3D width={rowWidth - 2 * m(tk.paddingX)} color={c.divider} />
|
|
82
|
+
{:else}
|
|
83
|
+
<Pressable3D
|
|
84
|
+
label={item.label}
|
|
85
|
+
end={endOf(item)}
|
|
86
|
+
{font}
|
|
87
|
+
{endFont}
|
|
88
|
+
textColor={textOf(item)}
|
|
89
|
+
endColor={selectable && selected === idOf(item) ? c.selected : c.end}
|
|
90
|
+
height={tk.itemHeight}
|
|
91
|
+
paddingX={tk.paddingX}
|
|
92
|
+
radius={tk.itemRadius}
|
|
93
|
+
fillWidth={rowWidth}
|
|
94
|
+
align="start"
|
|
95
|
+
hoverFill={c.hover}
|
|
96
|
+
activeFill={c.active}
|
|
97
|
+
disabled={item.disabled}
|
|
98
|
+
onclick={() => choose(item, i)}
|
|
99
|
+
/>
|
|
100
|
+
{/if}
|
|
101
|
+
{/each}
|
|
102
|
+
</a-entity>
|
|
103
|
+
</a-entity>
|
|
104
|
+
</a-entity>
|
|
105
|
+
{:else}
|
|
106
|
+
<div
|
|
107
|
+
class="ui-menu"
|
|
108
|
+
role="menu"
|
|
109
|
+
tabindex="-1"
|
|
110
|
+
{onkeydown}
|
|
111
|
+
style="--ui-fill: {c.fill}; --ui-border: {c.border}; --ui-hover: {c.hover}; --ui-active: {c.active}; --ui-text: {c.text}; --ui-end: {c.end}; --ui-selected: {c.selected}; --ui-divider: {c.divider}; --ui-disabled: {t.colors.buttonDisabledText}; --ui-focus: {t.colors.accent}; --ui-padding: {tk.padding}px; --ui-padding-x: {tk.paddingX}px; --ui-radius: {tk.radius}px; --ui-item-radius: {tk.itemRadius}px; --ui-item-height: {tk.itemHeight}px; --ui-width: {width ?? tk.minWidth}px; --ui-font: {font.fontWeight} {font.fontSize}px/{font.lineHeight}px {t.typography.fontFamily}; --ui-end-font: {endFont.fontWeight} {endFont.fontSize}px/{endFont.lineHeight}px {t.typography.fontFamily}; {userStyle}"
|
|
112
|
+
{...rest}
|
|
113
|
+
>
|
|
114
|
+
{#each items as item, i}
|
|
115
|
+
{#if isDivider(item)}
|
|
116
|
+
<hr />
|
|
117
|
+
{:else}
|
|
118
|
+
<button
|
|
119
|
+
bind:this={buttons[i]}
|
|
120
|
+
type="button"
|
|
121
|
+
role={selectable ? 'menuitemradio' : 'menuitem'}
|
|
122
|
+
aria-checked={selectable ? selected === idOf(item) : undefined}
|
|
123
|
+
class:selected={selectable && selected === idOf(item)}
|
|
124
|
+
tabindex={i === tabStop ? 0 : -1}
|
|
125
|
+
disabled={item.disabled}
|
|
126
|
+
onfocus={() => (focusIndex = i)}
|
|
127
|
+
onclick={() => choose(item, i)}
|
|
128
|
+
>
|
|
129
|
+
<span>{item.label}</span>
|
|
130
|
+
{#if endOf(item)}<span class="end">{endOf(item)}</span>{/if}
|
|
131
|
+
</button>
|
|
132
|
+
{/if}
|
|
133
|
+
{/each}
|
|
134
|
+
</div>
|
|
135
|
+
{/if}
|
|
136
|
+
|
|
137
|
+
<style>
|
|
138
|
+
.ui-menu {
|
|
139
|
+
box-sizing: border-box;
|
|
140
|
+
display: flex;
|
|
141
|
+
flex-direction: column;
|
|
142
|
+
min-width: var(--ui-width);
|
|
143
|
+
padding: var(--ui-padding);
|
|
144
|
+
border-radius: var(--ui-radius);
|
|
145
|
+
background: var(--ui-fill);
|
|
146
|
+
border: 1px solid var(--ui-border);
|
|
147
|
+
}
|
|
148
|
+
button {
|
|
149
|
+
display: flex;
|
|
150
|
+
align-items: center;
|
|
151
|
+
justify-content: space-between;
|
|
152
|
+
gap: 16px;
|
|
153
|
+
height: var(--ui-item-height);
|
|
154
|
+
padding: 0 var(--ui-padding-x);
|
|
155
|
+
border: 0;
|
|
156
|
+
border-radius: var(--ui-item-radius);
|
|
157
|
+
background: transparent;
|
|
158
|
+
color: var(--ui-text);
|
|
159
|
+
font: var(--ui-font);
|
|
160
|
+
text-align: left;
|
|
161
|
+
cursor: pointer;
|
|
162
|
+
}
|
|
163
|
+
button:hover:not(:disabled) {
|
|
164
|
+
background: var(--ui-hover);
|
|
165
|
+
}
|
|
166
|
+
button:active:not(:disabled) {
|
|
167
|
+
background: var(--ui-active);
|
|
168
|
+
}
|
|
169
|
+
button:focus-visible {
|
|
170
|
+
outline: 3px solid var(--ui-focus);
|
|
171
|
+
outline-offset: -3px;
|
|
172
|
+
}
|
|
173
|
+
button:disabled {
|
|
174
|
+
color: var(--ui-disabled);
|
|
175
|
+
cursor: not-allowed;
|
|
176
|
+
}
|
|
177
|
+
.selected {
|
|
178
|
+
color: var(--ui-selected);
|
|
179
|
+
}
|
|
180
|
+
.end {
|
|
181
|
+
font: var(--ui-end-font);
|
|
182
|
+
color: var(--ui-end);
|
|
183
|
+
}
|
|
184
|
+
.selected .end {
|
|
185
|
+
color: var(--ui-selected);
|
|
186
|
+
}
|
|
187
|
+
hr {
|
|
188
|
+
width: calc(100% - 2 * var(--ui-padding-x));
|
|
189
|
+
margin: 6px auto;
|
|
190
|
+
border: 0;
|
|
191
|
+
border-top: 1px solid var(--ui-divider);
|
|
192
|
+
}
|
|
193
|
+
</style>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type MenuItem = {
|
|
2
|
+
id?: string;
|
|
3
|
+
label: string;
|
|
4
|
+
end?: string;
|
|
5
|
+
disabled?: boolean;
|
|
6
|
+
} | {
|
|
7
|
+
divider: true;
|
|
8
|
+
};
|
|
9
|
+
import type { Dimension } from './theme';
|
|
10
|
+
type Item = Exclude<MenuItem, {
|
|
11
|
+
divider: true;
|
|
12
|
+
}>;
|
|
13
|
+
type Props = {
|
|
14
|
+
items: MenuItem[];
|
|
15
|
+
onselect?: (item: Item, index: number) => void;
|
|
16
|
+
selected?: string;
|
|
17
|
+
width?: number;
|
|
18
|
+
dimension?: Dimension;
|
|
19
|
+
[attribute: string]: unknown;
|
|
20
|
+
};
|
|
21
|
+
declare const Menu: import("svelte").Component<Props, {}, "selected">;
|
|
22
|
+
type Menu = ReturnType<typeof Menu>;
|
|
23
|
+
export default Menu;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
<script lang="ts">import Button from "./Button.svelte";
|
|
2
|
+
import { getDimension, getTheme } from "./context";
|
|
3
|
+
import Panel from "./Panel.svelte";
|
|
4
|
+
import Stack from "./Stack.svelte";
|
|
5
|
+
import StackScope from "./StackScope.svelte";
|
|
6
|
+
const theme = getTheme();
|
|
7
|
+
let { open = $bindable(false), title, children, actions, closeable = true, onclose, width = theme().modal.width, dimension, ...rest } = $props();
|
|
8
|
+
const t = $derived(theme());
|
|
9
|
+
const contextDimension = getDimension();
|
|
10
|
+
const is3d = $derived((dimension ?? contextDimension) === "3d");
|
|
11
|
+
function close() {
|
|
12
|
+
if (!open) return;
|
|
13
|
+
open = false;
|
|
14
|
+
onclose?.();
|
|
15
|
+
}
|
|
16
|
+
// 2D: keep the native dialog in sync with `open`.
|
|
17
|
+
let dialog = $state();
|
|
18
|
+
$effect(() => {
|
|
19
|
+
if (!dialog) return;
|
|
20
|
+
if (open && !dialog.open) dialog.showModal();
|
|
21
|
+
if (!open && dialog.open) dialog.close();
|
|
22
|
+
});
|
|
23
|
+
// 3D: Escape closes (desktop).
|
|
24
|
+
$effect(() => {
|
|
25
|
+
if (!is3d || !open || !closeable) return;
|
|
26
|
+
const onkey = (event) => event.key === "Escape" && close();
|
|
27
|
+
window.addEventListener("keydown", onkey);
|
|
28
|
+
return () => window.removeEventListener("keydown", onkey);
|
|
29
|
+
});
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
{#snippet closeButton()}
|
|
33
|
+
<Button label="×" kind="tertiary" shape="circle" size="compact" aria-label="Close" onclick={close} />
|
|
34
|
+
{/snippet}
|
|
35
|
+
|
|
36
|
+
{#snippet content()}
|
|
37
|
+
<Panel {title} {width} catchClicks action={closeable ? closeButton : undefined}>
|
|
38
|
+
{@render children?.()}
|
|
39
|
+
{#if actions}
|
|
40
|
+
<Stack direction="row" gap={12} alignSelf="end">{@render actions()}</Stack>
|
|
41
|
+
{/if}
|
|
42
|
+
</Panel>
|
|
43
|
+
{/snippet}
|
|
44
|
+
|
|
45
|
+
{#if is3d}
|
|
46
|
+
{#if open}
|
|
47
|
+
<!-- An overlay: not part of any surrounding Panel / Stack layout. -->
|
|
48
|
+
<StackScope>
|
|
49
|
+
<a-entity {...rest}>
|
|
50
|
+
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
|
|
51
|
+
<a-entity
|
|
52
|
+
class="raycastable"
|
|
53
|
+
geometry="primitive: plane; width: 12; height: 12"
|
|
54
|
+
material="color: {t.colors.modal.backdrop}; opacity: {t.colors.modal.backdropOpacity}; transparent: true; shader: flat"
|
|
55
|
+
position="0 0 -0.08"
|
|
56
|
+
onclick={() => closeable && close()}
|
|
57
|
+
></a-entity>
|
|
58
|
+
<a-entity
|
|
59
|
+
animation__open="property: scale; from: 0.92 0.92 0.92; to: 1 1 1; dur: {t.animation.timing200}; easing: {t.animation.easeOut3d}"
|
|
60
|
+
>
|
|
61
|
+
{@render content()}
|
|
62
|
+
</a-entity>
|
|
63
|
+
</a-entity>
|
|
64
|
+
</StackScope>
|
|
65
|
+
{/if}
|
|
66
|
+
{:else}
|
|
67
|
+
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_noninteractive_element_interactions -->
|
|
68
|
+
<dialog
|
|
69
|
+
bind:this={dialog}
|
|
70
|
+
class="ui-modal"
|
|
71
|
+
style="--ui-backdrop: {t.colors.modal.backdrop}; --ui-backdrop-opacity: {t.colors.modal.backdropOpacity};"
|
|
72
|
+
onclose={close}
|
|
73
|
+
oncancel={(event) => !closeable && event.preventDefault()}
|
|
74
|
+
onclick={(event) => event.target === dialog && closeable && close()}
|
|
75
|
+
{...rest}
|
|
76
|
+
>
|
|
77
|
+
{@render content()}
|
|
78
|
+
</dialog>
|
|
79
|
+
{/if}
|
|
80
|
+
|
|
81
|
+
<style>
|
|
82
|
+
.ui-modal {
|
|
83
|
+
padding: 0;
|
|
84
|
+
border: 0;
|
|
85
|
+
background: transparent;
|
|
86
|
+
color: inherit;
|
|
87
|
+
max-width: calc(100vw - 32px);
|
|
88
|
+
overflow: visible;
|
|
89
|
+
}
|
|
90
|
+
.ui-modal[open] {
|
|
91
|
+
animation: ui-modal-in 0.2s cubic-bezier(0.2, 0.8, 0.4, 1);
|
|
92
|
+
}
|
|
93
|
+
.ui-modal::backdrop {
|
|
94
|
+
background: color-mix(in srgb, var(--ui-backdrop) calc(var(--ui-backdrop-opacity) * 100%), transparent);
|
|
95
|
+
backdrop-filter: blur(4px);
|
|
96
|
+
}
|
|
97
|
+
@keyframes ui-modal-in {
|
|
98
|
+
from {
|
|
99
|
+
opacity: 0;
|
|
100
|
+
transform: scale(0.96);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
</style>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { Dimension } from './theme';
|
|
3
|
+
type Props = {
|
|
4
|
+
open?: boolean;
|
|
5
|
+
title?: string;
|
|
6
|
+
children?: Snippet;
|
|
7
|
+
actions?: Snippet;
|
|
8
|
+
closeable?: boolean;
|
|
9
|
+
onclose?: () => void;
|
|
10
|
+
width?: number;
|
|
11
|
+
dimension?: Dimension;
|
|
12
|
+
[attribute: string]: unknown;
|
|
13
|
+
};
|
|
14
|
+
declare const Modal: import("svelte").Component<Props, {}, "open">;
|
|
15
|
+
type Modal = ReturnType<typeof Modal>;
|
|
16
|
+
export default Modal;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
<script lang="ts">import { ensure3d } from "./aframe";
|
|
2
|
+
import { getDimension, getTheme } from "./context";
|
|
3
|
+
import { createStack, setStack, useSlot } from "./layout.svelte";
|
|
4
|
+
import StackScope from "./StackScope.svelte";
|
|
5
|
+
import Text from "./Text.svelte";
|
|
6
|
+
const theme = getTheme();
|
|
7
|
+
let { children, title, action, kind = "solid", fill, catchClicks = false, align = "start", width, padding = theme().panel.padding, gap = theme().panel.gap, dimension, style: userStyle = "", ...rest } = $props();
|
|
8
|
+
const t = $derived(theme());
|
|
9
|
+
const m = (px) => px * t.space3d.metersPerPx;
|
|
10
|
+
const contextDimension = getDimension();
|
|
11
|
+
// svelte-ignore state_referenced_locally
|
|
12
|
+
const is3d = (dimension ?? contextDimension) === "3d";
|
|
13
|
+
// 3D: join the parent's stack (if nested), then stack our own children.
|
|
14
|
+
if (is3d) ensure3d();
|
|
15
|
+
const column = is3d ? createStack(() => ({
|
|
16
|
+
direction: "column",
|
|
17
|
+
gap: m(gap),
|
|
18
|
+
align
|
|
19
|
+
})) : undefined;
|
|
20
|
+
const innerWidth = $derived(Math.max(column?.size.width ?? 0, width ? m(width) - 2 * m(padding) : 0));
|
|
21
|
+
const outer = $derived({
|
|
22
|
+
width: innerWidth + 2 * m(padding),
|
|
23
|
+
height: (column?.size.height ?? 0) + 2 * m(padding)
|
|
24
|
+
});
|
|
25
|
+
const slot = is3d ? useSlot(() => outer) : () => ({
|
|
26
|
+
x: 0,
|
|
27
|
+
y: 0
|
|
28
|
+
});
|
|
29
|
+
setStack(column);
|
|
30
|
+
const actionRow = is3d ? createStack(() => ({
|
|
31
|
+
direction: "row",
|
|
32
|
+
gap: m(8)
|
|
33
|
+
})) : undefined;
|
|
34
|
+
// The action's row is centered on the title line (or just tucked into the corner).
|
|
35
|
+
const titleHeight = $derived(title ? m(t.typography.HeadingSmall.lineHeight) * t.space3d.textScale : actionRow?.size.height ?? 0);
|
|
36
|
+
const actionPosition = $derived({
|
|
37
|
+
x: outer.width / 2 - m(padding) - (actionRow?.size.width ?? 0) / 2,
|
|
38
|
+
y: outer.height / 2 - m(padding) - titleHeight / 2
|
|
39
|
+
});
|
|
40
|
+
const background = $derived(fill ?? t.colors.panel.fill);
|
|
41
|
+
const depth = .02;
|
|
42
|
+
const slab = $derived([
|
|
43
|
+
`width: ${outer.width}`,
|
|
44
|
+
`height: ${outer.height}`,
|
|
45
|
+
`depth: ${depth}`,
|
|
46
|
+
`radius: ${Math.min(m(t.panel.radius), outer.height / 2)}`,
|
|
47
|
+
`glass: ${kind === "glass"}`,
|
|
48
|
+
`color: ${background}`,
|
|
49
|
+
`opacity: ${kind === "glass" ? 1 : t.space3d.panelOpacity}`
|
|
50
|
+
].join("; "));
|
|
51
|
+
// Start-aligned children line up with the inner left edge even when `width` makes the panel wider.
|
|
52
|
+
const contentX = $derived(align === "start" ? -(innerWidth - (column?.size.width ?? 0)) / 2 : 0);
|
|
53
|
+
</script>
|
|
54
|
+
|
|
55
|
+
{#if is3d}
|
|
56
|
+
<a-entity {...rest}>
|
|
57
|
+
<a-entity position="{slot().x} {slot().y} 0">
|
|
58
|
+
<!-- The slab's front face sits at z = 0; content just in front of it. -->
|
|
59
|
+
<a-entity rounded={slab} position="{-outer.width / 2} {-outer.height / 2} {-depth}"></a-entity>
|
|
60
|
+
{#if catchClicks}
|
|
61
|
+
<a-entity
|
|
62
|
+
class="raycastable"
|
|
63
|
+
geometry="primitive: plane; width: {outer.width}; height: {outer.height}"
|
|
64
|
+
material="visible: false"
|
|
65
|
+
position="0 0 0.001"
|
|
66
|
+
></a-entity>
|
|
67
|
+
{/if}
|
|
68
|
+
<a-entity position="{contentX} 0 0.005">
|
|
69
|
+
{#if title}<Text variant="HeadingSmall" text={title} />{/if}
|
|
70
|
+
{@render children?.()}
|
|
71
|
+
</a-entity>
|
|
72
|
+
{#if action}
|
|
73
|
+
<a-entity position="{actionPosition.x} {actionPosition.y} 0.005">
|
|
74
|
+
<StackScope stack={actionRow}>{@render action()}</StackScope>
|
|
75
|
+
</a-entity>
|
|
76
|
+
{/if}
|
|
77
|
+
</a-entity>
|
|
78
|
+
</a-entity>
|
|
79
|
+
{:else}
|
|
80
|
+
<section
|
|
81
|
+
class="ui-panel"
|
|
82
|
+
class:glass={kind === 'glass'}
|
|
83
|
+
class:has-corner-action={action && !title}
|
|
84
|
+
style="--ui-fill: {background}; --ui-border: {fill ? `color-mix(in srgb, ${fill} 70%, white 10%)` : t.colors.panel.border}; --ui-radius: {t.panel.radius}px; --ui-padding: {padding}px; --ui-gap: {gap}px; --ui-align: {align === 'start'
|
|
85
|
+
? 'flex-start'
|
|
86
|
+
: 'center'}; {width ? `width: ${width}px;` : ''} {userStyle}"
|
|
87
|
+
{...rest}
|
|
88
|
+
>
|
|
89
|
+
{#if title && action}
|
|
90
|
+
<div class="header"><Text variant="HeadingSmall" text={title} />{@render action()}</div>
|
|
91
|
+
{:else if title}
|
|
92
|
+
<Text variant="HeadingSmall" text={title} />
|
|
93
|
+
{:else if action}
|
|
94
|
+
<div class="corner">{@render action()}</div>
|
|
95
|
+
{/if}
|
|
96
|
+
{@render children?.()}
|
|
97
|
+
</section>
|
|
98
|
+
{/if}
|
|
99
|
+
|
|
100
|
+
<style>
|
|
101
|
+
.ui-panel {
|
|
102
|
+
box-sizing: border-box;
|
|
103
|
+
display: flex;
|
|
104
|
+
flex-direction: column;
|
|
105
|
+
align-items: var(--ui-align);
|
|
106
|
+
gap: var(--ui-gap);
|
|
107
|
+
padding: var(--ui-padding);
|
|
108
|
+
border-radius: var(--ui-radius);
|
|
109
|
+
background: var(--ui-fill);
|
|
110
|
+
border: 1px solid var(--ui-border);
|
|
111
|
+
}
|
|
112
|
+
.header {
|
|
113
|
+
display: flex;
|
|
114
|
+
align-items: center;
|
|
115
|
+
justify-content: space-between;
|
|
116
|
+
gap: 12px;
|
|
117
|
+
align-self: stretch;
|
|
118
|
+
}
|
|
119
|
+
.has-corner-action {
|
|
120
|
+
position: relative;
|
|
121
|
+
padding-right: calc(var(--ui-padding) + 32px);
|
|
122
|
+
}
|
|
123
|
+
.corner {
|
|
124
|
+
position: absolute;
|
|
125
|
+
top: calc(var(--ui-padding) / 2);
|
|
126
|
+
right: calc(var(--ui-padding) / 2);
|
|
127
|
+
}
|
|
128
|
+
.glass {
|
|
129
|
+
background: color-mix(in srgb, var(--ui-fill) 55%, transparent);
|
|
130
|
+
backdrop-filter: blur(16px) saturate(1.4);
|
|
131
|
+
}
|
|
132
|
+
</style>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { Dimension } from './theme';
|
|
3
|
+
type Props = {
|
|
4
|
+
children?: Snippet;
|
|
5
|
+
title?: string;
|
|
6
|
+
action?: Snippet;
|
|
7
|
+
kind?: 'solid' | 'glass';
|
|
8
|
+
fill?: string;
|
|
9
|
+
catchClicks?: boolean;
|
|
10
|
+
align?: 'start' | 'center';
|
|
11
|
+
width?: number;
|
|
12
|
+
padding?: number;
|
|
13
|
+
gap?: number;
|
|
14
|
+
dimension?: Dimension;
|
|
15
|
+
[attribute: string]: unknown;
|
|
16
|
+
};
|
|
17
|
+
declare const Panel: import("svelte").Component<Props, {}, "">;
|
|
18
|
+
type Panel = ReturnType<typeof Panel>;
|
|
19
|
+
export default Panel;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<script lang="ts">// Internal 3D building block for Tabs and Menu: a clickable row with a label, an
|
|
2
|
+
// optional end label (shortcut / meta), a hover background and an optional
|
|
3
|
+
// underline. The parent resolves every color; this only draws and reports its size.
|
|
4
|
+
import { ensure3d } from "./aframe";
|
|
5
|
+
import { watchTextSize } from "./aframe/measure";
|
|
6
|
+
import { getTheme } from "./context";
|
|
7
|
+
import { font3d } from "./fonts";
|
|
8
|
+
import { useSlot } from "./layout.svelte";
|
|
9
|
+
let { label, end, font, endFont = font, textColor, endColor = textColor, height, paddingX, minWidth = 0, radius = 0, fillWidth, align = "center", fill, hoverFill, activeFill, indicator, selected = false, disabled = false, onclick } = $props();
|
|
10
|
+
ensure3d();
|
|
11
|
+
const theme = getTheme();
|
|
12
|
+
const t = $derived(theme());
|
|
13
|
+
const m = (px) => px * t.space3d.metersPerPx;
|
|
14
|
+
let hovered = $state(false);
|
|
15
|
+
let pressed = $state(false);
|
|
16
|
+
let labelWidth = $state(0);
|
|
17
|
+
let endWidth = $state(0);
|
|
18
|
+
let labelEl = $state();
|
|
19
|
+
let endEl = $state();
|
|
20
|
+
const h = $derived(m(height));
|
|
21
|
+
const pad = $derived(m(paddingX));
|
|
22
|
+
const endGap = $derived(end ? m(16) : 0);
|
|
23
|
+
const intrinsic = $derived(Math.max(m(minWidth), labelWidth + endGap + endWidth + 2 * pad));
|
|
24
|
+
const w = $derived(fillWidth ?? intrinsic);
|
|
25
|
+
const slot = useSlot(() => ({
|
|
26
|
+
width: intrinsic,
|
|
27
|
+
height: h
|
|
28
|
+
}));
|
|
29
|
+
const background = $derived(disabled ? undefined : pressed ? activeFill : hovered ? hoverFill : fill);
|
|
30
|
+
const text = (value, f, color, anchor) => `value: ${value.replace(/;/g, ",")}; fontSize: ${m(f.fontSize) * t.space3d.textScale}; color: ${color}; font: ${font3d(t)}; anchor: ${anchor}; baseline: center`;
|
|
31
|
+
$effect(() => {
|
|
32
|
+
if (!disabled) return;
|
|
33
|
+
hovered = pressed = false;
|
|
34
|
+
});
|
|
35
|
+
$effect(() => {
|
|
36
|
+
if (labelEl) return watchTextSize(labelEl, (width) => labelWidth = width);
|
|
37
|
+
});
|
|
38
|
+
$effect(() => {
|
|
39
|
+
if (endEl) return watchTextSize(endEl, (width) => endWidth = width);
|
|
40
|
+
});
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions, a11y_mouse_events_have_key_events -->
|
|
44
|
+
<a-entity
|
|
45
|
+
position="{slot().x} {slot().y} 0"
|
|
46
|
+
onclick={(event: Event) => !disabled && onclick?.(event)}
|
|
47
|
+
onmouseenter={() => !disabled && (hovered = true)}
|
|
48
|
+
onmouseleave={() => (hovered = pressed = false)}
|
|
49
|
+
onmousedown={() => !disabled && (pressed = true)}
|
|
50
|
+
onmouseup={() => (pressed = false)}
|
|
51
|
+
>
|
|
52
|
+
<a-entity
|
|
53
|
+
class={disabled ? '' : 'raycastable'}
|
|
54
|
+
geometry="primitive: plane; width: {w}; height: {h}"
|
|
55
|
+
material="visible: false"
|
|
56
|
+
position="0 0 0.006"
|
|
57
|
+
></a-entity>
|
|
58
|
+
<a-entity
|
|
59
|
+
rounded="flat: true; width: {w}; height: {h}; radius: {Math.min(m(radius), h / 2)}; color: {background ?? '#000000'}; enabled: {!!background}"
|
|
60
|
+
position="{-w / 2} {-h / 2} 0.001"
|
|
61
|
+
></a-entity>
|
|
62
|
+
{#if indicator}
|
|
63
|
+
<a-entity
|
|
64
|
+
rounded="flat: true; width: {w}; height: {m(indicator.height)}; radius: {m(indicator.height) / 2}; color: {indicator.color}; enabled: {selected}"
|
|
65
|
+
position="{-w / 2} {-h / 2} 0.002"
|
|
66
|
+
></a-entity>
|
|
67
|
+
{/if}
|
|
68
|
+
<a-entity
|
|
69
|
+
bind:this={labelEl}
|
|
70
|
+
troika-text={text(label, font, textColor, align === 'start' ? 'left' : 'center')}
|
|
71
|
+
troika-text-material="shader: standard"
|
|
72
|
+
position="{align === 'start' ? -w / 2 + pad : end ? -(endGap + endWidth) / 2 : 0} 0 0.004"
|
|
73
|
+
></a-entity>
|
|
74
|
+
{#if end}
|
|
75
|
+
<a-entity
|
|
76
|
+
bind:this={endEl}
|
|
77
|
+
troika-text={text(end, endFont, endColor, 'right')}
|
|
78
|
+
troika-text-material="shader: standard"
|
|
79
|
+
position="{w / 2 - pad} 0 0.004"
|
|
80
|
+
></a-entity>
|
|
81
|
+
{/if}
|
|
82
|
+
</a-entity>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Font } from './theme';
|
|
2
|
+
type Props = {
|
|
3
|
+
label: string;
|
|
4
|
+
end?: string;
|
|
5
|
+
font: Font;
|
|
6
|
+
endFont?: Font;
|
|
7
|
+
textColor: string;
|
|
8
|
+
endColor?: string;
|
|
9
|
+
height: number;
|
|
10
|
+
paddingX: number;
|
|
11
|
+
minWidth?: number;
|
|
12
|
+
radius?: number;
|
|
13
|
+
fillWidth?: number;
|
|
14
|
+
align?: 'center' | 'start';
|
|
15
|
+
fill?: string;
|
|
16
|
+
hoverFill?: string;
|
|
17
|
+
activeFill?: string;
|
|
18
|
+
indicator?: {
|
|
19
|
+
color: string;
|
|
20
|
+
height: number;
|
|
21
|
+
};
|
|
22
|
+
selected?: boolean;
|
|
23
|
+
disabled?: boolean;
|
|
24
|
+
onclick?: (event: Event) => void;
|
|
25
|
+
};
|
|
26
|
+
declare const Pressable3D: import("svelte").Component<Props, {}, "">;
|
|
27
|
+
type Pressable3D = ReturnType<typeof Pressable3D>;
|
|
28
|
+
export default Pressable3D;
|