@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.
- package/package.json +2 -13
- package/src/lib/components/Avatar/Avatar.svelte +82 -0
- package/src/lib/components/Avatar/AvatarFileInput.svelte +85 -0
- package/src/lib/components/Avatar/AvatarPopover.svelte +34 -0
- package/src/lib/components/Avatar/index.ts +4 -0
- package/src/lib/components/Avatar/types.ts +24 -0
- package/src/lib/components/BrushSizeSlider/BrushSizeSlider.svelte +174 -0
- package/src/lib/components/BrushSizeSlider/index.ts +1 -0
- package/src/lib/components/Button/Button.svelte +182 -0
- package/src/lib/components/Button/ConfirmActionButton.svelte +98 -0
- package/src/lib/components/Button/IconButton.svelte +121 -0
- package/src/lib/components/Button/RadioButton.svelte +93 -0
- package/src/lib/components/Button/index.ts +5 -0
- package/src/lib/components/Button/types.ts +54 -0
- package/src/lib/components/CardFan/CardFan.svelte +165 -0
- package/src/lib/components/CardFan/index.ts +2 -0
- package/src/lib/components/CardFan/types.ts +6 -0
- package/src/lib/components/CodeBlock/Code.svelte +7 -0
- package/src/lib/components/CodeBlock/CodeBlock.svelte +102 -0
- package/src/lib/components/CodeBlock/index.ts +3 -0
- package/src/lib/components/CodeBlock/types.ts +10 -0
- package/src/lib/components/ColorMode/ColorMode.svelte +8 -0
- package/src/lib/components/ColorMode/index.ts +2 -0
- package/src/lib/components/ColorMode/types.ts +12 -0
- package/src/lib/components/ColorPicker/ColorPicker.svelte +838 -0
- package/src/lib/components/ColorPicker/ColorPickerSwatch.svelte +32 -0
- package/src/lib/components/ColorPicker/index.ts +3 -0
- package/src/lib/components/ColorPicker/types.ts +51 -0
- package/src/lib/components/ContextMenu/ContextMenu.svelte +86 -0
- package/src/lib/components/ContextMenu/index.ts +2 -0
- package/src/lib/components/ContextMenu/types.ts +15 -0
- package/src/lib/components/DrawingSliders/DrawingSliders.svelte +379 -0
- package/src/lib/components/DrawingSliders/index.ts +1 -0
- package/src/lib/components/Editor/Editor.svelte +825 -0
- package/src/lib/components/Editor/index.ts +1 -0
- package/src/lib/components/FogSliders/FogSliders.svelte +33 -0
- package/src/lib/components/FogSliders/index.ts +1 -0
- package/src/lib/components/Hr/Hr.svelte +15 -0
- package/src/lib/components/Hr/index.ts +1 -0
- package/src/lib/components/Icon/Icon.svelte +6 -0
- package/src/lib/components/Icon/index.ts +2 -0
- package/src/lib/components/Icon/types.ts +20 -0
- package/src/lib/components/Input/DualInputSlider.svelte +126 -0
- package/src/lib/components/Input/FileInput.svelte +176 -0
- package/src/lib/components/Input/FormControl.svelte +150 -0
- package/src/lib/components/Input/FormError.svelte +37 -0
- package/src/lib/components/Input/Input.svelte +56 -0
- package/src/lib/components/Input/InputCheckbox.svelte +99 -0
- package/src/lib/components/Input/InputSlider.svelte +86 -0
- package/src/lib/components/Input/Label.svelte +19 -0
- package/src/lib/components/Input/index.ts +9 -0
- package/src/lib/components/Input/types.ts +39 -0
- package/src/lib/components/Link/Link.svelte +41 -0
- package/src/lib/components/Link/LinkBox.svelte +20 -0
- package/src/lib/components/Link/LinkOverlay.svelte +23 -0
- package/src/lib/components/Link/index.ts +4 -0
- package/src/lib/components/Link/types.ts +17 -0
- package/src/lib/components/Loading/Loader.svelte +60 -0
- package/src/lib/components/Loading/Skeleton.svelte +9 -0
- package/src/lib/components/Loading/index.ts +2 -0
- package/src/lib/components/Logo/Logo.svelte +16 -0
- package/src/lib/components/Logo/index.ts +1 -0
- package/src/lib/components/MarkerTooltip/MarkerTooltip.svelte +435 -0
- package/src/lib/components/MarkerTooltip/index.ts +1 -0
- package/src/lib/components/Menu/SelectorMenu.svelte +280 -0
- package/src/lib/components/Menu/index.ts +2 -0
- package/src/lib/components/Menu/types.ts +17 -0
- package/src/lib/components/MyCounterButton.svelte +11 -0
- package/src/lib/components/Panel/index.ts +2 -0
- package/src/lib/components/Panel/panel.svelte +18 -0
- package/src/lib/components/Panel/types.ts +8 -0
- package/src/lib/components/PersistButton/PersistButton.svelte +100 -0
- package/src/lib/components/PersistButton/index.ts +1 -0
- package/src/lib/components/Popover/Popover.svelte +81 -0
- package/src/lib/components/Popover/index.ts +2 -0
- package/src/lib/components/Popover/types.ts +19 -0
- package/src/lib/components/PropsTable/PropsTable.svelte +107 -0
- package/src/lib/components/RadialMenu/EffectPreview.svelte +36 -0
- package/src/lib/components/RadialMenu/EffectPreviewScene.svelte +194 -0
- package/src/lib/components/RadialMenu/RadialMenu.svelte +503 -0
- package/src/lib/components/RadialMenu/RadialMenuItem.svelte +176 -0
- package/src/lib/components/RadialMenu/index.ts +2 -0
- package/src/lib/components/RadialMenu/types.ts +35 -0
- package/src/lib/components/Select/Select.svelte +342 -0
- package/src/lib/components/Select/index.ts +2 -0
- package/src/lib/components/Select/types.ts +22 -0
- package/src/lib/components/Spacer/Spacer.svelte +14 -0
- package/src/lib/components/Spacer/index.ts +2 -0
- package/src/lib/components/Spacer/types.ts +5 -0
- package/src/lib/components/Stage/components/AnnotationLayer/AnnotationLayer.svelte +445 -0
- package/src/lib/components/Stage/components/AnnotationLayer/AnnotationMaterial.svelte +167 -0
- package/src/lib/components/Stage/components/AnnotationLayer/types.ts +196 -0
- package/src/lib/components/Stage/components/CursorLayer/CursorLayer.svelte +148 -0
- package/src/lib/components/Stage/components/CursorLayer/cursor.svg +26 -0
- package/src/lib/components/Stage/components/CursorLayer/index.ts +2 -0
- package/src/lib/components/Stage/components/CursorLayer/types.ts +23 -0
- package/src/lib/components/Stage/components/DrawingLayer/DrawingMaterial.svelte +364 -0
- package/src/lib/components/Stage/components/DrawingLayer/types.ts +65 -0
- package/src/lib/components/Stage/components/EdgeOverlayLayer/EdgeOverlayLayer.svelte +72 -0
- package/src/lib/components/Stage/components/EdgeOverlayLayer/types.ts +34 -0
- package/src/lib/components/Stage/components/FogLayer/FogLayer.svelte +75 -0
- package/src/lib/components/Stage/components/FogLayer/types.ts +51 -0
- package/src/lib/components/Stage/components/FogOfWarLayer/FogOfWarLayer.svelte +249 -0
- package/src/lib/components/Stage/components/FogOfWarLayer/FogOfWarMaterial.svelte +200 -0
- package/src/lib/components/Stage/components/FogOfWarLayer/types.ts +116 -0
- package/src/lib/components/Stage/components/GridLayer/GridLayer.svelte +20 -0
- package/src/lib/components/Stage/components/GridLayer/GridMaterial.svelte +69 -0
- package/src/lib/components/Stage/components/GridLayer/types.ts +79 -0
- package/src/lib/components/Stage/components/LayerInput/LayerInput.svelte +300 -0
- package/src/lib/components/Stage/components/MapLayer/MapLayer.svelte +196 -0
- package/src/lib/components/Stage/components/MapLayer/dataSources/GifDataSource.ts +265 -0
- package/src/lib/components/Stage/components/MapLayer/dataSources/IMapDataSource.ts +55 -0
- package/src/lib/components/Stage/components/MapLayer/dataSources/ImageDataSource.ts +87 -0
- package/src/lib/components/Stage/components/MapLayer/dataSources/VideoDataSource.ts +150 -0
- package/src/lib/components/Stage/components/MapLayer/dataSources/dataSourceFactory.ts +48 -0
- package/src/lib/components/Stage/components/MapLayer/dataSources/index.ts +16 -0
- package/src/lib/components/Stage/components/MapLayer/types.ts +58 -0
- package/src/lib/components/Stage/components/MarkerLayer/MarkerLayer.svelte +398 -0
- package/src/lib/components/Stage/components/MarkerLayer/MarkerToken.svelte +262 -0
- package/src/lib/components/Stage/components/MarkerLayer/types.ts +126 -0
- package/src/lib/components/Stage/components/MeasurementLayer/MeasurementLayer.svelte +364 -0
- package/src/lib/components/Stage/components/MeasurementLayer/MeasurementManager.svelte +473 -0
- package/src/lib/components/Stage/components/MeasurementLayer/measurements/BaseMeasurement.ts +427 -0
- package/src/lib/components/Stage/components/MeasurementLayer/measurements/BeamMeasurement.ts +105 -0
- package/src/lib/components/Stage/components/MeasurementLayer/measurements/CircleMeasurement.ts +98 -0
- package/src/lib/components/Stage/components/MeasurementLayer/measurements/ConeMeasurement.ts +163 -0
- package/src/lib/components/Stage/components/MeasurementLayer/measurements/LineMeasurement.ts +102 -0
- package/src/lib/components/Stage/components/MeasurementLayer/measurements/RectangleMeasurement.ts +120 -0
- package/src/lib/components/Stage/components/MeasurementLayer/measurements/index.ts +7 -0
- package/src/lib/components/Stage/components/MeasurementLayer/types.ts +94 -0
- package/src/lib/components/Stage/components/MeasurementLayer/utils/canvasDrawing.ts +357 -0
- package/src/lib/components/Stage/components/MeasurementLayer/utils/distanceCalculations.ts +170 -0
- package/src/lib/components/Stage/components/ParticleSystem/ParticleSystem.svelte +220 -0
- package/src/lib/components/Stage/components/ParticleSystem/particles/atlases/ash.png +0 -0
- package/src/lib/components/Stage/components/ParticleSystem/particles/atlases/leaves.png +0 -0
- package/src/lib/components/Stage/components/ParticleSystem/particles/atlases/rain.png +0 -0
- package/src/lib/components/Stage/components/ParticleSystem/particles/atlases/snow.png +0 -0
- package/src/lib/components/Stage/components/ParticleSystem/rng.js +20 -0
- package/src/lib/components/Stage/components/ParticleSystem/types.ts +95 -0
- package/src/lib/components/Stage/components/PerformanceDebugger/PerformanceDebugger.svelte +144 -0
- package/src/lib/components/Stage/components/PerformanceDebugger/index.ts +1 -0
- package/src/lib/components/Stage/components/PerformanceOverlay/PerformanceOverlay.svelte +208 -0
- package/src/lib/components/Stage/components/PerformanceOverlay/index.ts +1 -0
- package/src/lib/components/Stage/components/PointerInputManager/PointerInputManager.svelte +201 -0
- package/src/lib/components/Stage/components/Scene/Scene.svelte +651 -0
- package/src/lib/components/Stage/components/Scene/luts.ts +24 -0
- package/src/lib/components/Stage/components/Scene/types.ts +225 -0
- package/src/lib/components/Stage/components/Stage/Stage.svelte +332 -0
- package/src/lib/components/Stage/components/Stage/types.ts +136 -0
- package/src/lib/components/Stage/components/WeatherLayer/WeatherLayer.svelte +135 -0
- package/src/lib/components/Stage/components/WeatherLayer/presets/AshPreset.ts +71 -0
- package/src/lib/components/Stage/components/WeatherLayer/presets/LeavesPreset.ts +70 -0
- package/src/lib/components/Stage/components/WeatherLayer/presets/RainPreset.ts +68 -0
- package/src/lib/components/Stage/components/WeatherLayer/presets/SnowPreset.ts +70 -0
- package/src/lib/components/Stage/components/WeatherLayer/presets/index.ts +6 -0
- package/src/lib/components/Stage/components/WeatherLayer/types.ts +35 -0
- package/src/lib/components/Stage/helpers/clippingPlaneStore.svelte.ts +28 -0
- package/src/lib/components/Stage/helpers/debugState.svelte.ts +18 -0
- package/src/lib/components/Stage/helpers/grid.ts +548 -0
- package/src/lib/components/Stage/helpers/lazyBrush.ts +171 -0
- package/src/lib/components/Stage/helpers/performanceMetrics.svelte.ts +220 -0
- package/src/lib/components/Stage/helpers/utils.ts +21 -0
- package/src/lib/components/Stage/index.ts +49 -0
- package/src/lib/components/Stage/shaders/AnnotationEffects.frag +1070 -0
- package/src/lib/components/Stage/shaders/Annotations.frag +29 -0
- package/src/lib/components/Stage/shaders/Drawing.frag +83 -0
- package/src/lib/components/Stage/shaders/Drawing.vert +5 -0
- package/src/lib/components/Stage/shaders/Fog.frag +147 -0
- package/src/lib/components/Stage/shaders/FractalNoise.frag +96 -0
- package/src/lib/components/Stage/shaders/GridShader.frag +174 -0
- package/src/lib/components/Stage/shaders/Overlay.frag +23 -0
- package/src/lib/components/Stage/shaders/Overlay.vert +0 -0
- package/src/lib/components/Stage/shaders/Particles.frag +27 -0
- package/src/lib/components/Stage/shaders/Particles.vert +51 -0
- package/src/lib/components/Stage/shaders/ToolOutline.frag +59 -0
- package/src/lib/components/Stage/shaders/default.vert +8 -0
- package/src/lib/components/Stage/types.ts +4 -0
- package/src/lib/components/Table/Table.svelte +16 -0
- package/src/lib/components/Table/Td.svelte +17 -0
- package/src/lib/components/Table/Th.svelte +18 -0
- package/src/lib/components/Table/index.ts +4 -0
- package/src/lib/components/Table/types.ts +14 -0
- package/src/lib/components/Text/Text.svelte +23 -0
- package/src/lib/components/Text/index.ts +2 -0
- package/src/lib/components/Text/types.ts +12 -0
- package/src/lib/components/Title/Title.svelte +54 -0
- package/src/lib/components/Title/index.ts +2 -0
- package/src/lib/components/Title/types.ts +9 -0
- package/src/lib/components/Toast/Toast.svelte +155 -0
- package/src/lib/components/Toast/index.ts +5 -0
- package/src/lib/components/Toast/toastCookie.ts +24 -0
- package/src/lib/components/Toast/types.ts +6 -0
- package/src/lib/components/ToolTip/ToolTip.svelte +70 -0
- package/src/lib/components/ToolTip/index.ts +2 -0
- package/src/lib/components/ToolTip/types.ts +14 -0
- package/src/lib/components/index.ts +32 -0
- package/src/lib/components/types.ts +0 -0
- package/src/lib/index.ts +2 -0
- package/src/lib/styles/globals.css +108 -0
- package/src/lib/styles/normalize.css +9 -0
- package/src/lib/styles/reset.css +133 -0
- package/src/lib/styles/utilities.css +179 -0
- package/src/lib/styles/vars.css +1103 -0
- package/src/lib/types/awareness.ts +17 -0
- 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,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
|
+
}
|