@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,435 @@
1
+ <script lang="ts">
2
+ import { MarkerVisibility } from '../Stage/components/MarkerLayer/types';
3
+ import { computePosition, flip, shift, offset, autoUpdate } from '@floating-ui/dom';
4
+ import { Editor } from '../Editor';
5
+ import { onMount, onDestroy } from 'svelte';
6
+ import { IconPin, IconPinFilled } from '@tabler/icons-svelte';
7
+
8
+ interface MarkerData {
9
+ id: string;
10
+ title?: string;
11
+ note?: unknown;
12
+ visibility?: MarkerVisibility;
13
+ size?: number;
14
+ tooltip?: {
15
+ title?: string;
16
+ content?: unknown;
17
+ };
18
+ }
19
+
20
+ interface Props {
21
+ marker: MarkerData | null;
22
+ position: { x: number; y: number } | null;
23
+ containerElement: HTMLElement | null;
24
+ markerDiameter?: number;
25
+ onTooltipHover?: (isHovering: boolean) => void;
26
+ isDM?: boolean;
27
+ isPinned?: boolean;
28
+ onPinToggle?: (markerId: string, pinned: boolean) => void;
29
+ existingTooltips?: Array<{ element: HTMLElement; bounds: DOMRect }>;
30
+ preferredPlacement?: 'top' | 'bottom' | 'left' | 'right';
31
+ onTooltipMount?: (element: HTMLElement, bounds: DOMRect) => void;
32
+ onTooltipUnmount?: (element: HTMLElement) => void;
33
+ }
34
+
35
+ let {
36
+ marker,
37
+ position,
38
+ containerElement,
39
+ markerDiameter = 40,
40
+ onTooltipHover,
41
+ isDM = false,
42
+ isPinned = false,
43
+ onPinToggle,
44
+ existingTooltips = [],
45
+ preferredPlacement = 'top',
46
+ onTooltipMount,
47
+ onTooltipUnmount
48
+ }: Props = $props();
49
+
50
+ let tooltipElement = $state<HTMLDivElement>();
51
+ let portalContainer: HTMLDivElement | undefined = $state();
52
+ let cleanup: (() => void) | null = null;
53
+ let currentPlacement = $state<'top' | 'bottom' | 'left' | 'right'>('top');
54
+
55
+ function handleTooltipMouseEnter() {
56
+ if (onTooltipHover) {
57
+ onTooltipHover(true);
58
+ }
59
+ }
60
+
61
+ function handleTooltipMouseLeave() {
62
+ if (onTooltipHover) {
63
+ onTooltipHover(false);
64
+ }
65
+ }
66
+
67
+ const getMarkerContent = (marker: MarkerData | null) => {
68
+ if (!marker) return null;
69
+
70
+ if (marker.note) {
71
+ return marker.note;
72
+ }
73
+
74
+ if (marker.tooltip?.content) {
75
+ if (typeof marker.tooltip.content === 'string') {
76
+ try {
77
+ return JSON.parse(marker.tooltip.content);
78
+ } catch {
79
+ return marker.tooltip.content;
80
+ }
81
+ }
82
+ return marker.tooltip.content;
83
+ }
84
+
85
+ return null;
86
+ };
87
+
88
+ const getMarkerTitle = (marker: MarkerData | null) => {
89
+ if (!marker) return null;
90
+
91
+ if (marker.title) {
92
+ return marker.title;
93
+ }
94
+
95
+ if (marker.tooltip?.title) {
96
+ return marker.tooltip.title;
97
+ }
98
+
99
+ return null;
100
+ };
101
+
102
+ let markerContent = $derived(getMarkerContent(marker));
103
+ let markerTitle = $derived(getMarkerTitle(marker));
104
+
105
+ function getEstimatedBounds(
106
+ virtualEl: {
107
+ getBoundingClientRect: () => {
108
+ left: number;
109
+ top: number;
110
+ right: number;
111
+ bottom: number;
112
+ width: number;
113
+ height: number;
114
+ x: number;
115
+ y: number;
116
+ };
117
+ },
118
+ element: HTMLElement,
119
+ placement: string,
120
+ offset: number
121
+ ) {
122
+ const rect = virtualEl.getBoundingClientRect();
123
+ const width = element.offsetWidth || 200;
124
+ const height = element.offsetHeight || 100;
125
+
126
+ let x = rect.left;
127
+ let y = rect.top;
128
+
129
+ switch (placement) {
130
+ case 'top':
131
+ x -= width / 2;
132
+ y -= height + offset;
133
+ break;
134
+ case 'bottom':
135
+ x -= width / 2;
136
+ y += offset;
137
+ break;
138
+ case 'left':
139
+ x -= width + offset;
140
+ y -= height / 2;
141
+ break;
142
+ case 'right':
143
+ x += offset;
144
+ y -= height / 2;
145
+ break;
146
+ }
147
+
148
+ return { x, y, width, height, left: x, top: y, right: x + width, bottom: y + height };
149
+ }
150
+
151
+ function calculateOverlap(
152
+ rect1: DOMRect | { left: number; right: number; top: number; bottom: number },
153
+ rect2: DOMRect | { left: number; right: number; top: number; bottom: number }
154
+ ) {
155
+ const xOverlap = Math.max(0, Math.min(rect1.right, rect2.right) - Math.max(rect1.left, rect2.left));
156
+ const yOverlap = Math.max(0, Math.min(rect1.bottom, rect2.bottom) - Math.max(rect1.top, rect2.top));
157
+ return xOverlap * yOverlap;
158
+ }
159
+
160
+ function calculateTotalOverlap(
161
+ testBounds: DOMRect | { left: number; right: number; top: number; bottom: number },
162
+ existingTooltips: Array<{ bounds: DOMRect }>
163
+ ) {
164
+ return existingTooltips.reduce((total, tooltip) => {
165
+ return total + calculateOverlap(testBounds, tooltip.bounds);
166
+ }, 0);
167
+ }
168
+
169
+ function createPortalContainer() {
170
+ const existingContainer = document.getElementById('markerTooltipPortal');
171
+ if (existingContainer) {
172
+ portalContainer = existingContainer as HTMLDivElement;
173
+ return;
174
+ }
175
+
176
+ const container = document.createElement('div');
177
+ container.id = 'markerTooltipPortal';
178
+ container.style.position = 'absolute';
179
+ container.style.top = '0';
180
+ container.style.left = '0';
181
+ container.style.width = '0';
182
+ container.style.height = '0';
183
+ container.style.pointerEvents = 'none';
184
+ container.style.zIndex = '1';
185
+ document.body.appendChild(container);
186
+ portalContainer = container;
187
+ }
188
+
189
+ onMount(() => {
190
+ createPortalContainer();
191
+ });
192
+
193
+ onDestroy(() => {
194
+ if (cleanup) {
195
+ cleanup();
196
+ cleanup = null;
197
+ }
198
+
199
+ if (onTooltipUnmount && tooltipElement) {
200
+ onTooltipUnmount(tooltipElement);
201
+ }
202
+
203
+ if (tooltipElement && portalContainer && portalContainer.contains(tooltipElement)) {
204
+ portalContainer.removeChild(tooltipElement);
205
+ }
206
+
207
+ if (portalContainer && portalContainer.childNodes.length === 0) {
208
+ if (document.body.contains(portalContainer)) {
209
+ document.body.removeChild(portalContainer);
210
+ }
211
+ }
212
+ });
213
+
214
+ $effect(() => {
215
+ if (tooltipElement && position && containerElement && portalContainer) {
216
+ if (!portalContainer.contains(tooltipElement)) {
217
+ portalContainer.appendChild(tooltipElement);
218
+ }
219
+
220
+ if (cleanup) {
221
+ cleanup();
222
+ cleanup = null;
223
+ }
224
+
225
+ const virtualEl = {
226
+ getBoundingClientRect() {
227
+ const rect = containerElement.getBoundingClientRect();
228
+ const viewportX = rect.left + position.x;
229
+ const viewportY = rect.top + position.y;
230
+
231
+ return {
232
+ width: 0,
233
+ height: 0,
234
+ x: viewportX,
235
+ y: viewportY,
236
+ top: viewportY,
237
+ left: viewportX,
238
+ right: viewportX,
239
+ bottom: viewportY
240
+ };
241
+ }
242
+ };
243
+
244
+ cleanup = autoUpdate(virtualEl, tooltipElement!, async () => {
245
+ const markerRadius = markerDiameter / 2;
246
+ const arrowSize = 8;
247
+ const dynamicOffset = markerRadius + arrowSize;
248
+
249
+ let bestPlacement = preferredPlacement;
250
+
251
+ if (existingTooltips.length > 0 && tooltipElement && tooltipElement.offsetWidth > 0) {
252
+ const placements: ('top' | 'bottom' | 'left' | 'right')[] = ['top', 'bottom', 'left', 'right'];
253
+ let minOverlap = Infinity;
254
+
255
+ for (const placement of placements) {
256
+ const testBounds = getEstimatedBounds(virtualEl, tooltipElement!, placement, dynamicOffset);
257
+ const overlap = calculateTotalOverlap(testBounds, existingTooltips);
258
+ if (overlap < minOverlap) {
259
+ minOverlap = overlap;
260
+ bestPlacement = placement;
261
+ }
262
+ }
263
+ }
264
+
265
+ const fallbackOptions: ('top' | 'bottom' | 'left' | 'right')[] = ['bottom', 'left', 'right', 'top'];
266
+ const middleware = [
267
+ offset(dynamicOffset),
268
+ flip({
269
+ fallbackPlacements: fallbackOptions.filter((p) => p !== bestPlacement)
270
+ }),
271
+ shift({ padding: 10 })
272
+ ];
273
+
274
+ const { x, y, placement } = await computePosition(virtualEl, tooltipElement!, {
275
+ placement: bestPlacement,
276
+ middleware,
277
+ strategy: 'fixed'
278
+ });
279
+
280
+ currentPlacement = placement.split('-')[0] as 'top' | 'bottom' | 'left' | 'right';
281
+
282
+ Object.assign(tooltipElement!.style, {
283
+ position: 'fixed',
284
+ left: `${x}px`,
285
+ top: `${y}px`,
286
+ pointerEvents: 'auto'
287
+ });
288
+
289
+ if (onTooltipMount && tooltipElement) {
290
+ setTimeout(() => {
291
+ if (tooltipElement) {
292
+ const bounds = tooltipElement.getBoundingClientRect();
293
+ onTooltipMount(tooltipElement, bounds);
294
+ }
295
+ }, 100);
296
+ }
297
+ });
298
+
299
+ tooltipElement.style.display = 'block';
300
+ } else if (tooltipElement) {
301
+ tooltipElement.style.display = 'none';
302
+ }
303
+
304
+ return () => {
305
+ if (cleanup) {
306
+ cleanup();
307
+ cleanup = null;
308
+ }
309
+ };
310
+ });
311
+ </script>
312
+
313
+ {#if (markerContent || markerTitle) && position}
314
+ <div
315
+ bind:this={tooltipElement}
316
+ class="markerTooltip"
317
+ style="display: none;"
318
+ role="tooltip"
319
+ data-placement={currentPlacement}
320
+ onmouseenter={handleTooltipMouseEnter}
321
+ onmouseleave={handleTooltipMouseLeave}
322
+ >
323
+ <div class="markerTooltip__arrow markerTooltip__arrow--{currentPlacement}"></div>
324
+ {#if isDM && onPinToggle && marker && marker.visibility !== MarkerVisibility.DM}
325
+ <button
326
+ class="markerTooltip__pin"
327
+ onclick={() => onPinToggle(marker.id, !isPinned)}
328
+ title={isPinned ? 'Unpin from player view' : 'Pin to player view'}
329
+ >
330
+ {#if isPinned}
331
+ <IconPinFilled size={16} />
332
+ {:else}
333
+ <IconPin size={16} />
334
+ {/if}
335
+ </button>
336
+ {/if}
337
+ {#if markerTitle}
338
+ <div class="markerTooltip__title {isDM ? 'markerTooltip__title--dm' : ''}">{markerTitle}</div>
339
+ {/if}
340
+ {#if markerContent}
341
+ <Editor content={markerContent} editable={false} />
342
+ {/if}
343
+ </div>
344
+ {/if}
345
+
346
+ <style>
347
+ .markerTooltip {
348
+ max-width: 400px;
349
+ background-color: var(--bg);
350
+ padding: 0.5rem 1rem;
351
+ border: 1px solid var(--border);
352
+ border-radius: 0.25rem;
353
+ box-shadow: var(--shadow-3);
354
+ position: relative;
355
+ }
356
+
357
+ .markerTooltip__pin {
358
+ position: absolute;
359
+ top: 0.25rem;
360
+ right: 0.25rem;
361
+ background: transparent;
362
+ border: none;
363
+ color: var(--fgMuted);
364
+ cursor: pointer;
365
+ padding: 0.25rem;
366
+ display: flex;
367
+ align-items: center;
368
+ justify-content: center;
369
+ border-radius: var(--radius);
370
+ transition:
371
+ color 0.2s,
372
+ background 0.2s;
373
+ }
374
+
375
+ .markerTooltip__pin:hover {
376
+ color: var(--fgPrimary);
377
+ background: var(--bgHover);
378
+ }
379
+
380
+ .markerTooltip__title {
381
+ font-weight: 600;
382
+ font-size: 1rem;
383
+ margin-bottom: 0.5rem;
384
+ color: var(--fg);
385
+ }
386
+ .markerTooltip__title--dm {
387
+ padding-right: 2rem;
388
+ }
389
+
390
+ .markerTooltip__title:last-child {
391
+ margin-bottom: 0;
392
+ }
393
+
394
+ /* Arrow indicator */
395
+ .markerTooltip__arrow {
396
+ position: absolute;
397
+ width: 0;
398
+ height: 0;
399
+ border-style: solid;
400
+ pointer-events: none;
401
+ }
402
+
403
+ /* Arrow modifiers for different placements */
404
+ .markerTooltip__arrow--top {
405
+ bottom: -8px;
406
+ left: 50%;
407
+ transform: translateX(-50%);
408
+ border-width: 8px 8px 0 8px;
409
+ border-color: var(--bg) transparent transparent transparent;
410
+ }
411
+
412
+ .markerTooltip__arrow--bottom {
413
+ top: -8px;
414
+ left: 50%;
415
+ transform: translateX(-50%);
416
+ border-width: 0 8px 8px 8px;
417
+ border-color: transparent transparent var(--bg) transparent;
418
+ }
419
+
420
+ .markerTooltip__arrow--left {
421
+ right: -8px;
422
+ top: 50%;
423
+ transform: translateY(-50%);
424
+ border-width: 8px 0 8px 8px;
425
+ border-color: transparent transparent transparent var(--bg);
426
+ }
427
+
428
+ .markerTooltip__arrow--right {
429
+ left: -8px;
430
+ top: 50%;
431
+ transform: translateY(-50%);
432
+ border-width: 8px 8px 8px 0;
433
+ border-color: transparent var(--bg) transparent transparent;
434
+ }
435
+ </style>
@@ -0,0 +1 @@
1
+ export { default as MarkerTooltip } from './MarkerTooltip.svelte';
@@ -0,0 +1,280 @@
1
+ <script lang="ts">
2
+ import { computePosition, offset, flip, shift, platform } from '@floating-ui/dom';
3
+ import { tick, type Snippet } from 'svelte';
4
+ import { type SelectorMenuProps } from './types';
5
+ import { IconButton } from '../Button';
6
+ import { Icon } from '../Icon';
7
+ import { IconSelector } from '@tabler/icons-svelte';
8
+ import { type SelectOption } from '../Select';
9
+ let {
10
+ trigger,
11
+ options,
12
+ selected = $bindable(),
13
+ disabled = false,
14
+ variant = 'default',
15
+ positioning = { placement: 'bottom-start', offset: 8 },
16
+ onSelectedChange,
17
+ footer,
18
+ ...restProps
19
+ }: SelectorMenuProps = $props();
20
+
21
+ let button: HTMLElement | null = null;
22
+ let menu = $state<HTMLElement | null>(null);
23
+ let isOpen = $state(false);
24
+ let activeIndex = $state<number | null>(null);
25
+ let floatingStyles = $state('');
26
+
27
+ function updatePosition() {
28
+ if (!button || !menu) {
29
+ return;
30
+ }
31
+
32
+ computePosition(button, menu, {
33
+ placement: positioning.placement,
34
+ middleware: [offset(positioning.offset), flip(), shift()],
35
+ platform
36
+ })
37
+ .then(({ x, y, strategy }) => {
38
+ floatingStyles = `left: ${x}px; top: ${y}px; position: ${strategy};`;
39
+ })
40
+ .catch((error) => {
41
+ console.error('Error in computePosition:', error);
42
+ });
43
+ }
44
+
45
+ async function toggleMenu() {
46
+ if (disabled) return;
47
+
48
+ isOpen = !isOpen;
49
+ if (isOpen) {
50
+ if (options.length > 0) {
51
+ activeIndex = 0;
52
+ }
53
+
54
+ await tick();
55
+ updatePosition();
56
+ }
57
+ }
58
+
59
+ const handleGlobalClick = (e: MouseEvent) => {
60
+ if (isOpen && menu && button) {
61
+ const target = e.target as Node;
62
+ if (!menu.contains(target) && !button.contains(target)) {
63
+ closeMenu();
64
+ }
65
+ }
66
+ };
67
+
68
+ function closeMenu() {
69
+ isOpen = false;
70
+ activeIndex = null;
71
+ }
72
+
73
+ function handleSelection(value: string) {
74
+ selected = value;
75
+ closeMenu();
76
+ if (onSelectedChange) {
77
+ onSelectedChange(selected);
78
+ }
79
+ }
80
+
81
+ function handleKeydown(event: KeyboardEvent) {
82
+ if (!isOpen) return;
83
+ const currentIndex = activeIndex ?? -1;
84
+
85
+ switch (event.key) {
86
+ case 'ArrowDown':
87
+ event.preventDefault();
88
+ activeIndex = currentIndex < options.length - 1 ? currentIndex + 1 : 0;
89
+ break;
90
+ case 'ArrowUp':
91
+ event.preventDefault();
92
+ activeIndex = currentIndex > 0 ? currentIndex - 1 : options.length - 1;
93
+ break;
94
+ case 'Enter':
95
+ case ' ':
96
+ event.preventDefault();
97
+ if (activeIndex !== null) {
98
+ handleSelection(options[activeIndex].value);
99
+ }
100
+ break;
101
+ case 'Escape':
102
+ closeMenu();
103
+ break;
104
+ }
105
+ }
106
+
107
+ function isSelected(value: string) {
108
+ return selected === value;
109
+ }
110
+
111
+ function isSnippet(value: string | Snippet): value is Snippet {
112
+ return typeof value === 'function';
113
+ }
114
+
115
+ function getOptionIndex(option: SelectOption): number {
116
+ return options.findIndex((o) => o.value === option.value);
117
+ }
118
+
119
+ const footerProps = {
120
+ close: closeMenu
121
+ };
122
+ </script>
123
+
124
+ <svelte:window onclick={handleGlobalClick} />
125
+
126
+ <div class={['selectorMenu', `selectorMenu--${variant}`, restProps.class]}>
127
+ <button
128
+ bind:this={button}
129
+ onclick={toggleMenu}
130
+ onkeydown={handleKeydown}
131
+ data-testid="menuButton"
132
+ aria-haspopup="listbox"
133
+ aria-expanded={isOpen}
134
+ aria-disabled={disabled}
135
+ {...restProps}
136
+ >
137
+ {#if trigger}
138
+ {@render trigger()}
139
+ {:else}
140
+ <IconButton as="div" variant="ghost">
141
+ <Icon Icon={IconSelector} />
142
+ </IconButton>
143
+ {/if}
144
+ </button>
145
+ {#if isOpen}
146
+ <div bind:this={menu} class="selectorMenu__popover" style={floatingStyles}>
147
+ <ul role="listbox" class="selectorMenu__optionList">
148
+ {#each options as option (option.value)}
149
+ <li
150
+ class={[
151
+ 'selectorMenu__option',
152
+ isSelected(option.value) && 'selectorMenu__option--isSelected',
153
+ getOptionIndex(option) === activeIndex && ' selectorMenu__option--isActive'
154
+ ]}
155
+ tabindex="0"
156
+ onclick={() => handleSelection(option.value)}
157
+ onkeydown={handleKeydown}
158
+ role="option"
159
+ data-testid="menuItem"
160
+ aria-selected={isSelected(option.value)}
161
+ >
162
+ <div class="selectorMenu__start">
163
+ <div class="selectorMenu__space">
164
+ {#if isSelected(option.value)}
165
+ <div class="selectorMenu__dot"></div>
166
+ {/if}
167
+ </div>
168
+ {#if option.icon}
169
+ <Icon Icon={option.icon} size="1.25rem" />
170
+ {/if}
171
+ {#if isSnippet(option.label)}
172
+ {@render option.label()}
173
+ {:else}
174
+ {option.label}
175
+ {/if}
176
+ </div>
177
+ {#if option.key}
178
+ <div class="selectorMenu__end">
179
+ <div class="selectorMenu__key">{option.key}</div>
180
+ </div>
181
+ {/if}
182
+ </li>
183
+ {/each}
184
+ </ul>
185
+ {#if footer}
186
+ <div class="selectorMenu__footer">
187
+ {@render footer({ footerProps })}
188
+ </div>
189
+ {/if}
190
+ </div>
191
+ {/if}
192
+ </div>
193
+
194
+ <style>
195
+ .selectorMenu {
196
+ position: relative;
197
+ display: inline-block;
198
+ width: 100%;
199
+ }
200
+ .selectorMenu--transparent .select__button {
201
+ border-color: transparent;
202
+ background: transparent;
203
+ &:hover {
204
+ border-color: var(--inputBorderColor);
205
+ }
206
+ }
207
+
208
+ .selectorMenu__popover {
209
+ position: absolute;
210
+ background: var(--popoverBg);
211
+ border: var(--borderThin);
212
+ border-radius: var(--radius-2);
213
+ box-shadow: var(--shadow-1);
214
+ white-space: nowrap;
215
+ z-index: 1000;
216
+ padding: 0.25rem;
217
+ list-style: none;
218
+ }
219
+ .selectorMenu__optionList {
220
+ max-height: 300px;
221
+ overflow-y: auto;
222
+ }
223
+
224
+ .selectorMenu__option {
225
+ width: 100%;
226
+ padding: 0.25rem 1rem;
227
+ cursor: pointer;
228
+ display: flex;
229
+ justify-content: space-between;
230
+ align-items: center;
231
+ font-size: 0.875rem;
232
+ gap: 3rem;
233
+ border: solid 2px transparent;
234
+ text-indent: -0.5rem;
235
+ line-height: 1.2rem;
236
+ }
237
+
238
+ .selectorMenu__option:hover,
239
+ .selectorMenu__option:focus {
240
+ background-color: var(--menuItemHover);
241
+ border: var(--menuItemBorderHover);
242
+ outline: none;
243
+ }
244
+ .selectorMenu__option--isActive {
245
+ background-color: var(--menuItemHover);
246
+ }
247
+
248
+ .selectorMenu__icon {
249
+ margin-left: auto;
250
+ }
251
+
252
+ .selectorMenu__dot {
253
+ width: 0.5rem;
254
+ height: 0.5rem;
255
+ background: var(--fgPrimary);
256
+ border-radius: 50%;
257
+ }
258
+ .selectorMenu__space {
259
+ width: 0.5rem;
260
+ height: 0.5rem;
261
+ }
262
+ .selectorMenu__selected {
263
+ display: flex;
264
+ align-items: center;
265
+ gap: 0.25rem;
266
+ flex-wrap: wrap;
267
+ }
268
+ .selectorMenu__start {
269
+ display: flex;
270
+ align-items: center;
271
+ gap: 1rem;
272
+ }
273
+ .selectorMenu__key {
274
+ display: flex;
275
+ text-indent: 0;
276
+ color: var(--fgMuted);
277
+ font-size: 0.75rem;
278
+ font-family: var(--font-mono);
279
+ }
280
+ </style>
@@ -0,0 +1,2 @@
1
+ export { default as SelectorMenu } from './SelectorMenu.svelte';
2
+ export * from './types';