@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,825 @@
1
+ <script lang="ts">
2
+ import { onMount, onDestroy } from 'svelte';
3
+ import { Editor, type JSONContent } from '@tiptap/core';
4
+ import StarterKit from '@tiptap/starter-kit';
5
+ import { Link } from '@tiptap/extension-link';
6
+ import Typography from '@tiptap/extension-typography';
7
+ import {
8
+ IconBold,
9
+ IconList,
10
+ IconListNumbers,
11
+ IconItalic,
12
+ IconLink,
13
+ IconSelector,
14
+ IconQuoteFilled,
15
+ IconCheck
16
+ } from '@tabler/icons-svelte';
17
+ import { Icon } from '../Icon';
18
+ import { Popover } from '../Popover';
19
+ import { Button } from '../Button';
20
+ import { Input } from '../Input';
21
+ import { computePosition, autoUpdate, offset, shift, flip } from '@floating-ui/dom';
22
+ import { IconButton } from '../Button';
23
+
24
+ let {
25
+ height = 'auto',
26
+ content = $bindable(undefined),
27
+ debug = false,
28
+ editable = true,
29
+ onChange
30
+ }: {
31
+ height?: number | string;
32
+ content?: JSONContent | null | undefined;
33
+ debug?: boolean;
34
+ editable?: boolean;
35
+ onChange?: () => void;
36
+ } = $props();
37
+
38
+ let element: HTMLDivElement | undefined = $state();
39
+ let editor: Editor | undefined = $state();
40
+ let editorReady = $state(false);
41
+
42
+ // Since we're now only using JSON content, this is no longer needed
43
+ // const isJsonContent = content !== null && content !== undefined && typeof content === 'object';
44
+
45
+ // We'll always use JSON, but for debug purposes we'll keep track of HTML
46
+ let editorHtml = $state('');
47
+
48
+ // Create explicit state variables for each button's active state
49
+ let isBold = $state(false);
50
+ let isItalic = $state(false);
51
+ let isLink = $state(false);
52
+ let isParagraph = $state(false);
53
+ let isH1 = $state(false);
54
+ let isH2 = $state(false);
55
+ let isH3 = $state(false);
56
+ let isBulletList = $state(false);
57
+ let isOrderedList = $state(false);
58
+ let isBlockquote = $state(false);
59
+
60
+ // Link popover state
61
+ let linkPopoverVisible = $state(false);
62
+ let linkPopoverElement: HTMLDivElement | undefined = $state();
63
+ let linkInputElement: HTMLInputElement | undefined = $state();
64
+ let currentLinkElement: HTMLAnchorElement | null = $state(null);
65
+ let currentLinkUrl = $state('');
66
+ let cleanupAutoUpdate: (() => void) | undefined;
67
+ let portalContainer: HTMLDivElement | undefined = $state();
68
+
69
+ // Create portal container in body when component is mounted
70
+ function createPortalContainer() {
71
+ // Check if portal container already exists
72
+ const existingContainer = document.getElementById('editorLinkPortal');
73
+ if (existingContainer) {
74
+ portalContainer = existingContainer as HTMLDivElement;
75
+ return;
76
+ }
77
+
78
+ // Create a new portal container
79
+ const container = document.createElement('div');
80
+ container.id = 'editorLinkPortal';
81
+ document.body.appendChild(container);
82
+ portalContainer = container;
83
+ }
84
+
85
+ const textType = $derived.by(() => {
86
+ if (isH1) return 'Huge';
87
+ if (isH2) return 'Large';
88
+ if (isH3) return 'Medium';
89
+ if (isParagraph) return 'Normal';
90
+ return '';
91
+ });
92
+
93
+ // Function to update all active states
94
+ function updateActiveStates() {
95
+ if (!editor) return;
96
+
97
+ isBold = editor.isActive('bold');
98
+ isItalic = editor.isActive('italic');
99
+ isLink = editor.isActive('link');
100
+ isParagraph = editor.isActive('paragraph');
101
+ isH1 = editor.isActive('heading', { level: 1 });
102
+ isH2 = editor.isActive('heading', { level: 2 });
103
+ isH3 = editor.isActive('heading', { level: 3 });
104
+ isBlockquote = editor.isActive('blockquote');
105
+ isBulletList = editor.isActive('bulletList');
106
+ isOrderedList = editor.isActive('orderedList');
107
+ }
108
+
109
+ // Effect to update editor content when it changes externally
110
+ $effect(() => {
111
+ if (editor && editorReady && content !== undefined) {
112
+ // Get current editor content
113
+ const currentContent = editor.getJSON();
114
+
115
+ // Only update if content actually changed (avoid infinite loops)
116
+ if (JSON.stringify(currentContent) !== JSON.stringify(content)) {
117
+ // Update editor with new content from outside
118
+ editor.commands.setContent(content || { type: 'doc', content: [{ type: 'paragraph' }] });
119
+ }
120
+ }
121
+ });
122
+
123
+ onMount(() => {
124
+ // Create portal container
125
+ createPortalContainer();
126
+
127
+ // Default empty content
128
+ const emptyContent = { type: 'doc', content: [{ type: 'paragraph' }] };
129
+
130
+ // Prepare content - always ensure we have valid JSON structure
131
+ let initialContent;
132
+
133
+ if (content === null || content === undefined) {
134
+ // Use empty content if nothing provided
135
+ initialContent = emptyContent;
136
+ } else if (typeof content === 'string') {
137
+ // If string was passed (shouldn't happen but just in case), convert to JSON
138
+ try {
139
+ // First, try to parse it as JSON string
140
+ initialContent = JSON.parse(content);
141
+ } catch (e) {
142
+ console.log(e);
143
+ // If it's plain HTML, initialize with it
144
+ initialContent = content;
145
+ // And immediately update our bound content to be JSON
146
+ setTimeout(() => {
147
+ if (editor) {
148
+ content = editor.getJSON();
149
+ }
150
+ }, 0);
151
+ }
152
+ } else {
153
+ // Content is already JSON object
154
+ initialContent = content;
155
+ }
156
+
157
+ editor = new Editor({
158
+ editable,
159
+ element: element,
160
+ extensions: [
161
+ StarterKit,
162
+ Typography,
163
+ Link.configure({
164
+ openOnClick: false,
165
+ HTMLAttributes: {
166
+ class: 'editor-link'
167
+ }
168
+ })
169
+ ],
170
+ content: initialContent, // Pass content with fallback for null/undefined
171
+ onSelectionUpdate: () => {
172
+ updateActiveStates();
173
+ },
174
+ onUpdate: ({ editor }) => {
175
+ // Always update content as JSON
176
+ const newJson = editor.getJSON();
177
+ content = newJson;
178
+ // Update HTML for debug purposes only
179
+ editorHtml = editor.getHTML();
180
+ updateActiveStates();
181
+
182
+ // Call onChange callback if provided
183
+ if (onChange) {
184
+ onChange();
185
+ }
186
+ },
187
+ onTransaction: () => {
188
+ updateActiveStates();
189
+ }
190
+ });
191
+
192
+ if (element) {
193
+ element.addEventListener('click', handleEditorClick, true);
194
+ element.addEventListener('mousedown', handleEditorMouseDown, true);
195
+ }
196
+
197
+ editorReady = true;
198
+ updateActiveStates();
199
+ });
200
+
201
+ onDestroy(() => {
202
+ if (editor) {
203
+ editor.destroy();
204
+ }
205
+
206
+ if (element) {
207
+ element.removeEventListener('click', handleEditorClick, true);
208
+ element.removeEventListener('mousedown', handleEditorMouseDown, true);
209
+ }
210
+
211
+ // Clean up floating UI positioning if active
212
+ if (cleanupAutoUpdate) {
213
+ cleanupAutoUpdate();
214
+ }
215
+
216
+ // Clean up the popover element from the portal container
217
+ if (linkPopoverElement && portalContainer && portalContainer.contains(linkPopoverElement)) {
218
+ portalContainer.removeChild(linkPopoverElement);
219
+ }
220
+
221
+ // Remove portal container if it exists and we are the last editor instance
222
+ if (portalContainer && portalContainer.childNodes.length === 0) {
223
+ if (document.body.contains(portalContainer)) {
224
+ document.body.removeChild(portalContainer);
225
+ }
226
+ }
227
+ });
228
+
229
+ function handleEditorClick(e: MouseEvent) {
230
+ const target = e.target as HTMLElement;
231
+ const linkElement = target.closest('a');
232
+
233
+ if (linkElement && linkElement.classList.contains('editor-link')) {
234
+ e.preventDefault();
235
+ e.stopPropagation();
236
+ e.stopImmediatePropagation();
237
+
238
+ hideLinkPopover();
239
+
240
+ currentLinkElement = linkElement;
241
+ currentLinkUrl = linkElement.getAttribute('href') || '';
242
+ showLinkPopover(linkElement);
243
+ return false;
244
+ } else if (!linkElement) {
245
+ hideLinkPopover();
246
+ }
247
+ }
248
+
249
+ function handleEditorMouseDown(e: MouseEvent) {
250
+ const target = e.target as HTMLElement;
251
+ const linkElement = target.closest('a');
252
+
253
+ if (linkElement && linkElement.classList.contains('editor-link')) {
254
+ e.preventDefault();
255
+ e.stopPropagation();
256
+ return false;
257
+ }
258
+ }
259
+
260
+ // This function is used by the <svelte:document> event handler
261
+ function handleDocumentClick(e: MouseEvent) {
262
+ // Only close if clicking outside the popover and link
263
+ if (linkPopoverVisible) {
264
+ const target = e.target as HTMLElement;
265
+ if (
266
+ linkPopoverElement &&
267
+ !linkPopoverElement.contains(target) &&
268
+ currentLinkElement !== target &&
269
+ !currentLinkElement?.contains(target) &&
270
+ !target.closest('.editor__btn') && // Don't close when clicking toolbar buttons
271
+ // Also check if we're clicking inside the editor but not on a link
272
+ !(element?.contains(target) && !target.closest('a'))
273
+ ) {
274
+ hideLinkPopover();
275
+ }
276
+ }
277
+ }
278
+
279
+ function getSelectionCoordinates(): { left: number; bottom: number } | null {
280
+ if (!editor) return null;
281
+
282
+ const view = editor.view;
283
+ const { from, to } = view.state.selection;
284
+
285
+ if (from === to) return null; // No selection
286
+
287
+ // Get the DOM range for the current selection
288
+ const start = view.coordsAtPos(from);
289
+ const end = view.coordsAtPos(to);
290
+
291
+ return {
292
+ left: (start.left + end.left) / 2,
293
+ bottom: Math.max(start.bottom, end.bottom)
294
+ };
295
+ }
296
+
297
+ function showLinkPopover(anchorElement: HTMLElement) {
298
+ if (!linkPopoverElement || !portalContainer) return;
299
+
300
+ // Move the popover element to the portal container if it's not already there
301
+ if (!portalContainer.contains(linkPopoverElement)) {
302
+ // Create a new popover element in the portal container
303
+ const popoverClone = linkPopoverElement.cloneNode(true) as HTMLDivElement;
304
+ portalContainer.appendChild(popoverClone);
305
+
306
+ // Update our reference to the new popover element
307
+ linkPopoverElement = popoverClone;
308
+
309
+ // Find the input element within the cloned popover
310
+ linkInputElement = popoverClone.querySelector('.linkPopover__input') as HTMLInputElement;
311
+
312
+ // Setup event handlers for the portal popover
313
+ setupPortalPopoverEvents();
314
+ }
315
+
316
+ // Always update the input field value with the current link URL
317
+ if (linkInputElement) {
318
+ linkInputElement.value = currentLinkUrl;
319
+ }
320
+
321
+ linkPopoverVisible = true;
322
+
323
+ // Position the popover using Floating UI
324
+ if (cleanupAutoUpdate) {
325
+ cleanupAutoUpdate();
326
+ }
327
+
328
+ cleanupAutoUpdate = autoUpdate(anchorElement, linkPopoverElement, () => {
329
+ computePosition(anchorElement, linkPopoverElement!, {
330
+ placement: 'bottom',
331
+ middleware: [offset(8), shift({ padding: 10 }), flip()],
332
+ strategy: 'fixed'
333
+ }).then(({ x, y }) => {
334
+ if (linkPopoverElement) {
335
+ Object.assign(linkPopoverElement.style, {
336
+ left: `${x}px`,
337
+ top: `${y}px`,
338
+ position: 'fixed',
339
+ display: 'block',
340
+ transform: 'translateZ(0)'
341
+ });
342
+ }
343
+ });
344
+ });
345
+
346
+ // Focus the input field if we're editing
347
+ setTimeout(() => {
348
+ if (linkInputElement) {
349
+ linkInputElement.focus();
350
+ linkInputElement.select();
351
+ }
352
+ }, 10);
353
+ }
354
+
355
+ function hideLinkPopover() {
356
+ linkPopoverVisible = false;
357
+ currentLinkElement = null;
358
+ currentLinkUrl = '';
359
+
360
+ if (cleanupAutoUpdate) {
361
+ cleanupAutoUpdate();
362
+ cleanupAutoUpdate = undefined;
363
+ }
364
+
365
+ // Hide the popover in the portal if it exists
366
+ if (linkPopoverElement && portalContainer && portalContainer.contains(linkPopoverElement)) {
367
+ linkPopoverElement.style.display = 'none';
368
+ }
369
+ }
370
+
371
+ function addOrUpdateLink() {
372
+ if (!editor) return;
373
+
374
+ if (currentLinkUrl.trim() === '') {
375
+ removeLink();
376
+ return;
377
+ }
378
+
379
+ try {
380
+ // If we're updating an existing link, we need to select it first
381
+ if (currentLinkElement && currentLinkElement.tagName === 'A') {
382
+ const pos = editor.view.posAtDOM(currentLinkElement, 0);
383
+ const end = pos + currentLinkElement.textContent!.length;
384
+ editor.chain().focus().setTextSelection({ from: pos, to: end }).run();
385
+ }
386
+
387
+ editor.chain().focus().extendMarkRange('link').setLink({ href: currentLinkUrl }).run();
388
+ hideLinkPopover();
389
+ } catch (e) {
390
+ console.error(e);
391
+ }
392
+ }
393
+
394
+ function removeLink() {
395
+ if (!editor) return;
396
+
397
+ if (currentLinkElement && currentLinkElement.tagName === 'A') {
398
+ const pos = editor.view.posAtDOM(currentLinkElement, 0);
399
+ const end = pos + currentLinkElement.textContent!.length;
400
+ editor.chain().focus().setTextSelection({ from: pos, to: end }).unsetLink().run();
401
+ } else {
402
+ editor.chain().focus().extendMarkRange('link').unsetLink().run();
403
+ }
404
+
405
+ hideLinkPopover();
406
+ }
407
+
408
+ function visitLink() {
409
+ if (currentLinkUrl) {
410
+ window.open(currentLinkUrl, '_blank');
411
+ hideLinkPopover();
412
+ }
413
+ }
414
+
415
+ // Setup portal popover event handlers when the component is mounted
416
+ function setupPortalPopoverEvents() {
417
+ if (!linkPopoverElement || !portalContainer) return;
418
+
419
+ // Add event handlers for the buttons in the portal popover
420
+ const confirmButton = linkPopoverElement.querySelector('.linkPopover__inputRow button') as HTMLElement;
421
+ if (confirmButton) {
422
+ // Use the correct typing approach for TypeScript with Svelte 5
423
+ (confirmButton as unknown as { onclick: typeof addOrUpdateLink }).onclick = addOrUpdateLink;
424
+ }
425
+
426
+ // Add event handlers for the visit and remove buttons
427
+ const buttons = linkPopoverElement.querySelectorAll('.linkPopover__actions button');
428
+ if (buttons.length >= 2) {
429
+ (buttons[0] as unknown as { onclick: typeof visitLink }).onclick = visitLink;
430
+ (buttons[1] as unknown as { onclick: typeof removeLink }).onclick = removeLink;
431
+ }
432
+
433
+ if (linkInputElement) {
434
+ // Set up keydown handler for Enter key
435
+ (linkInputElement as unknown as { onkeydown: typeof handleLinkInputKeydown }).onkeydown = handleLinkInputKeydown;
436
+
437
+ // Set up input handler to keep our state variable in sync
438
+ linkInputElement.addEventListener('input', (e: Event) => {
439
+ currentLinkUrl = (e.target as HTMLInputElement).value;
440
+ });
441
+ }
442
+ }
443
+
444
+ function setNewLink() {
445
+ if (!editor) return;
446
+
447
+ // For creating a new link (toolbar button)
448
+ hideLinkPopover(); // Close any existing popover first
449
+
450
+ const selection = editor.view.state.selection;
451
+ const hasSelection = !selection.empty;
452
+
453
+ if (hasSelection) {
454
+ // With selection, first create the link with a temporary URL
455
+ currentLinkUrl = '';
456
+ editor.chain().focus().extendMarkRange('link').setLink({ href: 'https://' }).run();
457
+
458
+ // Now find the newly created link
459
+ setTimeout(() => {
460
+ // Get coordinates of the selection
461
+ const coords = getSelectionCoordinates();
462
+ if (!coords) return;
463
+
464
+ // Create a temp anchor element for positioning the popover
465
+ const tempElement = document.createElement('span');
466
+ tempElement.style.position = 'absolute';
467
+ tempElement.style.left = `${coords.left}px`;
468
+ tempElement.style.top = `${coords.bottom}px`;
469
+ document.body.appendChild(tempElement);
470
+
471
+ currentLinkElement = tempElement as unknown as HTMLAnchorElement;
472
+ showLinkPopover(tempElement);
473
+
474
+ // Clean up the temporary element after we're done
475
+ setTimeout(() => {
476
+ if (document.body.contains(tempElement)) {
477
+ document.body.removeChild(tempElement);
478
+ }
479
+ }, 100);
480
+ }, 10);
481
+ } else {
482
+ // For empty selection, insert a placeholder link
483
+ currentLinkUrl = 'https://';
484
+
485
+ // Insert link with default text
486
+ editor.chain().focus().insertContent('<a href="https://">link</a>').run();
487
+
488
+ // Find the newly inserted link and show popover
489
+ setTimeout(() => {
490
+ if (!element) return;
491
+
492
+ const links = element.querySelectorAll('a[href="https://"]');
493
+ if (links && links.length > 0) {
494
+ currentLinkElement = links[links.length - 1] as HTMLAnchorElement;
495
+ showLinkPopover(currentLinkElement);
496
+ }
497
+ }, 10);
498
+ }
499
+ }
500
+
501
+ // Handle enter key in the link input
502
+ function handleLinkInputKeydown(e: KeyboardEvent) {
503
+ if (e.key === 'Enter') {
504
+ e.preventDefault();
505
+ addOrUpdateLink();
506
+ }
507
+ }
508
+ </script>
509
+
510
+ <svelte:document on:click={handleDocumentClick} />
511
+
512
+ <div class={['editor', !editable && 'editor--notEdtiable']} style={`height: ${height}; max-height: ${height}`}>
513
+ {#if editable}
514
+ <div class="editor__toolbar">
515
+ {#if editor}
516
+ <button
517
+ onclick={() => editor?.chain().focus().toggleBold().run()}
518
+ class={['editor__btn', isBold && 'isActive']}
519
+ >
520
+ <Icon Icon={IconBold} size="20px" stroke={2} />
521
+ </button>
522
+ <button
523
+ onclick={() => editor?.chain().focus().toggleItalic().run()}
524
+ class={['editor__btn', isItalic && 'isActive']}
525
+ >
526
+ <Icon Icon={IconItalic} size="20px" stroke={2} />
527
+ </button>
528
+ <button onclick={setNewLink} class={['editor__btn', isLink && 'isActive']}>
529
+ <Icon Icon={IconLink} size="20px" stroke={2} />
530
+ </button>
531
+ <button
532
+ onclick={() => editor?.chain().focus().toggleBulletList().run()}
533
+ class={['editor__btn', isBulletList && 'isActive']}
534
+ >
535
+ <Icon Icon={IconList} size="20px" stroke={2} />
536
+ </button>
537
+ <button
538
+ onclick={() => editor?.chain().focus().toggleOrderedList().run()}
539
+ class={['editor__btn', isOrderedList && 'isActive']}
540
+ >
541
+ <Icon Icon={IconListNumbers} size="20px" stroke={2} />
542
+ </button>
543
+ <button
544
+ onclick={() => editor?.chain().focus().toggleBlockquote().run()}
545
+ class={['editor__btn', isBlockquote && 'isActive']}
546
+ >
547
+ <Icon Icon={IconQuoteFilled} size="20px" stroke={2} />
548
+ </button>
549
+ <Popover>
550
+ {#snippet trigger()}
551
+ <div class="editor__toolbarTrigger">
552
+ {textType}
553
+ <Icon Icon={IconSelector} size="1rem" color="var(--fgMuted)" />
554
+ </div>
555
+ {/snippet}
556
+ {#snippet content()}
557
+ <div class="editor__toolbarTextOptions">
558
+ <button
559
+ onclick={() => editor?.chain().focus().setParagraph().run()}
560
+ class:active={['editor__toolbarTextP', isParagraph && 'isActive']}
561
+ >
562
+ Normal
563
+ </button>
564
+ <button
565
+ onclick={() => editor?.chain().focus().toggleHeading({ level: 3 }).run()}
566
+ class={['editor__toolbarTextH3', isH3 && 'isActive']}
567
+ >
568
+ Medium
569
+ </button>
570
+ <button
571
+ onclick={() => editor?.chain().focus().toggleHeading({ level: 2 }).run()}
572
+ class={['editor__toolbarTextH2', isH2 && 'isActive']}
573
+ >
574
+ Large
575
+ </button>
576
+ <button
577
+ onclick={() => editor?.chain().focus().toggleHeading({ level: 1 }).run()}
578
+ class={['editor__toolbarTextH1', isH1 && 'isActive']}
579
+ >
580
+ Huge
581
+ </button>
582
+ </div>
583
+ {/snippet}
584
+ </Popover>
585
+ {/if}
586
+ </div>
587
+ {/if}
588
+
589
+ <div class="editor__content">
590
+ <div bind:this={element}></div>
591
+ </div>
592
+
593
+ <!-- Link Popover Template (hidden) - Will be cloned to portal container -->
594
+ <div bind:this={linkPopoverElement} class="linkPopover" style="display: none;">
595
+ <div class="linkPopover__content">
596
+ <div class="linkPopover__inputRow">
597
+ <Input
598
+ bind:element={linkInputElement}
599
+ type="text"
600
+ bind:value={currentLinkUrl}
601
+ placeholder="https://"
602
+ class="linkPopover__input"
603
+ onkeydown={handleLinkInputKeydown}
604
+ />
605
+ <IconButton onclick={addOrUpdateLink}>
606
+ <Icon Icon={IconCheck} stroke={2} />
607
+ </IconButton>
608
+ </div>
609
+ <div class="linkPopover__actions">
610
+ <Button onclick={visitLink}>Go to link</Button>
611
+ <Button variant="danger" onclick={removeLink}>Remove</Button>
612
+ </div>
613
+ </div>
614
+ </div>
615
+
616
+ {#if debug}
617
+ <div class="editor__state">
618
+ <h4>Editor Status: {editorReady ? 'Ready' : 'Loading...'}</h4>
619
+ <h4>Content Type: JSON</h4>
620
+ {#if content === null || content === undefined}
621
+ <pre>null/undefined</pre>
622
+ {:else}
623
+ <pre>{JSON.stringify(content, null, 2)}</pre>
624
+ {/if}
625
+
626
+ <h4>HTML representation:</h4>
627
+ <pre>{editorHtml}</pre>
628
+ </div>
629
+ {/if}
630
+ </div>
631
+
632
+ <style>
633
+ .editor {
634
+ position: relative;
635
+ width: 100%;
636
+ border: var(--borderThin);
637
+ border-radius: var(--radius-2);
638
+ overflow-y: auto;
639
+ }
640
+ .editor.editor--notEdtiable {
641
+ pointer-events: none;
642
+ border: none;
643
+ border-radius: 0;
644
+ }
645
+ .editor.editor--notEdtiable .editor__content {
646
+ pointer-events: none;
647
+ padding: 0;
648
+ }
649
+ .editor__toolbar {
650
+ position: sticky;
651
+ top: 0;
652
+ background-color: var(--bg);
653
+ z-index: 2;
654
+ display: flex;
655
+ gap: 0.25rem;
656
+ padding: 0.5rem 0.5rem;
657
+ }
658
+ .editor__content {
659
+ padding: 1rem;
660
+ position: relative;
661
+ }
662
+ .editor__state {
663
+ padding: 1rem;
664
+ border-top: var(--borderThin);
665
+ background-color: var(--contrastLow);
666
+ font-size: 0.85rem;
667
+ }
668
+ .editor__state pre {
669
+ max-height: 150px;
670
+ overflow-y: auto;
671
+ background-color: var(--contrastMedium);
672
+ padding: 0.5rem;
673
+ border-radius: var(--radius-2);
674
+ }
675
+ .editor__btn {
676
+ border: solid 2px transparent;
677
+ border-radius: var(--radius-2);
678
+ display: flex;
679
+ align-items: center;
680
+ justify-content: center;
681
+ cursor: pointer;
682
+ height: 2rem;
683
+ width: 2rem;
684
+ }
685
+ .editor__btn.isActive {
686
+ border-color: var(--fg);
687
+ }
688
+ .editor__btn:hover {
689
+ background-color: var(--iconBtn-bgHover);
690
+ border: var(--iconBtn-borderHover);
691
+ }
692
+
693
+ .linkPopover {
694
+ position: fixed;
695
+ z-index: 9999;
696
+ filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 0.1));
697
+ will-change: transform;
698
+ }
699
+ .linkPopover__content {
700
+ box-shadow: var(--shadow-1);
701
+ background-color: var(--bg);
702
+ border: var(--borderThin);
703
+ border-radius: var(--radius-2);
704
+ padding: 0.5rem;
705
+ width: 280px;
706
+ }
707
+ .linkPopover__inputRow {
708
+ display: flex;
709
+ gap: 0.5rem;
710
+ margin-bottom: 0.75rem;
711
+ }
712
+
713
+ :global(#editorLinkPortal) {
714
+ position: fixed;
715
+ top: 0;
716
+ left: 0;
717
+ width: 0;
718
+ height: 0;
719
+ z-index: 9999;
720
+ pointer-events: none;
721
+ }
722
+ :global(#editorLinkPortal .linkPopover) {
723
+ pointer-events: auto;
724
+ }
725
+ :global {
726
+ .tiptap:focus-visible {
727
+ outline: none;
728
+ }
729
+ .editor__content p {
730
+ line-height: 1.5;
731
+ }
732
+ .editor__content ul {
733
+ list-style-type: disc;
734
+ margin-left: 1.5rem;
735
+ }
736
+ .editor__content ol {
737
+ list-style-type: decimal;
738
+ margin-left: 1.5rem;
739
+ }
740
+ .editor__content h1,
741
+ .editor__content h2,
742
+ .editor__content h3 {
743
+ font-weight: 600;
744
+ }
745
+ .editor__content h1 {
746
+ font-size: 2rem;
747
+ }
748
+ .editor__content h2 {
749
+ font-size: 1.5rem;
750
+ }
751
+ .editor__content h3 {
752
+ font-size: 1.25rem;
753
+ }
754
+ .editor__content a {
755
+ color: var(--link-color);
756
+ text-decoration: none;
757
+ cursor: pointer;
758
+ font-weight: var(--font-weight-6);
759
+ }
760
+ .editor__content a:hover {
761
+ text-decoration: underline;
762
+ }
763
+ .editor__content strong {
764
+ font-weight: 700;
765
+ }
766
+ .editor__content em {
767
+ font-style: italic;
768
+ }
769
+ .editor__content blockquote {
770
+ border-left: 4px solid var(--contrastMedium);
771
+ color: var(--fgMuted);
772
+ padding-left: 1rem;
773
+ margin-left: 0;
774
+ font-family: var(--font-sans);
775
+ }
776
+ .editor__content hr {
777
+ height: 1px;
778
+ background-color: var(--contrastMedium);
779
+ }
780
+ .editor__toolbarTextOptions {
781
+ display: flex;
782
+ flex-direction: column;
783
+ gap: 0.5rem;
784
+ }
785
+ .editor__toolbarTextOptions button {
786
+ cursor: pointer;
787
+ padding: 0.25rem 0.5rem;
788
+ width: 100%;
789
+ }
790
+ .editor__toolbarTextOptions button:hover {
791
+ background-color: var(--contrastLow);
792
+ }
793
+
794
+ .editor__toolbarTextH1,
795
+ .editor__toolbarTextH2,
796
+ .editor__toolbarTextH3 {
797
+ font-weight: 600;
798
+ }
799
+ .editor__toolbarTextP {
800
+ font-weight: normal;
801
+ }
802
+ .editor__toolbarTextH1 {
803
+ font-size: 2.5rem;
804
+ }
805
+ .editor__toolbarTextH2 {
806
+ font-size: 2rem;
807
+ }
808
+ .editor__toolbarTextH3 {
809
+ font-size: 1.5rem;
810
+ }
811
+ .editor__toolbarTrigger {
812
+ display: flex;
813
+ align-items: center;
814
+ gap: 0.5rem;
815
+ cursor: pointer;
816
+ border: solid 2px transparent;
817
+ padding: 0.25rem 0.5rem;
818
+ border-radius: var(--radius-2);
819
+ }
820
+ .editor__toolbarTrigger:hover {
821
+ background-color: var(--iconBtn-bgHover);
822
+ border: var(--iconBtn-borderHover);
823
+ }
824
+ }
825
+ </style>