@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,145 @@
1
+ <script lang="ts">import { nextIndex } from "./component-style";
2
+ import { getDimension, getTheme } from "./context";
3
+ import { createStack, setStack, useSlot } from "./layout.svelte";
4
+ import Pressable3D from "./Pressable3D.svelte";
5
+ import Stack from "./Stack.svelte";
6
+ import { setTabs } from "./tabs-context";
7
+ let { children, active = $bindable(), onchange, gap = 12, dimension, style: userStyle = "", ...rest } = $props();
8
+ const theme = getTheme();
9
+ const t = $derived(theme());
10
+ const c = $derived(t.colors.tabs);
11
+ const font = $derived(t.typography[t.tabs.typography]);
12
+ const uid = `ui-tabs-${Math.random().toString(36).slice(2, 8)}`;
13
+ let tabs = $state.raw([]);
14
+ const current = $derived(active ?? tabs.find((tab) => !tab.disabled)?.key);
15
+ function select(key) {
16
+ if (key === current) return;
17
+ active = key;
18
+ onchange?.(key);
19
+ }
20
+ setTabs({
21
+ id: uid,
22
+ register: (tab) => tabs = [...tabs, tab],
23
+ unregister: (tab) => tabs = tabs.filter((x) => x !== tab),
24
+ get active() {
25
+ return current;
26
+ }
27
+ });
28
+ const contextDimension = getDimension();
29
+ // svelte-ignore state_referenced_locally
30
+ const is3d = (dimension ?? contextDimension) === "3d";
31
+ const m = (px) => px * t.space3d.metersPerPx;
32
+ const column = is3d ? createStack(() => ({
33
+ direction: "column",
34
+ gap: m(gap),
35
+ align: "start"
36
+ })) : undefined;
37
+ const slot = is3d ? useSlot(() => column.size) : () => ({
38
+ x: 0,
39
+ y: 0
40
+ });
41
+ setStack(column);
42
+ // 2D keyboard: arrows / Home / End move to (and select) another enabled tab.
43
+ let buttons = $state([]);
44
+ function onkeydown(event) {
45
+ const usable = tabs.map((tab) => !tab.disabled);
46
+ const from = tabs.findIndex((tab) => tab.key === current);
47
+ const to = event.key === "ArrowRight" ? nextIndex(usable, from, 1) : event.key === "ArrowLeft" ? nextIndex(usable, from, -1) : event.key === "Home" ? nextIndex(usable, -1, 1, false) : event.key === "End" ? nextIndex(usable, tabs.length, -1, false) : -1;
48
+ if (to < 0 || to === from) return;
49
+ event.preventDefault();
50
+ select(tabs[to].key);
51
+ buttons[to]?.focus();
52
+ }
53
+ </script>
54
+
55
+ {#if is3d}
56
+ <a-entity {...rest}>
57
+ <a-entity position="{slot().x} {slot().y} 0">
58
+ <Stack direction="row" gap={t.tabs.gap}>
59
+ {#each tabs as tab (tab)}
60
+ <Pressable3D
61
+ label={tab.title}
62
+ {font}
63
+ textColor={tab.disabled ? t.colors.buttonDisabledText : tab.key === current ? c.textActive : c.text}
64
+ height={t.tabs.height}
65
+ paddingX={t.tabs.paddingX}
66
+ radius={t.tabs.radius}
67
+ hoverFill={c.hover}
68
+ activeFill={c.hover}
69
+ indicator={{ color: c.indicator, height: t.tabs.indicator }}
70
+ selected={tab.key === current}
71
+ disabled={tab.disabled}
72
+ onclick={() => select(tab.key)}
73
+ />
74
+ {/each}
75
+ </Stack>
76
+ {@render children()}
77
+ </a-entity>
78
+ </a-entity>
79
+ {:else}
80
+ <div
81
+ class="ui-tabs"
82
+ style="--ui-text: {c.text}; --ui-text-active: {c.textActive}; --ui-hover: {c.hover}; --ui-indicator: {c.indicator}; --ui-border: {c.border}; --ui-height: {t.tabs.height}px; --ui-padding-x: {t.tabs.paddingX}px; --ui-radius: {t.tabs.radius}px; --ui-indicator-height: {t.tabs.indicator}px; --ui-gap: {gap}px; --ui-focus: {t.colors.accent}; --ui-disabled: {t.colors.buttonDisabledText}; --ui-font: {font.fontWeight} {font.fontSize}px/{font.lineHeight}px {t.typography.fontFamily}; {userStyle}"
83
+ {...rest}
84
+ >
85
+ <div role="tablist" tabindex="-1" {onkeydown}>
86
+ {#each tabs as tab, i (tab)}
87
+ <button
88
+ bind:this={buttons[i]}
89
+ type="button"
90
+ role="tab"
91
+ id="{uid}-tab-{tab.key}"
92
+ aria-controls="{uid}-panel-{tab.key}"
93
+ aria-selected={tab.key === current}
94
+ tabindex={tab.key === current ? 0 : -1}
95
+ disabled={tab.disabled}
96
+ onclick={() => select(tab.key)}
97
+ >
98
+ {tab.title}
99
+ </button>
100
+ {/each}
101
+ </div>
102
+ {@render children()}
103
+ </div>
104
+ {/if}
105
+
106
+ <style>
107
+ .ui-tabs {
108
+ display: flex;
109
+ flex-direction: column;
110
+ gap: var(--ui-gap);
111
+ align-self: stretch;
112
+ }
113
+ [role='tablist'] {
114
+ display: flex;
115
+ gap: 4px;
116
+ border-bottom: 1px solid var(--ui-border);
117
+ }
118
+ button {
119
+ height: var(--ui-height);
120
+ padding: 0 var(--ui-padding-x);
121
+ border: 0;
122
+ border-radius: var(--ui-radius) var(--ui-radius) 0 0;
123
+ background: transparent;
124
+ color: var(--ui-text);
125
+ font: var(--ui-font);
126
+ cursor: pointer;
127
+ box-shadow: inset 0 calc(-1 * var(--ui-indicator-height)) 0 transparent;
128
+ transition: background-color 0.2s, box-shadow 0.2s, color 0.2s;
129
+ }
130
+ button:hover:not(:disabled) {
131
+ background: var(--ui-hover);
132
+ }
133
+ button[aria-selected='true'] {
134
+ color: var(--ui-text-active);
135
+ box-shadow: inset 0 calc(-1 * var(--ui-indicator-height)) 0 var(--ui-indicator);
136
+ }
137
+ button:disabled {
138
+ color: var(--ui-disabled);
139
+ cursor: not-allowed;
140
+ }
141
+ button:focus-visible {
142
+ outline: 3px solid var(--ui-focus);
143
+ outline-offset: -3px;
144
+ }
145
+ </style>
@@ -0,0 +1,13 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { Dimension } from './theme';
3
+ type Props = {
4
+ children: Snippet;
5
+ active?: string;
6
+ onchange?: (key: string) => void;
7
+ gap?: number;
8
+ dimension?: Dimension;
9
+ [attribute: string]: unknown;
10
+ };
11
+ declare const Tabs: import("svelte").Component<Props, {}, "active">;
12
+ type Tabs = ReturnType<typeof Tabs>;
13
+ export default Tabs;
@@ -0,0 +1,71 @@
1
+ <script lang="ts">// Base UI-style Tag: a small status chip ("Live", "3 players"). Not interactive.
2
+ import { ensure3d } from "./aframe";
3
+ import { watchTextSize } from "./aframe/measure";
4
+ import { resolveTagStyle } from "./component-style";
5
+ import { getDimension, getTheme } from "./context";
6
+ import { font3d } from "./fonts";
7
+ import { useSlot } from "./layout.svelte";
8
+ let { label, kind, variant, size, dimension, style: userStyle = "", ...rest } = $props();
9
+ const theme = getTheme();
10
+ const t = $derived(theme());
11
+ const s = $derived(resolveTagStyle(t, {
12
+ kind,
13
+ variant,
14
+ size
15
+ }));
16
+ const contextDimension = getDimension();
17
+ // svelte-ignore state_referenced_locally
18
+ const is3d = (dimension ?? contextDimension) === "3d";
19
+ // ---- 3D ----
20
+ const m = (px) => px * t.space3d.metersPerPx;
21
+ if (is3d) ensure3d();
22
+ let textWidth = $state(0);
23
+ let labelEl = $state();
24
+ const height = $derived(m(s.height));
25
+ const width = $derived(Math.max(height, textWidth + 2 * m(s.paddingX)));
26
+ const slot = is3d ? useSlot(() => ({
27
+ width,
28
+ height
29
+ })) : () => ({
30
+ x: 0,
31
+ y: 0
32
+ });
33
+ const depth = .01;
34
+ $effect(() => {
35
+ if (labelEl) return watchTextSize(labelEl, (w) => textWidth = w);
36
+ });
37
+ </script>
38
+
39
+ {#if is3d}
40
+ <a-entity {...rest}>
41
+ <a-entity position="{slot().x} {slot().y} 0">
42
+ <a-entity
43
+ rounded="width: {width}; height: {height}; depth: {depth}; radius: {m(s.radius)}; color: {s.fill}"
44
+ position="{-width / 2} {-height / 2} 0"
45
+ ></a-entity>
46
+ <a-entity
47
+ bind:this={labelEl}
48
+ troika-text="value: {label.replace(/;/g, ',')}; fontSize: {m(s.font.fontSize) * t.space3d.textScale}; color: {s.text}; font: {font3d(t)}; anchor: center; baseline: center"
49
+ troika-text-material="shader: standard"
50
+ position="0 0 {depth + 0.002}"
51
+ ></a-entity>
52
+ </a-entity>
53
+ </a-entity>
54
+ {:else}
55
+ <span
56
+ class="ui-tag"
57
+ style="height: {s.height}px; padding: 0 {s.paddingX}px; border-radius: {s.radius}px; background: {s.fill}; color: {s.text}; font: {s.font.fontWeight} {s.font.fontSize}px/{s.font.lineHeight}px {t.typography.fontFamily}; {userStyle}"
58
+ {...rest}
59
+ >
60
+ {label}
61
+ </span>
62
+ {/if}
63
+
64
+ <style>
65
+ .ui-tag {
66
+ display: inline-flex;
67
+ align-items: center;
68
+ box-sizing: border-box;
69
+ white-space: nowrap;
70
+ }
71
+ </style>
@@ -0,0 +1,12 @@
1
+ import type { Dimension, TagKind, TagSize, TagVariant } from './theme';
2
+ type Props = {
3
+ label: string;
4
+ kind?: TagKind;
5
+ variant?: TagVariant;
6
+ size?: TagSize;
7
+ dimension?: Dimension;
8
+ [attribute: string]: unknown;
9
+ };
10
+ declare const Tag: import("svelte").Component<Props, {}, "">;
11
+ type Tag = ReturnType<typeof Tag>;
12
+ export default Tag;
@@ -0,0 +1,75 @@
1
+ <script lang="ts">// Typography. 2D: a <span>. 3D: troika text, anchored by `align` (default center).
2
+ // Inside a Panel / ButtonGroup it measures itself and takes its place in the stack.
3
+ import { ensure3d } from "./aframe";
4
+ import { setTroikaValue, watchTextSize } from "./aframe/measure";
5
+ import { getDimension, getTheme } from "./context";
6
+ import { font3d } from "./fonts";
7
+ import { inStack, useSlot } from "./layout.svelte";
8
+ let { text = "", children, variant = "ParagraphMedium", color, align, maxWidth, dimension, style: userStyle = "", ...rest } = $props();
9
+ const contextDimension = getDimension();
10
+ const theme = getTheme();
11
+ const t = $derived(theme());
12
+ const font = $derived(t.typography[variant]);
13
+ const textColor = $derived(color ?? t.colors.contentPrimary);
14
+ const is3d = $derived((dimension ?? contextDimension) === "3d");
15
+ const anchor = $derived(align ?? "center");
16
+ // 3D only.
17
+ let size = $state({
18
+ width: 0,
19
+ height: 0
20
+ });
21
+ let textEl = $state();
22
+ // svelte-ignore state_referenced_locally
23
+ const stacked = (dimension ?? contextDimension) === "3d" && inStack();
24
+ const slot = stacked ? useSlot(() => size) : () => ({
25
+ x: 0,
26
+ y: 0
27
+ });
28
+ // In a stack the slot is the text's center, so shift left/right-anchored text to fill it.
29
+ // Outside one, the anchor is relative to the entity's own position.
30
+ const anchorShift = $derived(!stacked ? 0 : anchor === "left" ? -size.width / 2 : anchor === "right" ? size.width / 2 : 0);
31
+ $effect(() => {
32
+ if (is3d) ensure3d();
33
+ });
34
+ $effect(() => {
35
+ if (textEl) return watchTextSize(textEl, (width, height) => size = {
36
+ width,
37
+ height
38
+ });
39
+ });
40
+ // Set apart from the attribute string, which A-Frame splits on `;` (chat text can have one).
41
+ $effect(() => setTroikaValue(textEl, text));
42
+ const troika = $derived.by(() => {
43
+ const m = t.space3d.metersPerPx;
44
+ return [
45
+ `fontSize: ${font.fontSize * m * t.space3d.textScale}`,
46
+ `lineHeight: ${font.lineHeight / font.fontSize}`,
47
+ `color: ${textColor}`,
48
+ `font: ${font3d(t)}`,
49
+ `align: ${anchor}`,
50
+ `anchor: ${anchor}`,
51
+ maxWidth ? `maxWidth: ${maxWidth * m}` : ""
52
+ ].filter(Boolean).join("; ");
53
+ });
54
+ </script>
55
+
56
+ {#if is3d}
57
+ <a-entity {...rest}>
58
+ <a-entity
59
+ bind:this={textEl}
60
+ troika-text={troika}
61
+ troika-text-material="shader: standard"
62
+ position="{slot().x + anchorShift} {slot().y} 0"
63
+ ></a-entity>
64
+ </a-entity>
65
+ {:else}
66
+ <span
67
+ class="ui-text"
68
+ style="font: {font.fontWeight} {font.fontSize}px/{font.lineHeight}px {t.typography.fontFamily}; color: {textColor}; text-align: {align ?? 'inherit'}; {maxWidth
69
+ ? `max-width: ${maxWidth}px; display: inline-block;`
70
+ : ''} {userStyle}"
71
+ {...rest}
72
+ >
73
+ {#if children}{@render children()}{:else}{text}{/if}
74
+ </span>
75
+ {/if}
@@ -0,0 +1,4 @@
1
+ import type { TextProps } from './types';
2
+ declare const Text: import("svelte").Component<TextProps, {}, "">;
3
+ type Text = ReturnType<typeof Text>;
4
+ export default Text;
@@ -0,0 +1,84 @@
1
+ <script lang="ts">// Internal: one line of editable text in 3D — troika text clipped to a window, a
2
+ // blinking caret, a placeholder, and horizontal scroll that keeps the caret in view.
3
+ // Clicking it reports the caret index under the cursor (`onpick`). Used by Input's
4
+ // field and the VR keyboard's preview line. Centered on its position.
5
+ import { onDestroy } from "svelte";
6
+ import { setTroikaValue, watchTextLayout } from "./aframe/measure";
7
+ import { getTheme } from "./context";
8
+ import { font3d } from "./fonts";
9
+ import { caretIndexAt, caretX, scrollToCaret } from "./text-edit";
10
+ let { text, caret, focused = false, placeholder = "", font, color, placeholderColor, caretColor, width, hitWidth = width, hitHeight, interactive = true, onpick } = $props();
11
+ const theme = getTheme();
12
+ const t = $derived(theme());
13
+ const m = (px) => px * t.space3d.metersPerPx;
14
+ const fontSize = $derived(m(font.fontSize) * t.space3d.textScale);
15
+ const lineHeight = $derived(m(font.lineHeight) * t.space3d.textScale);
16
+ let textEl = $state();
17
+ let placeholderEl = $state();
18
+ // troika's caret positions for the text as last laid out (it lays out asynchronously).
19
+ let positions = $state.raw(null);
20
+ const laidOut = $derived(positions ? positions.length / 4 : 0);
21
+ const caretAt = $derived(caretX(Math.min(caret, laidOut), positions, laidOut));
22
+ const textWidth = $derived(caretX(laidOut, positions, laidOut));
23
+ let scroll = $state(0);
24
+ $effect(() => {
25
+ scroll = scrollToCaret(caretAt, scroll, width, textWidth);
26
+ });
27
+ $effect(() => {
28
+ if (textEl) return watchTextLayout(textEl, (info) => positions = info.caretPositions ?? null);
29
+ });
30
+ $effect(() => setTroikaValue(textEl, text));
31
+ $effect(() => setTroikaValue(placeholderEl, placeholder));
32
+ // Blink, solid again whenever the text or caret moves. A timer, not
33
+ // requestAnimationFrame, so it keeps going in an immersive session.
34
+ let blinkOn = $state(true);
35
+ let timer;
36
+ $effect(() => {
37
+ void text;
38
+ void caret;
39
+ blinkOn = true;
40
+ clearInterval(timer);
41
+ if (focused) timer = setInterval(() => blinkOn = !blinkOn, 530);
42
+ });
43
+ onDestroy(() => clearInterval(timer));
44
+ const common = $derived(`fontSize: ${fontSize}; color: ${color}; font: ${font3d(t)}; anchor: left; baseline: center; whiteSpace: nowrap`);
45
+ function pick(event) {
46
+ const point = event.detail?.intersection?.point;
47
+ const obj = textEl?.object3D;
48
+ if (!point || !obj || !onpick) return;
49
+ const x = obj.worldToLocal(point.clone()).x;
50
+ onpick(caretIndexAt(x, positions, laidOut));
51
+ }
52
+ </script>
53
+
54
+ <!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
55
+ <a-entity onclick={pick}>
56
+ <!-- Hit area (the raycaster ignores `visible`). -->
57
+ <a-entity
58
+ class={interactive ? 'raycastable' : ''}
59
+ geometry="primitive: plane; width: {hitWidth}; height: {hitHeight ?? lineHeight * 1.6}"
60
+ material="visible: false"
61
+ position="0 0 0.003"
62
+ ></a-entity>
63
+ <a-entity
64
+ bind:this={textEl}
65
+ troika-text="{common}; clipRect: {scroll} {-lineHeight} {scroll + width} {lineHeight}"
66
+ troika-text-material="shader: standard"
67
+ position="{-width / 2 - scroll} 0 0.001"
68
+ ></a-entity>
69
+ {#if !text && placeholder}
70
+ <a-entity
71
+ bind:this={placeholderEl}
72
+ troika-text="{common}; color: {placeholderColor}; clipRect: 0 {-lineHeight} {width} {lineHeight}"
73
+ troika-text-material="shader: standard"
74
+ position="{-width / 2} 0 0.001"
75
+ ></a-entity>
76
+ {/if}
77
+ {#if focused}
78
+ <a-entity
79
+ geometry="primitive: plane; width: {m(t.input.caretWidth) * t.space3d.textScale}; height: {fontSize * 1.25}"
80
+ material="shader: flat; color: {caretColor}; opacity: {blinkOn ? 1 : 0}; transparent: true"
81
+ position="{-width / 2 + caretAt - scroll} 0 0.002"
82
+ ></a-entity>
83
+ {/if}
84
+ </a-entity>
@@ -0,0 +1,19 @@
1
+ import type { Font } from './theme';
2
+ type Props = {
3
+ text: string;
4
+ caret: number;
5
+ focused?: boolean;
6
+ placeholder?: string;
7
+ font: Font;
8
+ color: string;
9
+ placeholderColor: string;
10
+ caretColor: string;
11
+ width: number;
12
+ hitWidth?: number;
13
+ hitHeight?: number;
14
+ interactive?: boolean;
15
+ onpick?: (index: number) => void;
16
+ };
17
+ declare const TextLine3D: import("svelte").Component<Props, {}, "">;
18
+ type TextLine3D = ReturnType<typeof TextLine3D>;
19
+ export default TextLine3D;
@@ -0,0 +1,7 @@
1
+ <script lang="ts">import { setTheme } from "./context";
2
+ import { defaultTheme } from "./theme";
3
+ let { theme = defaultTheme, children } = $props();
4
+ setTheme(() => theme);
5
+ </script>
6
+
7
+ {@render children()}
@@ -0,0 +1,9 @@
1
+ import type { Snippet } from 'svelte';
2
+ import { type Theme } from './theme';
3
+ type $$ComponentProps = {
4
+ theme?: Theme;
5
+ children: Snippet;
6
+ };
7
+ declare const ThemeProvider: import("svelte").Component<$$ComponentProps, {}, "">;
8
+ type ThemeProvider = ReturnType<typeof ThemeProvider>;
9
+ export default ThemeProvider;
@@ -0,0 +1,104 @@
1
+ <script lang="ts">// Shows the toast queue (toast.svelte.ts). 2D: fixed in a corner of the page.
2
+ // 3D: a column whose `placement` corner sits at this entity's position (bottom-right:
3
+ // grows up and to the left from it) — e.g. under the camera, low and to the side.
4
+ import { flip } from "svelte/animate";
5
+ import { fly } from "svelte/transition";
6
+ import Button from "./Button.svelte";
7
+ import { getDimension, getTheme } from "./context";
8
+ import { createStack, setStack, useSlot } from "./layout.svelte";
9
+ import Panel from "./Panel.svelte";
10
+ import Text from "./Text.svelte";
11
+ import { toaster } from "./toast.svelte";
12
+ let { placement = "bottom-right", max = 4, dimension, ...rest } = $props();
13
+ const theme = getTheme();
14
+ const t = $derived(theme());
15
+ const tk = $derived(t.toast);
16
+ const visible = $derived(toaster.items.slice(-max));
17
+ const contextDimension = getDimension();
18
+ // svelte-ignore state_referenced_locally
19
+ const is3d = (dimension ?? contextDimension) === "3d";
20
+ const m = (px) => px * t.space3d.metersPerPx;
21
+ const column = is3d ? createStack(() => ({
22
+ direction: "column",
23
+ gap: m(8),
24
+ align: placement.endsWith("left") ? "start" : placement.endsWith("right") ? "end" : "center"
25
+ })) : undefined;
26
+ const slot = is3d ? useSlot(() => column.size) : () => ({
27
+ x: 0,
28
+ y: 0
29
+ });
30
+ setStack(column);
31
+ // Shift the column so its `placement` corner/edge is at the origin.
32
+ const anchor = $derived({
33
+ x: placement.endsWith("right") ? -(column?.size.width ?? 0) / 2 : placement.endsWith("left") ? (column?.size.width ?? 0) / 2 : 0,
34
+ y: placement.startsWith("bottom") ? (column?.size.height ?? 0) / 2 : -(column?.size.height ?? 0) / 2
35
+ });
36
+ </script>
37
+
38
+ {#snippet toastPanel(toast: Toast)}
39
+ {@const colors = t.colors.toast[toast.kind]}
40
+ <Panel
41
+ fill={colors.fill}
42
+ width={tk.width}
43
+ padding={tk.padding}
44
+ style="border-left: 4px solid {colors.accent};"
45
+ role={toast.kind === 'negative' ? 'alert' : 'status'}
46
+ >
47
+ {#snippet action()}
48
+ <Button label="×" kind="tertiary" shape="circle" size="mini" aria-label="Dismiss" onclick={() => toaster.dismiss(toast.id)} />
49
+ {/snippet}
50
+ <Text variant="LabelMedium" align="left" color={colors.text} maxWidth={tk.width - 2 * tk.padding - 32} text={toast.message} />
51
+ </Panel>
52
+ {/snippet}
53
+
54
+ {#if is3d}
55
+ <a-entity {...rest}>
56
+ <a-entity position="{slot().x + anchor.x} {slot().y + anchor.y} 0">
57
+ {#each visible as toast (toast.id)}
58
+ <!-- Slides forward into place when it appears. -->
59
+ <a-entity animation__in="property: position; from: 0 0 -0.08; to: 0 0 0; dur: {t.animation.timing300}; easing: {t.animation.easeOut3d}">
60
+ {@render toastPanel(toast)}
61
+ </a-entity>
62
+ {/each}
63
+ </a-entity>
64
+ </a-entity>
65
+ {:else}
66
+ <div class="ui-toaster" data-placement={placement} aria-live="polite" {...rest}>
67
+ {#each visible as toast (toast.id)}
68
+ <div animate:flip={{ duration: 200 }} transition:fly={{ y: placement.startsWith('top') ? -16 : 16, duration: 200 }}>
69
+ {@render toastPanel(toast)}
70
+ </div>
71
+ {/each}
72
+ </div>
73
+ {/if}
74
+
75
+ <style>
76
+ .ui-toaster {
77
+ position: fixed;
78
+ z-index: 1000;
79
+ display: flex;
80
+ flex-direction: column;
81
+ gap: 8px;
82
+ pointer-events: none;
83
+ }
84
+ .ui-toaster > :global(*) {
85
+ pointer-events: auto;
86
+ }
87
+ [data-placement^='top'] {
88
+ top: 16px;
89
+ }
90
+ [data-placement^='bottom'] {
91
+ bottom: 16px;
92
+ }
93
+ [data-placement$='left'] {
94
+ left: 16px;
95
+ }
96
+ [data-placement$='right'] {
97
+ right: 16px;
98
+ }
99
+ [data-placement='top'],
100
+ [data-placement='bottom'] {
101
+ left: 50%;
102
+ transform: translateX(-50%);
103
+ }
104
+ </style>
@@ -0,0 +1,11 @@
1
+ import type { Dimension } from './theme';
2
+ type Placement = 'top-left' | 'top' | 'top-right' | 'bottom-left' | 'bottom' | 'bottom-right';
3
+ type Props = {
4
+ placement?: Placement;
5
+ max?: number;
6
+ dimension?: Dimension;
7
+ [attribute: string]: unknown;
8
+ };
9
+ declare const Toaster: import("svelte").Component<Props, {}, "">;
10
+ type Toaster = ReturnType<typeof Toaster>;
11
+ export default Toaster;