@tableslayer/ui 0.1.3 → 0.1.4

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 (205) hide show
  1. package/package.json +2 -13
  2. package/src/lib/components/Avatar/Avatar.svelte +82 -0
  3. package/src/lib/components/Avatar/AvatarFileInput.svelte +85 -0
  4. package/src/lib/components/Avatar/AvatarPopover.svelte +34 -0
  5. package/src/lib/components/Avatar/index.ts +4 -0
  6. package/src/lib/components/Avatar/types.ts +24 -0
  7. package/src/lib/components/BrushSizeSlider/BrushSizeSlider.svelte +174 -0
  8. package/src/lib/components/BrushSizeSlider/index.ts +1 -0
  9. package/src/lib/components/Button/Button.svelte +182 -0
  10. package/src/lib/components/Button/ConfirmActionButton.svelte +98 -0
  11. package/src/lib/components/Button/IconButton.svelte +121 -0
  12. package/src/lib/components/Button/RadioButton.svelte +93 -0
  13. package/src/lib/components/Button/index.ts +5 -0
  14. package/src/lib/components/Button/types.ts +54 -0
  15. package/src/lib/components/CardFan/CardFan.svelte +165 -0
  16. package/src/lib/components/CardFan/index.ts +2 -0
  17. package/src/lib/components/CardFan/types.ts +6 -0
  18. package/src/lib/components/CodeBlock/Code.svelte +7 -0
  19. package/src/lib/components/CodeBlock/CodeBlock.svelte +102 -0
  20. package/src/lib/components/CodeBlock/index.ts +3 -0
  21. package/src/lib/components/CodeBlock/types.ts +10 -0
  22. package/src/lib/components/ColorMode/ColorMode.svelte +8 -0
  23. package/src/lib/components/ColorMode/index.ts +2 -0
  24. package/src/lib/components/ColorMode/types.ts +12 -0
  25. package/src/lib/components/ColorPicker/ColorPicker.svelte +838 -0
  26. package/src/lib/components/ColorPicker/ColorPickerSwatch.svelte +32 -0
  27. package/src/lib/components/ColorPicker/index.ts +3 -0
  28. package/src/lib/components/ColorPicker/types.ts +51 -0
  29. package/src/lib/components/ContextMenu/ContextMenu.svelte +86 -0
  30. package/src/lib/components/ContextMenu/index.ts +2 -0
  31. package/src/lib/components/ContextMenu/types.ts +15 -0
  32. package/src/lib/components/DrawingSliders/DrawingSliders.svelte +379 -0
  33. package/src/lib/components/DrawingSliders/index.ts +1 -0
  34. package/src/lib/components/Editor/Editor.svelte +825 -0
  35. package/src/lib/components/Editor/index.ts +1 -0
  36. package/src/lib/components/FogSliders/FogSliders.svelte +33 -0
  37. package/src/lib/components/FogSliders/index.ts +1 -0
  38. package/src/lib/components/Hr/Hr.svelte +15 -0
  39. package/src/lib/components/Hr/index.ts +1 -0
  40. package/src/lib/components/Icon/Icon.svelte +6 -0
  41. package/src/lib/components/Icon/index.ts +2 -0
  42. package/src/lib/components/Icon/types.ts +20 -0
  43. package/src/lib/components/Input/DualInputSlider.svelte +126 -0
  44. package/src/lib/components/Input/FileInput.svelte +176 -0
  45. package/src/lib/components/Input/FormControl.svelte +150 -0
  46. package/src/lib/components/Input/FormError.svelte +37 -0
  47. package/src/lib/components/Input/Input.svelte +56 -0
  48. package/src/lib/components/Input/InputCheckbox.svelte +99 -0
  49. package/src/lib/components/Input/InputSlider.svelte +86 -0
  50. package/src/lib/components/Input/Label.svelte +19 -0
  51. package/src/lib/components/Input/index.ts +9 -0
  52. package/src/lib/components/Input/types.ts +39 -0
  53. package/src/lib/components/Link/Link.svelte +41 -0
  54. package/src/lib/components/Link/LinkBox.svelte +20 -0
  55. package/src/lib/components/Link/LinkOverlay.svelte +23 -0
  56. package/src/lib/components/Link/index.ts +4 -0
  57. package/src/lib/components/Link/types.ts +17 -0
  58. package/src/lib/components/Loading/Loader.svelte +60 -0
  59. package/src/lib/components/Loading/Skeleton.svelte +9 -0
  60. package/src/lib/components/Loading/index.ts +2 -0
  61. package/src/lib/components/Logo/Logo.svelte +16 -0
  62. package/src/lib/components/Logo/index.ts +1 -0
  63. package/src/lib/components/MarkerTooltip/MarkerTooltip.svelte +435 -0
  64. package/src/lib/components/MarkerTooltip/index.ts +1 -0
  65. package/src/lib/components/Menu/SelectorMenu.svelte +280 -0
  66. package/src/lib/components/Menu/index.ts +2 -0
  67. package/src/lib/components/Menu/types.ts +17 -0
  68. package/src/lib/components/MyCounterButton.svelte +11 -0
  69. package/src/lib/components/Panel/index.ts +2 -0
  70. package/src/lib/components/Panel/panel.svelte +18 -0
  71. package/src/lib/components/Panel/types.ts +8 -0
  72. package/src/lib/components/PersistButton/PersistButton.svelte +100 -0
  73. package/src/lib/components/PersistButton/index.ts +1 -0
  74. package/src/lib/components/Popover/Popover.svelte +81 -0
  75. package/src/lib/components/Popover/index.ts +2 -0
  76. package/src/lib/components/Popover/types.ts +19 -0
  77. package/src/lib/components/PropsTable/PropsTable.svelte +107 -0
  78. package/src/lib/components/RadialMenu/EffectPreview.svelte +36 -0
  79. package/src/lib/components/RadialMenu/EffectPreviewScene.svelte +194 -0
  80. package/src/lib/components/RadialMenu/RadialMenu.svelte +503 -0
  81. package/src/lib/components/RadialMenu/RadialMenuItem.svelte +176 -0
  82. package/src/lib/components/RadialMenu/index.ts +2 -0
  83. package/src/lib/components/RadialMenu/types.ts +35 -0
  84. package/src/lib/components/Select/Select.svelte +342 -0
  85. package/src/lib/components/Select/index.ts +2 -0
  86. package/src/lib/components/Select/types.ts +22 -0
  87. package/src/lib/components/Spacer/Spacer.svelte +14 -0
  88. package/src/lib/components/Spacer/index.ts +2 -0
  89. package/src/lib/components/Spacer/types.ts +5 -0
  90. package/src/lib/components/Stage/components/AnnotationLayer/AnnotationLayer.svelte +445 -0
  91. package/src/lib/components/Stage/components/AnnotationLayer/AnnotationMaterial.svelte +167 -0
  92. package/src/lib/components/Stage/components/AnnotationLayer/types.ts +196 -0
  93. package/src/lib/components/Stage/components/CursorLayer/CursorLayer.svelte +148 -0
  94. package/src/lib/components/Stage/components/CursorLayer/cursor.svg +26 -0
  95. package/src/lib/components/Stage/components/CursorLayer/index.ts +2 -0
  96. package/src/lib/components/Stage/components/CursorLayer/types.ts +23 -0
  97. package/src/lib/components/Stage/components/DrawingLayer/DrawingMaterial.svelte +364 -0
  98. package/src/lib/components/Stage/components/DrawingLayer/types.ts +65 -0
  99. package/src/lib/components/Stage/components/EdgeOverlayLayer/EdgeOverlayLayer.svelte +72 -0
  100. package/src/lib/components/Stage/components/EdgeOverlayLayer/types.ts +34 -0
  101. package/src/lib/components/Stage/components/FogLayer/FogLayer.svelte +75 -0
  102. package/src/lib/components/Stage/components/FogLayer/types.ts +51 -0
  103. package/src/lib/components/Stage/components/FogOfWarLayer/FogOfWarLayer.svelte +249 -0
  104. package/src/lib/components/Stage/components/FogOfWarLayer/FogOfWarMaterial.svelte +200 -0
  105. package/src/lib/components/Stage/components/FogOfWarLayer/types.ts +116 -0
  106. package/src/lib/components/Stage/components/GridLayer/GridLayer.svelte +20 -0
  107. package/src/lib/components/Stage/components/GridLayer/GridMaterial.svelte +69 -0
  108. package/src/lib/components/Stage/components/GridLayer/types.ts +79 -0
  109. package/src/lib/components/Stage/components/LayerInput/LayerInput.svelte +300 -0
  110. package/src/lib/components/Stage/components/MapLayer/MapLayer.svelte +196 -0
  111. package/src/lib/components/Stage/components/MapLayer/dataSources/GifDataSource.ts +265 -0
  112. package/src/lib/components/Stage/components/MapLayer/dataSources/IMapDataSource.ts +55 -0
  113. package/src/lib/components/Stage/components/MapLayer/dataSources/ImageDataSource.ts +87 -0
  114. package/src/lib/components/Stage/components/MapLayer/dataSources/VideoDataSource.ts +150 -0
  115. package/src/lib/components/Stage/components/MapLayer/dataSources/dataSourceFactory.ts +48 -0
  116. package/src/lib/components/Stage/components/MapLayer/dataSources/index.ts +16 -0
  117. package/src/lib/components/Stage/components/MapLayer/types.ts +58 -0
  118. package/src/lib/components/Stage/components/MarkerLayer/MarkerLayer.svelte +398 -0
  119. package/src/lib/components/Stage/components/MarkerLayer/MarkerToken.svelte +262 -0
  120. package/src/lib/components/Stage/components/MarkerLayer/types.ts +126 -0
  121. package/src/lib/components/Stage/components/MeasurementLayer/MeasurementLayer.svelte +364 -0
  122. package/src/lib/components/Stage/components/MeasurementLayer/MeasurementManager.svelte +473 -0
  123. package/src/lib/components/Stage/components/MeasurementLayer/measurements/BaseMeasurement.ts +427 -0
  124. package/src/lib/components/Stage/components/MeasurementLayer/measurements/BeamMeasurement.ts +105 -0
  125. package/src/lib/components/Stage/components/MeasurementLayer/measurements/CircleMeasurement.ts +98 -0
  126. package/src/lib/components/Stage/components/MeasurementLayer/measurements/ConeMeasurement.ts +163 -0
  127. package/src/lib/components/Stage/components/MeasurementLayer/measurements/LineMeasurement.ts +102 -0
  128. package/src/lib/components/Stage/components/MeasurementLayer/measurements/RectangleMeasurement.ts +120 -0
  129. package/src/lib/components/Stage/components/MeasurementLayer/measurements/index.ts +7 -0
  130. package/src/lib/components/Stage/components/MeasurementLayer/types.ts +94 -0
  131. package/src/lib/components/Stage/components/MeasurementLayer/utils/canvasDrawing.ts +357 -0
  132. package/src/lib/components/Stage/components/MeasurementLayer/utils/distanceCalculations.ts +170 -0
  133. package/src/lib/components/Stage/components/ParticleSystem/ParticleSystem.svelte +220 -0
  134. package/src/lib/components/Stage/components/ParticleSystem/particles/atlases/ash.png +0 -0
  135. package/src/lib/components/Stage/components/ParticleSystem/particles/atlases/leaves.png +0 -0
  136. package/src/lib/components/Stage/components/ParticleSystem/particles/atlases/rain.png +0 -0
  137. package/src/lib/components/Stage/components/ParticleSystem/particles/atlases/snow.png +0 -0
  138. package/src/lib/components/Stage/components/ParticleSystem/rng.js +20 -0
  139. package/src/lib/components/Stage/components/ParticleSystem/types.ts +95 -0
  140. package/src/lib/components/Stage/components/PerformanceDebugger/PerformanceDebugger.svelte +144 -0
  141. package/src/lib/components/Stage/components/PerformanceDebugger/index.ts +1 -0
  142. package/src/lib/components/Stage/components/PerformanceOverlay/PerformanceOverlay.svelte +208 -0
  143. package/src/lib/components/Stage/components/PerformanceOverlay/index.ts +1 -0
  144. package/src/lib/components/Stage/components/PointerInputManager/PointerInputManager.svelte +201 -0
  145. package/src/lib/components/Stage/components/Scene/Scene.svelte +651 -0
  146. package/src/lib/components/Stage/components/Scene/luts.ts +24 -0
  147. package/src/lib/components/Stage/components/Scene/types.ts +225 -0
  148. package/src/lib/components/Stage/components/Stage/Stage.svelte +332 -0
  149. package/src/lib/components/Stage/components/Stage/types.ts +136 -0
  150. package/src/lib/components/Stage/components/WeatherLayer/WeatherLayer.svelte +135 -0
  151. package/src/lib/components/Stage/components/WeatherLayer/presets/AshPreset.ts +71 -0
  152. package/src/lib/components/Stage/components/WeatherLayer/presets/LeavesPreset.ts +70 -0
  153. package/src/lib/components/Stage/components/WeatherLayer/presets/RainPreset.ts +68 -0
  154. package/src/lib/components/Stage/components/WeatherLayer/presets/SnowPreset.ts +70 -0
  155. package/src/lib/components/Stage/components/WeatherLayer/presets/index.ts +6 -0
  156. package/src/lib/components/Stage/components/WeatherLayer/types.ts +35 -0
  157. package/src/lib/components/Stage/helpers/clippingPlaneStore.svelte.ts +28 -0
  158. package/src/lib/components/Stage/helpers/debugState.svelte.ts +18 -0
  159. package/src/lib/components/Stage/helpers/grid.ts +548 -0
  160. package/src/lib/components/Stage/helpers/lazyBrush.ts +171 -0
  161. package/src/lib/components/Stage/helpers/performanceMetrics.svelte.ts +220 -0
  162. package/src/lib/components/Stage/helpers/utils.ts +21 -0
  163. package/src/lib/components/Stage/index.ts +49 -0
  164. package/src/lib/components/Stage/shaders/AnnotationEffects.frag +1070 -0
  165. package/src/lib/components/Stage/shaders/Annotations.frag +29 -0
  166. package/src/lib/components/Stage/shaders/Drawing.frag +83 -0
  167. package/src/lib/components/Stage/shaders/Drawing.vert +5 -0
  168. package/src/lib/components/Stage/shaders/Fog.frag +147 -0
  169. package/src/lib/components/Stage/shaders/FractalNoise.frag +96 -0
  170. package/src/lib/components/Stage/shaders/GridShader.frag +174 -0
  171. package/src/lib/components/Stage/shaders/Overlay.frag +23 -0
  172. package/src/lib/components/Stage/shaders/Overlay.vert +0 -0
  173. package/src/lib/components/Stage/shaders/Particles.frag +27 -0
  174. package/src/lib/components/Stage/shaders/Particles.vert +51 -0
  175. package/src/lib/components/Stage/shaders/ToolOutline.frag +59 -0
  176. package/src/lib/components/Stage/shaders/default.vert +8 -0
  177. package/src/lib/components/Stage/types.ts +4 -0
  178. package/src/lib/components/Table/Table.svelte +16 -0
  179. package/src/lib/components/Table/Td.svelte +17 -0
  180. package/src/lib/components/Table/Th.svelte +18 -0
  181. package/src/lib/components/Table/index.ts +4 -0
  182. package/src/lib/components/Table/types.ts +14 -0
  183. package/src/lib/components/Text/Text.svelte +23 -0
  184. package/src/lib/components/Text/index.ts +2 -0
  185. package/src/lib/components/Text/types.ts +12 -0
  186. package/src/lib/components/Title/Title.svelte +54 -0
  187. package/src/lib/components/Title/index.ts +2 -0
  188. package/src/lib/components/Title/types.ts +9 -0
  189. package/src/lib/components/Toast/Toast.svelte +155 -0
  190. package/src/lib/components/Toast/index.ts +5 -0
  191. package/src/lib/components/Toast/toastCookie.ts +24 -0
  192. package/src/lib/components/Toast/types.ts +6 -0
  193. package/src/lib/components/ToolTip/ToolTip.svelte +70 -0
  194. package/src/lib/components/ToolTip/index.ts +2 -0
  195. package/src/lib/components/ToolTip/types.ts +14 -0
  196. package/src/lib/components/index.ts +32 -0
  197. package/src/lib/components/types.ts +0 -0
  198. package/src/lib/index.ts +2 -0
  199. package/src/lib/styles/globals.css +108 -0
  200. package/src/lib/styles/normalize.css +9 -0
  201. package/src/lib/styles/reset.css +133 -0
  202. package/src/lib/styles/utilities.css +179 -0
  203. package/src/lib/styles/vars.css +1103 -0
  204. package/src/lib/types/awareness.ts +17 -0
  205. package/src/lib/utils/rle.ts +217 -0
@@ -0,0 +1,503 @@
1
+ <script lang="ts">
2
+ import type { RadialMenuProps, RadialMenuItemProps, SubmenuLayout, TableFilterOption } from './types';
3
+ import RadialMenuItem from './RadialMenuItem.svelte';
4
+ import { Select } from '../Select';
5
+
6
+ const { visible = false, position, items, backIcon, onItemSelect, onClose, onReposition }: RadialMenuProps = $props();
7
+
8
+ let activeSubmenu: RadialMenuItemProps[] | null = $state(null);
9
+ let activeSubmenuLayout: SubmenuLayout = $state('radial');
10
+ let activeSubmenuFilterOptions: TableFilterOption[] | undefined = $state(undefined);
11
+ let activeSubmenuFilterKey: string | undefined = $state(undefined);
12
+ let selectedFilter: string[] = $state([]);
13
+ let menuContainer: HTMLDivElement | null = $state(null);
14
+ let adjustedPosition = $state({ x: position.x, y: position.y });
15
+ let menuRotation = $state(0); // 0, 90, 180, or 270 degrees
16
+ const TABLE_COLUMN_COUNT = 3; // Fixed number of columns in table layout
17
+
18
+ // Calculate which edge is closest and determine rotation
19
+ // The menu should rotate so items face the nearest edge (where the player is viewing from)
20
+ function calculateRotationFromEdge(x: number, y: number, viewportWidth: number, viewportHeight: number): number {
21
+ const distanceToTop = y;
22
+ const distanceToBottom = viewportHeight - y;
23
+ const distanceToLeft = x;
24
+ const distanceToRight = viewportWidth - x;
25
+
26
+ const minDistance = Math.min(distanceToTop, distanceToBottom, distanceToLeft, distanceToRight);
27
+
28
+ // Determine which edge is closest and return corresponding rotation
29
+ // Default orientation is items starting at top (facing down toward bottom edge)
30
+ if (minDistance === distanceToBottom) return 0; // Bottom edge - default orientation (items face up)
31
+ if (minDistance === distanceToRight) return 90; // Right edge - rotate 90° clockwise (items face left)
32
+ if (minDistance === distanceToTop) return 180; // Top edge - rotate 180° (items face down)
33
+ return 270; // Left edge - rotate 270° clockwise (items face right)
34
+ }
35
+
36
+ // Calculate angle for each item in the radial menu
37
+ function getItemAngle(index: number, total: number, rotationDegrees: number): number {
38
+ // Start from top (270 degrees / -90 degrees)
39
+ // Distribute items evenly around the circle
40
+ const angleStep = (2 * Math.PI) / total;
41
+ const baseAngle = -Math.PI / 2 + angleStep * index;
42
+
43
+ // Apply rotation offset (convert degrees to radians)
44
+ const rotationRadians = (rotationDegrees * Math.PI) / 180;
45
+ return baseAngle + rotationRadians;
46
+ }
47
+
48
+ function handleItemSelect(itemId: string) {
49
+ // Find the selected item
50
+ const currentItems = activeSubmenu || items;
51
+ const selectedItem = currentItems.find((item) => item.id === itemId);
52
+
53
+ if (selectedItem?.submenu && selectedItem.submenu.length > 0) {
54
+ // Show submenu with its layout type
55
+ activeSubmenu = selectedItem.submenu;
56
+ activeSubmenuLayout = selectedItem.submenuLayout || 'radial';
57
+ activeSubmenuFilterOptions = selectedItem.submenuFilterOptions;
58
+ activeSubmenuFilterKey = selectedItem.submenuFilterKey;
59
+ // Set initial filter selection
60
+ if (selectedItem.submenuFilterDefault) {
61
+ selectedFilter = [selectedItem.submenuFilterDefault];
62
+ } else if (selectedItem.submenuFilterOptions && selectedItem.submenuFilterOptions.length > 0) {
63
+ selectedFilter = [selectedItem.submenuFilterOptions[0].value];
64
+ } else {
65
+ selectedFilter = [];
66
+ }
67
+ } else {
68
+ // No submenu, trigger selection and close
69
+ if (onItemSelect) {
70
+ onItemSelect(itemId);
71
+ }
72
+ handleClose();
73
+ }
74
+ }
75
+
76
+ function resetSubmenuState() {
77
+ activeSubmenu = null;
78
+ activeSubmenuLayout = 'radial';
79
+ activeSubmenuFilterOptions = undefined;
80
+ activeSubmenuFilterKey = undefined;
81
+ selectedFilter = [];
82
+ }
83
+
84
+ function handleClose() {
85
+ resetSubmenuState();
86
+ if (onClose) {
87
+ onClose();
88
+ }
89
+ }
90
+
91
+ function handleBackdropClick() {
92
+ if (activeSubmenu) {
93
+ // If submenu is open, go back to main menu
94
+ resetSubmenuState();
95
+ } else {
96
+ // Otherwise close the menu
97
+ handleClose();
98
+ }
99
+ }
100
+
101
+ function handleBackdropContextMenu(e: MouseEvent) {
102
+ e.preventDefault();
103
+ if (onReposition) {
104
+ // Reset to main menu when repositioning
105
+ resetSubmenuState();
106
+ onReposition({ x: e.clientX, y: e.clientY });
107
+ }
108
+ }
109
+
110
+ // Two-finger touch tracking for repositioning
111
+ let twoFingerTouchStart: { x: number; y: number } | null = null;
112
+ let twoFingerHoldTimer: ReturnType<typeof setTimeout> | null = null;
113
+
114
+ function handleBackdropTouchStart(e: TouchEvent) {
115
+ if (e.touches.length === 2 && onReposition) {
116
+ // Calculate center point of two fingers
117
+ const x = (e.touches[0].clientX + e.touches[1].clientX) / 2;
118
+ const y = (e.touches[0].clientY + e.touches[1].clientY) / 2;
119
+ twoFingerTouchStart = { x, y };
120
+
121
+ // Start hold timer (500ms like the original gesture detector)
122
+ twoFingerHoldTimer = setTimeout(() => {
123
+ if (twoFingerTouchStart && onReposition) {
124
+ // Reset to main menu when repositioning
125
+ resetSubmenuState();
126
+ onReposition(twoFingerTouchStart);
127
+ }
128
+ twoFingerTouchStart = null;
129
+ }, 500);
130
+ }
131
+ }
132
+
133
+ function handleBackdropTouchEnd() {
134
+ twoFingerTouchStart = null;
135
+ if (twoFingerHoldTimer) {
136
+ clearTimeout(twoFingerHoldTimer);
137
+ twoFingerHoldTimer = null;
138
+ }
139
+ }
140
+
141
+ function handleBackdropTouchMove(e: TouchEvent) {
142
+ // Cancel if fingers moved too much or finger count changed
143
+ if (twoFingerTouchStart && e.touches.length !== 2) {
144
+ handleBackdropTouchEnd();
145
+ }
146
+ }
147
+
148
+ // Reset submenu when menu visibility changes
149
+ $effect(() => {
150
+ if (!visible) {
151
+ resetSubmenuState();
152
+ }
153
+ });
154
+
155
+ // Filter submenu items based on selected filter
156
+ const filteredSubmenu = $derived.by(() => {
157
+ if (!activeSubmenu) return [];
158
+ if (!activeSubmenuFilterKey || selectedFilter.length === 0) return activeSubmenu;
159
+ const filterValue = selectedFilter[0];
160
+ return activeSubmenu.filter((item) => {
161
+ const itemValue = (item as Record<string, unknown>)[activeSubmenuFilterKey as string];
162
+ return itemValue === filterValue;
163
+ });
164
+ });
165
+
166
+ // Helper to organize table items into fixed 3 columns, distributed evenly
167
+ const tableColumns = $derived.by(() => {
168
+ if (!filteredSubmenu.length || activeSubmenuLayout !== 'table') return [];
169
+
170
+ // Always create exactly 3 columns, distributing items evenly
171
+ const columns: RadialMenuItemProps[][] = [[], [], []];
172
+ filteredSubmenu.forEach((item, index) => {
173
+ columns[index % TABLE_COLUMN_COUNT].push(item);
174
+ });
175
+ return columns;
176
+ });
177
+
178
+ // Table rotation needs left/right swapped compared to radial menu
179
+ const tableRotation = $derived.by(() => {
180
+ if (menuRotation === 90) return 270; // Right edge: swap to -90
181
+ if (menuRotation === 270) return 90; // Left edge: swap to 90
182
+ return menuRotation; // Top (180) and bottom (0) stay the same
183
+ });
184
+
185
+ // Track last calculated position to avoid redundant calculations
186
+ let lastCalculatedPosition = { x: 0, y: 0 };
187
+ let lastVisibleState = false;
188
+
189
+ // Update position and rotation to orient menu toward nearest edge
190
+ $effect(() => {
191
+ // Skip if nothing changed
192
+ if (
193
+ visible === lastVisibleState &&
194
+ position.x === lastCalculatedPosition.x &&
195
+ position.y === lastCalculatedPosition.y
196
+ ) {
197
+ return;
198
+ }
199
+
200
+ lastVisibleState = visible;
201
+ lastCalculatedPosition = { x: position.x, y: position.y };
202
+
203
+ if (visible) {
204
+ // Account for the radial menu items extending in all directions
205
+ // Items can extend menuRadius + some padding for the item size
206
+ const itemPadding = 80; // Approximate max item width/height
207
+ const totalRadius = menuRadius + itemPadding;
208
+ const padding = 10; // Additional screen edge padding
209
+
210
+ // Get viewport dimensions
211
+ const viewportWidth = window.innerWidth;
212
+ const viewportHeight = window.innerHeight;
213
+
214
+ // Start with the click position
215
+ let x = position.x;
216
+ let y = position.y;
217
+
218
+ // Clamp X position to keep items in viewport
219
+ const minX = totalRadius + padding;
220
+ const maxX = viewportWidth - totalRadius - padding;
221
+ x = Math.max(minX, Math.min(maxX, x));
222
+
223
+ // Clamp Y position to keep items in viewport
224
+ const minY = totalRadius + padding;
225
+ const maxY = viewportHeight - totalRadius - padding;
226
+ y = Math.max(minY, Math.min(maxY, y));
227
+
228
+ adjustedPosition = { x, y };
229
+
230
+ // Calculate rotation based on nearest edge
231
+ const rotation = calculateRotationFromEdge(position.x, position.y, viewportWidth, viewportHeight);
232
+ menuRotation = rotation;
233
+ } else {
234
+ // Reset to original position when hidden
235
+ adjustedPosition = { x: position.x, y: position.y };
236
+ menuRotation = 0;
237
+ }
238
+ });
239
+
240
+ const currentItems = $derived(activeSubmenu || items);
241
+ const menuRadius = 120; // Distance from center to items
242
+ </script>
243
+
244
+ {#if visible}
245
+ <div class="radialMenu">
246
+ <!-- Backdrop to catch clicks outside menu -->
247
+ <button
248
+ class="radialMenu__backdrop"
249
+ onclick={handleBackdropClick}
250
+ oncontextmenu={handleBackdropContextMenu}
251
+ ontouchstart={handleBackdropTouchStart}
252
+ ontouchend={handleBackdropTouchEnd}
253
+ ontouchmove={handleBackdropTouchMove}
254
+ ontouchcancel={handleBackdropTouchEnd}
255
+ type="button"
256
+ aria-label="Close menu"
257
+ ></button>
258
+
259
+ <!-- Menu container positioned at touch point -->
260
+ <div
261
+ bind:this={menuContainer}
262
+ class="radialMenu__container"
263
+ class:radialMenu__container--table={activeSubmenuLayout === 'table'}
264
+ style:left="{adjustedPosition.x}px"
265
+ style:top="{adjustedPosition.y}px"
266
+ >
267
+ {#if activeSubmenu && activeSubmenuLayout === 'table'}
268
+ <!-- Table layout for submenus like scene lists -->
269
+ <div class="radialMenu__table" style="transform: rotate({tableRotation}deg);">
270
+ <div class="radialMenu__tableHeader">
271
+ <button class="radialMenu__tableBack" onclick={() => resetSubmenuState()} type="button">
272
+ {#if backIcon}
273
+ {@const BackIcon = backIcon}
274
+ <BackIcon size={18} stroke={2} />
275
+ {:else}
276
+
277
+ {/if}
278
+ </button>
279
+ {#if activeSubmenuFilterOptions && activeSubmenuFilterOptions.length > 0}
280
+ <div class="radialMenu__tableFilter">
281
+ <Select
282
+ options={activeSubmenuFilterOptions.map((opt) => ({ value: opt.value, label: opt.label }))}
283
+ bind:selected={selectedFilter}
284
+ variant="transparent"
285
+ />
286
+ </div>
287
+ {/if}
288
+ </div>
289
+ <div class="radialMenu__tableColumns">
290
+ {#each tableColumns as column, colIndex (colIndex)}
291
+ <div class="radialMenu__tableColumn">
292
+ {#each column as item (item.id)}
293
+ <button
294
+ class="radialMenu__tableItem"
295
+ class:radialMenu__tableItem--disabled={item.disabled}
296
+ onclick={() => handleItemSelect(item.id)}
297
+ type="button"
298
+ disabled={item.disabled}
299
+ >
300
+ {#if item.icon}
301
+ {@const ItemIcon = item.icon}
302
+ <span class="radialMenu__tableItemIcon">
303
+ <ItemIcon size={18} stroke={2} />
304
+ </span>
305
+ {/if}
306
+ <span class="radialMenu__tableItemLabel">{item.label}</span>
307
+ </button>
308
+ {/each}
309
+ </div>
310
+ {/each}
311
+ </div>
312
+ </div>
313
+ {:else}
314
+ {#if activeSubmenu}
315
+ <!-- Back button in center for radial submenu -->
316
+ <button
317
+ class="radialMenu__centerBtn"
318
+ onclick={() => {
319
+ activeSubmenu = null;
320
+ activeSubmenuLayout = 'radial';
321
+ }}
322
+ type="button"
323
+ >
324
+ {#if backIcon}
325
+ {@const BackIcon = backIcon}
326
+ <BackIcon size={20} stroke={2} />
327
+ {:else}
328
+ Back
329
+ {/if}
330
+ </button>
331
+ {/if}
332
+
333
+ <!-- Render menu items in a circle -->
334
+ {#each currentItems as item, index (item.id)}
335
+ <RadialMenuItem
336
+ {item}
337
+ angle={getItemAngle(index, currentItems.length, menuRotation)}
338
+ radius={menuRadius}
339
+ counterRotation={menuRotation}
340
+ onSelect={handleItemSelect}
341
+ />
342
+ {/each}
343
+ {/if}
344
+ </div>
345
+ </div>
346
+ {/if}
347
+
348
+ <style>
349
+ .radialMenu {
350
+ position: fixed;
351
+ inset: 0;
352
+ pointer-events: auto;
353
+ z-index: 1000;
354
+ }
355
+
356
+ .radialMenu__backdrop {
357
+ position: absolute;
358
+ inset: 0;
359
+ background: transparent;
360
+ border: none;
361
+ cursor: pointer;
362
+ padding: 0;
363
+ }
364
+
365
+ .radialMenu__container {
366
+ position: absolute;
367
+ transform: translate(-50%, -50%);
368
+ pointer-events: none;
369
+ }
370
+
371
+ .radialMenu__container--table {
372
+ transform: translate(-50%, -50%);
373
+ }
374
+
375
+ .radialMenu__centerBtn {
376
+ position: absolute;
377
+ top: 0;
378
+ left: 0;
379
+ width: 3rem;
380
+ height: 3rem;
381
+ padding: 0;
382
+ background: var(--bg);
383
+ border: 1px solid var(--fgMuted);
384
+ border-radius: 50%;
385
+ color: var(--fg);
386
+ font-size: 0.875rem;
387
+ font-weight: 600;
388
+ cursor: pointer;
389
+ pointer-events: auto;
390
+ display: flex;
391
+ align-items: center;
392
+ justify-content: center;
393
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
394
+ }
395
+
396
+ .radialMenu__centerBtn:hover {
397
+ background: var(--fgPrimary);
398
+ color: var(--bg);
399
+ }
400
+
401
+ .radialMenu__table {
402
+ display: flex;
403
+ flex-direction: column;
404
+ gap: 0.5rem;
405
+ background: var(--bg);
406
+ border: 1px solid var(--fgMuted);
407
+ border-radius: 0.5rem;
408
+ padding: 0.5rem;
409
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
410
+ pointer-events: auto;
411
+ }
412
+
413
+ .radialMenu__tableHeader {
414
+ display: flex;
415
+ align-items: center;
416
+ gap: 0.5rem;
417
+ border-bottom: 1px solid var(--fgMuted);
418
+ padding-bottom: 0.5rem;
419
+ }
420
+
421
+ .radialMenu__tableBack {
422
+ width: 2rem;
423
+ height: 2rem;
424
+ padding: 0;
425
+ background: transparent;
426
+ border: 1px solid var(--fgMuted);
427
+ border-radius: 50%;
428
+ color: var(--fgMuted);
429
+ font-size: 0.875rem;
430
+ font-weight: 500;
431
+ cursor: pointer;
432
+ display: flex;
433
+ align-items: center;
434
+ justify-content: center;
435
+ flex-shrink: 0;
436
+ }
437
+
438
+ .radialMenu__tableBack:hover {
439
+ color: var(--fg);
440
+ border-color: var(--fg);
441
+ }
442
+
443
+ .radialMenu__tableFilter {
444
+ flex: 1;
445
+ min-width: 0;
446
+ }
447
+
448
+ .radialMenu__tableColumns {
449
+ display: flex;
450
+ gap: 0.25rem;
451
+ max-height: 20rem;
452
+ overflow-y: auto;
453
+ }
454
+
455
+ .radialMenu__tableColumn {
456
+ display: flex;
457
+ flex-direction: column;
458
+ gap: 0.125rem;
459
+ min-width: 8rem;
460
+ }
461
+
462
+ .radialMenu__tableItem {
463
+ padding: 0.5rem 1rem;
464
+ background: transparent;
465
+ border: none;
466
+ border-radius: 0.25rem;
467
+ color: var(--fg);
468
+ font-size: 0.875rem;
469
+ font-weight: 500;
470
+ cursor: pointer;
471
+ text-align: left;
472
+ white-space: nowrap;
473
+ display: flex;
474
+ align-items: center;
475
+ gap: 0.5rem;
476
+ }
477
+
478
+ .radialMenu__tableItem:hover {
479
+ background: var(--fgPrimary);
480
+ color: var(--bg);
481
+ }
482
+
483
+ .radialMenu__tableItem--disabled {
484
+ opacity: 0.5;
485
+ cursor: not-allowed;
486
+ }
487
+
488
+ .radialMenu__tableItem--disabled:hover {
489
+ background: transparent;
490
+ color: var(--fg);
491
+ }
492
+
493
+ .radialMenu__tableItemIcon {
494
+ font-size: 1rem;
495
+ display: flex;
496
+ align-items: center;
497
+ justify-content: center;
498
+ }
499
+
500
+ .radialMenu__tableItemLabel {
501
+ font-family: inherit;
502
+ }
503
+ </style>
@@ -0,0 +1,176 @@
1
+ <script lang="ts">
2
+ import type { RadialMenuItemProps } from './types';
3
+ import EffectPreview from './EffectPreview.svelte';
4
+
5
+ interface Props {
6
+ item: RadialMenuItemProps;
7
+ angle: number;
8
+ radius: number;
9
+ counterRotation: number;
10
+ onSelect: (itemId: string) => void;
11
+ }
12
+
13
+ const { item, angle, radius, counterRotation, onSelect }: Props = $props();
14
+
15
+ // Calculate position using polar to cartesian conversion
16
+ const x = $derived(Math.cos(angle) * radius);
17
+ const y = $derived(Math.sin(angle) * radius);
18
+
19
+ // Determine if this is an icon-only button (has icon but no label)
20
+ const isIconOnly = $derived(item.icon && !item.label && !item.color && item.effectType === undefined);
21
+
22
+ // Determine if this is an effect-only button
23
+ const isEffectOnly = $derived(item.effectType !== undefined && !item.label);
24
+
25
+ function handleClick() {
26
+ if (!item.disabled) {
27
+ onSelect(item.id);
28
+ }
29
+ }
30
+ </script>
31
+
32
+ <button
33
+ class="radialMenu__item"
34
+ class:radialMenu__item--disabled={item.disabled}
35
+ class:radialMenu__item--colorOnly={item.color && !item.label}
36
+ class:radialMenu__item--iconOnly={isIconOnly}
37
+ class:radialMenu__item--effectOnly={isEffectOnly}
38
+ style="transform: translate({x}px, {y}px) rotate({-counterRotation}deg);"
39
+ onclick={handleClick}
40
+ type="button"
41
+ >
42
+ {#if item.effectType !== undefined}
43
+ <span class="radialMenu__itemEffect">
44
+ <EffectPreview effectType={item.effectType} size="2.5rem" />
45
+ </span>
46
+ {:else if item.color}
47
+ <span class="radialMenu__itemSwatch" style="background-color: {item.color};"></span>
48
+ {:else if item.icon}
49
+ <span class="radialMenu__itemIcon">
50
+ <item.icon size={24} stroke={2} />
51
+ </span>
52
+ {/if}
53
+ {#if item.label}
54
+ <span class="radialMenu__itemLabel">{item.label}</span>
55
+ {/if}
56
+ </button>
57
+
58
+ <style>
59
+ .radialMenu__item {
60
+ position: absolute;
61
+ top: 0;
62
+ left: 0;
63
+ transform-origin: center;
64
+ padding: 0.75rem 1rem;
65
+ background: var(--bg);
66
+ border: 1px solid var(--fgMuted);
67
+ border-radius: 0.5rem;
68
+ color: var(--fg);
69
+ font-size: 0.875rem;
70
+ font-weight: 500;
71
+ cursor: pointer;
72
+ pointer-events: auto;
73
+ white-space: nowrap;
74
+ display: flex;
75
+ align-items: center;
76
+ gap: 0.5rem;
77
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
78
+ }
79
+
80
+ .radialMenu__item:hover {
81
+ background: var(--fgPrimary);
82
+ color: var(--bg);
83
+ border-color: var(--fgPrimary);
84
+ }
85
+
86
+ .radialMenu__item--disabled {
87
+ opacity: 0.5;
88
+ cursor: not-allowed;
89
+ pointer-events: none;
90
+ }
91
+
92
+ .radialMenu__itemIcon {
93
+ font-size: 1.125rem;
94
+ display: flex;
95
+ align-items: center;
96
+ justify-content: center;
97
+ }
98
+
99
+ .radialMenu__itemLabel {
100
+ font-family: inherit;
101
+ }
102
+
103
+ .radialMenu__itemSwatch {
104
+ width: 1.5rem;
105
+ height: 1.5rem;
106
+ border-radius: 50%;
107
+ border: 2px solid var(--bg);
108
+ box-shadow: 0 0 0 1px var(--fgMuted);
109
+ display: flex;
110
+ align-items: center;
111
+ justify-content: center;
112
+ }
113
+
114
+ .radialMenu__item--colorOnly {
115
+ width: 3rem;
116
+ height: 3rem;
117
+ padding: 0;
118
+ background: transparent;
119
+ border: none;
120
+ border-radius: 50%;
121
+ box-shadow: none;
122
+ }
123
+
124
+ .radialMenu__item--colorOnly:hover {
125
+ background: transparent;
126
+ border: none;
127
+ }
128
+
129
+ .radialMenu__item--colorOnly .radialMenu__itemSwatch {
130
+ width: 3rem;
131
+ height: 3rem;
132
+ border: 2px solid var(--bg);
133
+ box-shadow:
134
+ 0 0 0 1px var(--fgMuted),
135
+ 0 4px 6px rgba(0, 0, 0, 0.1);
136
+ }
137
+
138
+ .radialMenu__item--iconOnly {
139
+ width: 3rem;
140
+ height: 3rem;
141
+ padding: 0;
142
+ border-radius: 50%;
143
+ display: flex;
144
+ align-items: center;
145
+ justify-content: center;
146
+ }
147
+
148
+ .radialMenu__item--effectOnly {
149
+ width: 3rem;
150
+ height: 3rem;
151
+ padding: 0;
152
+ background: transparent;
153
+ border: none;
154
+ border-radius: 50%;
155
+ box-shadow: none;
156
+ }
157
+
158
+ .radialMenu__item--effectOnly:hover {
159
+ background: transparent;
160
+ border: none;
161
+ }
162
+
163
+ .radialMenu__itemEffect {
164
+ width: 2.5rem;
165
+ height: 2.5rem;
166
+ border-radius: 50%;
167
+ overflow: hidden;
168
+ border: 2px solid var(--bg);
169
+ box-shadow:
170
+ 0 0 0 1px var(--fgMuted),
171
+ 0 4px 6px rgba(0, 0, 0, 0.1);
172
+ display: flex;
173
+ align-items: center;
174
+ justify-content: center;
175
+ }
176
+ </style>
@@ -0,0 +1,2 @@
1
+ export { default as RadialMenu } from './RadialMenu.svelte';
2
+ export * from './types';
@@ -0,0 +1,35 @@
1
+ export type SubmenuLayout = 'radial' | 'table';
2
+
3
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
4
+ export type IconComponent = any;
5
+
6
+ export interface TableFilterOption {
7
+ value: string;
8
+ label: string;
9
+ }
10
+
11
+ export interface RadialMenuItemProps {
12
+ id: string;
13
+ label: string;
14
+ icon?: IconComponent;
15
+ color?: string; // Hex color for rendering a color swatch
16
+ effectType?: number; // AnnotationEffect enum value for rendering effect preview
17
+ submenu?: RadialMenuItemProps[];
18
+ submenuLayout?: SubmenuLayout; // 'radial' (default) or 'table' for column-based layout
19
+ submenuFilterOptions?: TableFilterOption[]; // Filter options for table layout (e.g., game sessions)
20
+ submenuFilterDefault?: string; // Default selected filter value
21
+ submenuFilterKey?: string; // Key on submenu items to match against filter value
22
+ disabled?: boolean;
23
+ // Allow additional properties for filtering (e.g., gameSessionId)
24
+ [key: string]: unknown;
25
+ }
26
+
27
+ export interface RadialMenuProps {
28
+ visible: boolean;
29
+ position: { x: number; y: number };
30
+ items: RadialMenuItemProps[];
31
+ backIcon?: IconComponent;
32
+ onItemSelect?: (itemId: string) => void;
33
+ onClose?: () => void;
34
+ onReposition?: (position: { x: number; y: number }) => void;
35
+ }