svelte-comp 1.2.7 → 1.3.5

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 (139) hide show
  1. package/README.md +12 -11
  2. package/dist/App.svelte +497 -2
  3. package/dist/app.css +2 -3
  4. package/dist/app.d.ts +10 -0
  5. package/dist/lang.d.ts +3 -3
  6. package/dist/lang.js +3 -3
  7. package/dist/lib/Accordion.svelte +14 -14
  8. package/dist/lib/Badge.svelte +44 -0
  9. package/dist/lib/Badge.svelte.d.ts +10 -0
  10. package/dist/lib/Button.svelte +23 -8
  11. package/dist/lib/Calendar.svelte +384 -377
  12. package/dist/lib/Card.svelte +6 -6
  13. package/dist/lib/Carousel.svelte +16 -16
  14. package/dist/lib/Carousel.svelte.d.ts +1 -1
  15. package/dist/lib/CheckBox.svelte +2 -2
  16. package/dist/lib/CodeView.svelte +6 -5
  17. package/dist/lib/ColorPicker.svelte +6 -6
  18. package/dist/lib/ContextMenu.svelte +328 -0
  19. package/dist/lib/ContextMenu.svelte.d.ts +14 -0
  20. package/dist/lib/DatePicker.svelte +161 -161
  21. package/dist/lib/Dialog.svelte +10 -10
  22. package/dist/lib/Field.svelte +1 -1
  23. package/dist/lib/FilePicker.svelte +127 -74
  24. package/dist/lib/FilePicker.svelte.d.ts +6 -3
  25. package/dist/lib/Hamburger.svelte +27 -21
  26. package/dist/lib/InstallPWA.svelte +94 -0
  27. package/dist/lib/InstallPWA.svelte.d.ts +8 -0
  28. package/dist/lib/Menu.svelte +18 -18
  29. package/dist/lib/NoticeBase.svelte +140 -0
  30. package/dist/lib/NoticeBase.svelte.d.ts +43 -0
  31. package/dist/lib/PrimaryColorSelect.svelte +6 -6
  32. package/dist/lib/ProgressCircle.svelte +7 -9
  33. package/dist/lib/ProgressCircle.svelte.d.ts +7 -9
  34. package/dist/lib/SearchInput.svelte +6 -6
  35. package/dist/lib/Select.svelte +2 -2
  36. package/dist/lib/Slider.svelte +1 -1
  37. package/dist/lib/Splitter.svelte +15 -6
  38. package/dist/lib/Switch.svelte +5 -4
  39. package/dist/lib/Tabs.svelte +6 -6
  40. package/dist/lib/ThemeToggle.svelte +1 -0
  41. package/dist/lib/TimePicker.svelte +103 -95
  42. package/dist/lib/TimePickerNew.svelte +634 -0
  43. package/dist/lib/TimePickerNew.svelte.d.ts +49 -0
  44. package/dist/lib/Toast.svelte +17 -120
  45. package/dist/lib/Tooltip.svelte +7 -7
  46. package/dist/lib/Topbar.svelte +112 -0
  47. package/dist/lib/Topbar.svelte.d.ts +44 -0
  48. package/dist/lib/__tests__/Accordion.test.d.ts +1 -0
  49. package/dist/lib/__tests__/Accordion.test.js +171 -0
  50. package/dist/lib/__tests__/Badge.test.d.ts +1 -0
  51. package/dist/lib/__tests__/Badge.test.js +41 -0
  52. package/dist/lib/__tests__/Button.test.d.ts +1 -0
  53. package/dist/lib/__tests__/Button.test.js +269 -0
  54. package/dist/lib/__tests__/Calendar.test.d.ts +1 -0
  55. package/dist/lib/__tests__/Calendar.test.js +171 -0
  56. package/dist/lib/__tests__/Card.test.d.ts +1 -0
  57. package/dist/lib/__tests__/Card.test.js +148 -0
  58. package/dist/lib/__tests__/Carousel.test.d.ts +1 -0
  59. package/dist/lib/__tests__/Carousel.test.js +439 -0
  60. package/dist/lib/__tests__/CheckBox.test.d.ts +1 -0
  61. package/dist/lib/__tests__/CheckBox.test.js +152 -0
  62. package/dist/lib/__tests__/CodeView.test.d.ts +1 -0
  63. package/dist/lib/__tests__/CodeView.test.js +157 -0
  64. package/dist/lib/__tests__/ColorPicker.test.d.ts +1 -0
  65. package/dist/lib/__tests__/ColorPicker.test.js +93 -0
  66. package/dist/lib/__tests__/ContextMenu.test.d.ts +1 -0
  67. package/dist/lib/__tests__/ContextMenu.test.js +67 -0
  68. package/dist/lib/__tests__/DatePicker.test.d.ts +1 -0
  69. package/dist/lib/__tests__/DatePicker.test.js +108 -0
  70. package/dist/lib/__tests__/Dialog.test.d.ts +1 -0
  71. package/dist/lib/__tests__/Dialog.test.js +183 -0
  72. package/dist/lib/__tests__/Field.test.d.ts +1 -0
  73. package/dist/lib/__tests__/Field.test.js +190 -0
  74. package/dist/lib/__tests__/FilePicker.test.d.ts +1 -0
  75. package/dist/lib/__tests__/FilePicker.test.js +179 -0
  76. package/dist/lib/__tests__/Form.integration.test.d.ts +1 -0
  77. package/dist/lib/__tests__/Form.integration.test.js +158 -0
  78. package/dist/lib/__tests__/Form.test.d.ts +1 -0
  79. package/dist/lib/__tests__/Form.test.js +463 -0
  80. package/dist/lib/__tests__/Hamburger.test.d.ts +1 -0
  81. package/dist/lib/__tests__/Hamburger.test.js +161 -0
  82. package/dist/lib/__tests__/InstallPWA.test.d.ts +1 -0
  83. package/dist/lib/__tests__/InstallPWA.test.js +15 -0
  84. package/dist/lib/__tests__/Menu.test.d.ts +1 -0
  85. package/dist/lib/__tests__/Menu.test.js +285 -0
  86. package/dist/lib/__tests__/NoticeBase.test.d.ts +1 -0
  87. package/dist/lib/__tests__/NoticeBase.test.js +60 -0
  88. package/dist/lib/__tests__/PaginatedCard.test.d.ts +1 -0
  89. package/dist/lib/__tests__/PaginatedCard.test.js +89 -0
  90. package/dist/lib/__tests__/Pagination.test.d.ts +1 -0
  91. package/dist/lib/__tests__/Pagination.test.js +168 -0
  92. package/dist/lib/__tests__/PrimaryColorSelect.test.d.ts +1 -0
  93. package/dist/lib/__tests__/PrimaryColorSelect.test.js +92 -0
  94. package/dist/lib/__tests__/ProgressBar.test.d.ts +1 -0
  95. package/dist/lib/__tests__/ProgressBar.test.js +69 -0
  96. package/dist/lib/__tests__/ProgressCircle.test.d.ts +1 -0
  97. package/dist/lib/__tests__/ProgressCircle.test.js +71 -0
  98. package/dist/lib/__tests__/Radio.test.d.ts +1 -0
  99. package/dist/lib/__tests__/Radio.test.js +127 -0
  100. package/dist/lib/__tests__/SearchInput.test.d.ts +1 -0
  101. package/dist/lib/__tests__/SearchInput.test.js +80 -0
  102. package/dist/lib/__tests__/Select.test.d.ts +1 -0
  103. package/dist/lib/__tests__/Select.test.js +408 -0
  104. package/dist/lib/__tests__/Slider.test.d.ts +1 -0
  105. package/dist/lib/__tests__/Slider.test.js +213 -0
  106. package/dist/lib/__tests__/Splitter.test.d.ts +1 -0
  107. package/dist/lib/__tests__/Splitter.test.js +87 -0
  108. package/dist/lib/__tests__/Switch.test.d.ts +1 -0
  109. package/dist/lib/__tests__/Switch.test.js +97 -0
  110. package/dist/lib/__tests__/Table.test.d.ts +1 -0
  111. package/dist/lib/__tests__/Table.test.js +349 -0
  112. package/dist/lib/__tests__/Tabs.test.d.ts +1 -0
  113. package/dist/lib/__tests__/Tabs.test.js +262 -0
  114. package/dist/lib/__tests__/ThemeToggle.test.d.ts +1 -0
  115. package/dist/lib/__tests__/ThemeToggle.test.js +84 -0
  116. package/dist/lib/__tests__/TimePicker.test.d.ts +1 -0
  117. package/dist/lib/__tests__/TimePicker.test.js +146 -0
  118. package/dist/lib/__tests__/TimePickerNew.test.d.ts +1 -0
  119. package/dist/lib/__tests__/TimePickerNew.test.js +322 -0
  120. package/dist/lib/__tests__/Toast.test.d.ts +1 -0
  121. package/dist/lib/__tests__/Toast.test.js +135 -0
  122. package/dist/lib/__tests__/Tooltip.test.d.ts +1 -0
  123. package/dist/lib/__tests__/Tooltip.test.js +171 -0
  124. package/dist/lib/__tests__/Topbar.test.d.ts +1 -0
  125. package/dist/lib/__tests__/Topbar.test.js +25 -0
  126. package/dist/lib/__tests__/setupLangContext.d.ts +1 -0
  127. package/dist/lib/__tests__/setupLangContext.js +65 -0
  128. package/dist/lib/__tests__/storage.test.d.ts +1 -0
  129. package/dist/lib/__tests__/storage.test.js +124 -0
  130. package/dist/lib/__tests__/utils.test.d.ts +1 -0
  131. package/dist/lib/__tests__/utils.test.js +11 -0
  132. package/dist/lib/index.d.ts +5 -0
  133. package/dist/lib/index.js +5 -0
  134. package/dist/lib/lang.d.ts +64 -0
  135. package/dist/lib/lang.js +64 -0
  136. package/dist/lib/types/index.d.ts +1 -0
  137. package/dist/styles.css +2 -0
  138. package/dist/utils/index.js +15 -4
  139. package/package.json +12 -12
@@ -53,16 +53,16 @@
53
53
  }: Props = $props();
54
54
 
55
55
  const paddingSizes: Record<SizeKey, string> = {
56
- xs: "px-3 py-2",
57
- sm: "px-4 py-2",
58
- md: "px-5 py-3",
59
- lg: "px-6 py-4",
60
- xl: "px-7 py-5",
56
+ xs: "px-[calc(var(--spacing-sm)+var(--spacing-xs))] py-[var(--spacing-sm)]",
57
+ sm: "px-[var(--spacing-md)] py-[var(--spacing-sm)]",
58
+ md: "px-[calc(var(--spacing-md)+var(--spacing-xs))] py-[calc(var(--spacing-sm)+var(--spacing-xs))]",
59
+ lg: "px-[calc(var(--spacing-md)+var(--spacing-sm))] py-[var(--spacing-md)]",
60
+ xl: "px-[calc(var(--spacing-md)+var(--spacing-sm)+var(--spacing-xs))] py-[calc(var(--spacing-md)+var(--spacing-xs))]",
61
61
  };
62
62
 
63
63
  const cardClass = $derived(
64
64
  cx(
65
- "bg-[var(--color-bg-surface)] border border-[var(--border-color-default)] rounded-xl shadow-sm overflow-hidden",
65
+ "bg-[var(--color-bg-surface)] border border-[var(--border-color-default)] rounded-[var(--radius-xl)] shadow-[0_1px_2px_var(--shadow-color)] overflow-hidden",
66
66
  TEXT[sz],
67
67
  "flex flex-col",
68
68
  externalClass
@@ -34,7 +34,7 @@
34
34
  */
35
35
  import Card from "./Card.svelte";
36
36
  import type { HTMLAttributes } from "svelte/elements";
37
- import type { SizeKey, CarouselItem } from "./types";
37
+ import { TEXT, type SizeKey, type CarouselItem } from "./types";
38
38
  import { cx } from "../utils";
39
39
 
40
40
  type Props = HTMLAttributes<HTMLDivElement> & {
@@ -66,19 +66,19 @@
66
66
  "relative w-full overflow-hidden rounded-[var(--radius-lg)] bg-[var(--color-bg-surface)]";
67
67
 
68
68
  const sizes: Record<SizeKey, string> = {
69
- xs: "rounded-[var(--radius-md)] text-sm",
70
- sm: "rounded-[var(--radius-md)] text-base",
71
- md: "rounded-[var(--radius-lg)] text-lg",
72
- lg: "rounded-[var(--radius-lg)] text-xl",
73
- xl: "rounded-[var(--radius-xl)] text-2xl",
69
+ xs: cx("rounded-[var(--radius-md)]", TEXT.xs),
70
+ sm: cx("rounded-[var(--radius-md)]", TEXT.sm),
71
+ md: cx("rounded-[var(--radius-lg)]", TEXT.md),
72
+ lg: cx("rounded-[var(--radius-lg)]", TEXT.lg),
73
+ xl: cx("rounded-[var(--radius-xl)]", TEXT.xl),
74
74
  };
75
75
 
76
76
  const contentSize: Record<SizeKey, string> = {
77
- xs: "p-3 gap-2",
78
- sm: "p-4 gap-2.5",
79
- md: "p-5 gap-3",
80
- lg: "p-6 gap-4",
81
- xl: "p-8 gap-5",
77
+ xs: "p-[calc(var(--spacing-sm)+var(--spacing-xs))] gap-[var(--spacing-sm)]",
78
+ sm: "p-[var(--spacing-md)] gap-[calc(var(--spacing-sm)+var(--spacing-xs)/2)]",
79
+ md: "p-[calc(var(--spacing-md)+var(--spacing-xs))] gap-[calc(var(--spacing-sm)+var(--spacing-xs))]",
80
+ lg: "p-[calc(var(--spacing-md)+var(--spacing-sm))] gap-[var(--spacing-md)]",
81
+ xl: "p-[var(--spacing-xl)] gap-[calc(var(--spacing-md)+var(--spacing-xs))]",
82
82
  };
83
83
 
84
84
  const arrowSize: Record<SizeKey, string> = {
@@ -123,14 +123,14 @@
123
123
  const arrowClass = $derived(
124
124
  cx(
125
125
  arrowSize[sz],
126
- "rounded-full bg-[var(--color-bg-surface)] shadow-lg flex items-center justify-center [color:var(--color-text-default)] hover:bg-[var(--color-bg-hover)] transition-colors focus-visible:ring-2 focus-visible:ring-[var(--border-color-focus)] focus:outline-none"
126
+ "rounded-full bg-[var(--color-bg-surface)] shadow-[0_8px_16px_var(--shadow-color)] flex items-center justify-center [color:var(--color-text-default)] hover:bg-[var(--color-bg-hover)] transition-colors focus-visible:ring-2 focus-visible:ring-[var(--border-color-focus)] focus:outline-none"
127
127
  )
128
128
  );
129
129
 
130
130
  const dotClass = $derived(
131
131
  cx(
132
132
  dotSize[sz],
133
- "rounded-full transition-all duration-200 focus-visible:ring-2 focus-visible:ring-[var(--border-color-focus)] focus:outline-none"
133
+ "rounded-full transition-all duration-[var(--transition-fast)] focus-visible:ring-2 focus-visible:ring-[var(--border-color-focus)] focus:outline-none"
134
134
  )
135
135
  );
136
136
 
@@ -156,7 +156,7 @@
156
156
 
157
157
  $effect(() => {
158
158
  if (autoplay && hasItems && items.length > 1) {
159
- autoplayTimer = setInterval(next, interval);
159
+ autoplayTimer = setInterval(next, Math.max(1000, interval));
160
160
  }
161
161
  return () => {
162
162
  if (autoplayTimer) {
@@ -203,7 +203,7 @@
203
203
  {/snippet}
204
204
 
205
205
  <div
206
- class="transition-opacity duration-300 ease-in-out"
206
+ class="transition-opacity duration-[var(--transition-normal)] ease-in-out"
207
207
  class:opacity-100={i === current}
208
208
  class:opacity-0={i !== current}
209
209
  class:block={i === current}
@@ -275,7 +275,7 @@
275
275
  </div>
276
276
 
277
277
  {#if showDots && hasItems && items.length > 1}
278
- <div class="flex justify-center gap-2 p-4">
278
+ <div class="flex justify-center gap-[var(--spacing-sm)] p-[var(--spacing-md)]">
279
279
  {#each items as item, i (item.id ?? i)}
280
280
  <button
281
281
  type="button"
@@ -1,5 +1,5 @@
1
1
  import type { HTMLAttributes } from "svelte/elements";
2
- import type { SizeKey, CarouselItem } from "./types";
2
+ import { type SizeKey, type CarouselItem } from "./types";
3
3
  type Props = HTMLAttributes<HTMLDivElement> & {
4
4
  items?: CarouselItem[];
5
5
  sz?: SizeKey;
@@ -116,12 +116,12 @@
116
116
  ? state === "checked" || state === "mixed"
117
117
  ? "var(--border-color-strong)"
118
118
  : "var(--border-color-default)"
119
- : "white"
119
+ : "var(--color-text-inverse,#fff)"
120
120
  );
121
121
 
122
122
  const rootClass = $derived(
123
123
  cx(
124
- "inline-flex items-center cursor-pointer select-none",
124
+ "inline-flex items-center cursor-pointer select-none [@media(pointer:coarse)]:min-h-11",
125
125
  gapBySize[sz],
126
126
  externalClass
127
127
  )
@@ -139,6 +139,7 @@
139
139
  });
140
140
 
141
141
  async function copyToClipboard() {
142
+ if (typeof navigator === "undefined" || !navigator.clipboard) return;
142
143
  await navigator.clipboard.writeText(code);
143
144
  copied = true;
144
145
  setTimeout(() => (copied = false), 1200);
@@ -155,7 +156,7 @@
155
156
  {#if title}
156
157
  <div
157
158
  class={cx(
158
- "px-3 py-1 bg-[var(--color-bg-muted)] font-semibold uppercase flex items-center justify-between",
159
+ "px-[calc(var(--spacing-sm)+var(--spacing-xs))] py-[var(--spacing-xs)] bg-[var(--color-bg-muted)] font-semibold uppercase flex items-center justify-between",
159
160
  TEXT[sz]
160
161
  )}
161
162
  >
@@ -165,7 +166,7 @@
165
166
  <button
166
167
  onclick={copyToClipboard}
167
168
  class={cx(
168
- "px-3 py-0.5 text-xs rounded bg-[var(--color-primary)] text-white hover:opacity-[var(--opacity-hover)]",
169
+ "px-[calc(var(--spacing-sm)+var(--spacing-xs))] py-[calc(var(--spacing-xs)/2)] [font-size:var(--text-xs)] rounded-[var(--radius-sm)] bg-[var(--color-primary)] text-[var(--color-text-inverse,#fff)] hover:opacity-[var(--opacity-hover)]",
169
170
  "transition focus-visible:ring-2 focus-visible:ring-[var(--border-color-focus)] focus:outline-none"
170
171
  )}
171
172
  class:!bg-green-600={copied}
@@ -187,7 +188,7 @@
187
188
  <div
188
189
  bind:this={gutterEl}
189
190
  class={cx(
190
- "select-none px-3 py-[12px] border-r border-[var(--border-color-default)]",
191
+ "select-none px-[calc(var(--spacing-sm)+var(--spacing-xs))] py-[calc(var(--spacing-sm)+var(--spacing-xs))] border-r border-[var(--border-color-default)]",
191
192
  "text-[var(--color-text-muted)] text-right overflow-hidden",
192
193
  "cv-gutter bg-[var(--color-bg-surface)] tabular-nums h-full min-h-0"
193
194
  )}
@@ -232,8 +233,8 @@
232
233
  <style>
233
234
  .cv-layer {
234
235
  position: absolute;
235
- padding: 12px;
236
- white-space: pre;
236
+ padding: calc(var(--spacing-sm) + var(--spacing-xs));
237
+ white-space: var(--code-white-space, pre);
237
238
  box-sizing: border-box;
238
239
  font: inherit;
239
240
  line-height: inherit;
@@ -28,9 +28,9 @@
28
28
  * @note `clearable=false` hides the clear button; when `disabled`, pointer/keyboard handlers are skipped.
29
29
  */
30
30
  import type { HTMLAttributes } from "svelte/elements";
31
- import Button from "./Button.svelte";
32
- import { cx } from "../utils";
33
- import { getComponentText, getLangContext, getLangKey } from "./lang-context";
31
+ import Button from "./Button.svelte";
32
+ import { cx } from "../utils";
33
+ import { getComponentText, getLangContext, getLangKey } from "./lang-context";
34
34
 
35
35
  type Props = HTMLAttributes<HTMLDivElement> & {
36
36
  value?: string | null;
@@ -53,9 +53,9 @@
53
53
  ...rest
54
54
  }: Props = $props();
55
55
 
56
- const langCtx = getLangContext();
57
- const langKey = $derived(getLangKey(langCtx));
58
- const L = $derived(getComponentText("colorPicker", langKey));
56
+ const langCtx = getLangContext();
57
+ const langKey = $derived(getLangKey(langCtx));
58
+ const L = $derived(getComponentText("colorPicker", langKey));
59
59
 
60
60
  const labelFinal = $derived(label ?? L.text);
61
61
  const placeholderFinal = $derived(placeholder ?? L.placeholder);
@@ -0,0 +1,328 @@
1
+ <!-- src/lib/ContextMenu.svelte -->
2
+ <script lang="ts">
3
+ /**
4
+ * @component ContextMenu
5
+ * @description Right-click context menu for editor actions.
6
+ *
7
+ * @prop onUndo {() => void} - Fired when Undo is selected
8
+ * @prop onRedo {() => void} - Fired when Redo is selected
9
+ * @prop onCopy {() => void} - Fired when Copy is selected
10
+ * @prop onCut {() => void} - Fired when Cut is selected
11
+ * @prop onPaste {() => void} - Fired when Paste is selected
12
+ * @prop onDelete {() => void} - Fired when Delete is selected
13
+ *
14
+ * @note Call `openAt(event)` to show the menu at the pointer.
15
+ * @note Uses lang-context for localization.
16
+ */
17
+ import {
18
+ getComponentText,
19
+ getLangContext,
20
+ getLangKey,
21
+ } from "./lang-context";
22
+
23
+ interface Props {
24
+ onUndo?: () => void;
25
+ onRedo?: () => void;
26
+ onCopy?: () => void;
27
+ onCut?: () => void;
28
+ onPaste?: () => void;
29
+ onDelete?: () => void;
30
+ }
31
+
32
+ let {
33
+ onUndo = () => {},
34
+ onRedo = () => {},
35
+ onCopy = () => {},
36
+ onCut = () => {},
37
+ onPaste = () => {},
38
+ onDelete = () => {},
39
+ }: Props = $props();
40
+ const langCtx = getLangContext();
41
+ const langKey = $derived(getLangKey(langCtx));
42
+ const L = $derived(getComponentText("contextMenu", langKey));
43
+
44
+ let visible = $state(false);
45
+ let x = $state(0);
46
+ let y = $state(0);
47
+
48
+ function clampToViewport(): void {
49
+ requestAnimationFrame(() => {
50
+ const menu = document.getElementById("ctx-menu");
51
+ if (!menu) return;
52
+ const rect = menu.getBoundingClientRect();
53
+ const pad = 8;
54
+ let nextX = x;
55
+ let nextY = y;
56
+ if (rect.right > window.innerWidth - pad) {
57
+ nextX = Math.max(
58
+ pad,
59
+ x - (rect.right - (window.innerWidth - pad))
60
+ );
61
+ }
62
+ if (rect.bottom > window.innerHeight - pad) {
63
+ nextY = Math.max(
64
+ pad,
65
+ y - (rect.bottom - (window.innerHeight - pad))
66
+ );
67
+ }
68
+ if (nextX !== x) x = nextX;
69
+ if (nextY !== y) y = nextY;
70
+ });
71
+ }
72
+
73
+ export function openAt(event: MouseEvent): void {
74
+ event?.preventDefault?.();
75
+ const hasClient =
76
+ Number.isFinite(event?.clientX) && Number.isFinite(event?.clientY);
77
+ const hasPage =
78
+ Number.isFinite(event?.pageX) && Number.isFinite(event?.pageY);
79
+ if (hasClient && (event.clientX !== 0 || event.clientY !== 0)) {
80
+ x = event.clientX;
81
+ y = event.clientY;
82
+ } else if (hasPage) {
83
+ x = event.pageX - window.scrollX;
84
+ y = event.pageY - window.scrollY;
85
+ } else {
86
+ x = 0;
87
+ y = 0;
88
+ }
89
+ visible = true;
90
+ clampToViewport();
91
+ }
92
+
93
+ export function close(): void {
94
+ visible = false;
95
+ }
96
+
97
+ function handleDocClick(e: MouseEvent): void {
98
+ if (!visible) return;
99
+ const menu = document.getElementById("ctx-menu");
100
+ if (!menu) return;
101
+ if (!menu.contains(e.target as Node)) close();
102
+ }
103
+
104
+ function handleEsc(e: KeyboardEvent): void {
105
+ if (e.key === "Escape" && visible) close();
106
+ }
107
+
108
+ $effect(() => {
109
+ document.addEventListener("click", handleDocClick);
110
+ document.addEventListener("keydown", handleEsc);
111
+ return () => {
112
+ document.removeEventListener("click", handleDocClick);
113
+ document.removeEventListener("keydown", handleEsc);
114
+ };
115
+ });
116
+
117
+ const doUndo = () => (onUndo(), close());
118
+ const doRedo = () => (onRedo(), close());
119
+ const doCopy = () => (onCopy(), close());
120
+ const doCut = () => (onCut(), close());
121
+ const doPaste = () => (onPaste(), close());
122
+ const doDelete = () => (onDelete(), close());
123
+
124
+ const menuPanelClass =
125
+ "fixed bg-[var(--color-bg-surface)] border border-[var(--border-color-default)] rounded-[var(--radius-md)] min-w-[160px] max-w-[260px] py-[var(--spacing-xs)] z-[9999] box-border text-[var(--text-sm)] shadow-[0_2px_4px_var(--shadow-color)] font-[var(--font-sans)] text-[var(--color-text-default)] m-0 scale-90 origin-top-left";
126
+ const itemClass =
127
+ "w-full flex items-center justify-between bg-transparent border-0 text-[var(--color-text-default)] px-[calc(var(--spacing-sm)+var(--spacing-xs))] py-[var(--spacing-sm)] m-0 font-inherit cursor-pointer rounded-[var(--radius-sm)] whitespace-nowrap leading-[var(--line-height-normal)] gap-[calc(var(--spacing-sm)+var(--spacing-xs))] outline-none shadow-none relative hover:bg-[var(--color-bg-hover)] hover:text-[var(--color-text-default)] active:bg-[color-mix(in_srgb,var(--color-primary)_12%,var(--color-bg-hover)_88%)] active:text-[var(--color-text-default)] transition-colors duration-[var(--transition-fast)]";
128
+ const itemContentClass = "flex items-center gap-[calc(var(--spacing-sm)+var(--spacing-xs)/2)]";
129
+ </script>
130
+
131
+ {#if visible}
132
+ <div
133
+ id="ctx-menu"
134
+ class={menuPanelClass}
135
+ style="top: {y}px; left: {x}px;"
136
+ role="menu"
137
+ tabindex="-1"
138
+ >
139
+ <button
140
+ class={itemClass}
141
+ onclick={(e) => {
142
+ e.stopPropagation();
143
+ doUndo();
144
+ }}
145
+ title={L.hotkeys.undo}
146
+ >
147
+ <div class={itemContentClass}>
148
+ <svg
149
+ xmlns="http://www.w3.org/2000/svg"
150
+ width="24"
151
+ height="24"
152
+ viewBox="0 0 24 24"
153
+ fill="none"
154
+ stroke="currentColor"
155
+ stroke-width="2"
156
+ stroke-linecap="round"
157
+ stroke-linejoin="round"
158
+ class="w-4 h-4 opacity-80"
159
+ aria-hidden="true"
160
+ >
161
+ <path d="M3 7v6h6" />
162
+ <path d="M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6 2.3L3 13" />
163
+ </svg>
164
+ <span>{L.contextMenu.undo.replace(/↩️\s*/, "")}</span>
165
+ </div>
166
+ <span class="text-[var(--color-text-muted)] text-[0.7rem]">{L.hotkeys.undo}</span>
167
+ </button>
168
+
169
+ <button
170
+ class={itemClass}
171
+ onclick={(e) => {
172
+ e.stopPropagation();
173
+ doRedo();
174
+ }}
175
+ title={L.hotkeys.redo}
176
+ >
177
+ <div class={itemContentClass}>
178
+ <svg
179
+ xmlns="http://www.w3.org/2000/svg"
180
+ width="24"
181
+ height="24"
182
+ viewBox="0 0 24 24"
183
+ fill="none"
184
+ stroke="currentColor"
185
+ stroke-width="2"
186
+ stroke-linecap="round"
187
+ stroke-linejoin="round"
188
+ class="w-4 h-4 opacity-80"
189
+ aria-hidden="true"
190
+ >
191
+ <path d="M21 7v6h-6" />
192
+ <path d="M3 17a9 9 0 0 1 9-9 9 9 0 0 1 6 2.3l3 2.7" />
193
+ </svg>
194
+ <span>{L.contextMenu.redo.replace(/↪️\s*/, "")}</span>
195
+ </div>
196
+ <span class="text-[var(--color-text-muted)] text-[0.7rem]">{L.hotkeys.redo}</span>
197
+ </button>
198
+
199
+ <button
200
+ class={itemClass}
201
+ onclick={(e) => {
202
+ e.stopPropagation();
203
+ doCopy();
204
+ }}
205
+ title={L.hotkeys.copy}
206
+ >
207
+ <div class={itemContentClass}>
208
+ <svg
209
+ xmlns="http://www.w3.org/2000/svg"
210
+ width="24"
211
+ height="24"
212
+ viewBox="0 0 24 24"
213
+ fill="none"
214
+ stroke="currentColor"
215
+ stroke-width="2"
216
+ stroke-linecap="round"
217
+ stroke-linejoin="round"
218
+ class="w-4 h-4 opacity-80"
219
+ aria-hidden="true"
220
+ >
221
+ <rect width="14" height="14" x="8" y="8" rx="2" ry="2" />
222
+ <path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
223
+ </svg>
224
+ <span>{L.contextMenu.copy.replace(/📑\s*/, "")}</span>
225
+ </div>
226
+ <span class="text-[var(--color-text-muted)] text-[0.7rem]">{L.hotkeys.copy}</span>
227
+ </button>
228
+
229
+ <button
230
+ class={itemClass}
231
+ onclick={(e) => {
232
+ e.stopPropagation();
233
+ doCut();
234
+ }}
235
+ title={L.hotkeys.cut}
236
+ >
237
+ <div class={itemContentClass}>
238
+ <svg
239
+ xmlns="http://www.w3.org/2000/svg"
240
+ width="24"
241
+ height="24"
242
+ viewBox="0 0 24 24"
243
+ fill="none"
244
+ stroke="currentColor"
245
+ stroke-width="2"
246
+ stroke-linecap="round"
247
+ stroke-linejoin="round"
248
+ class="w-4 h-4 opacity-80"
249
+ aria-hidden="true"
250
+ >
251
+ <circle cx="6" cy="6" r="3" />
252
+ <path d="M8.12 8.12 12 12" />
253
+ <path d="M20 4 8.12 15.88" />
254
+ <circle cx="6" cy="18" r="3" />
255
+ <path d="M14.8 14.8 20 20" />
256
+ </svg>
257
+ <span>{L.contextMenu.cut.replace(/✂️\s*/, "")}</span>
258
+ </div>
259
+ <span class="text-[var(--color-text-muted)] text-[0.7rem]">{L.hotkeys.cut}</span>
260
+ </button>
261
+
262
+ <button
263
+ class={itemClass}
264
+ onclick={(e) => {
265
+ e.stopPropagation();
266
+ doPaste();
267
+ }}
268
+ title={L.hotkeys.paste}
269
+ >
270
+ <div class={itemContentClass}>
271
+ <svg
272
+ xmlns="http://www.w3.org/2000/svg"
273
+ width="24"
274
+ height="24"
275
+ viewBox="0 0 24 24"
276
+ fill="none"
277
+ stroke="currentColor"
278
+ stroke-width="2"
279
+ stroke-linecap="round"
280
+ stroke-linejoin="round"
281
+ class="w-4 h-4 opacity-80"
282
+ aria-hidden="true"
283
+ >
284
+ <path d="M11 14h10" />
285
+ <path d="M16 4h2a2 2 0 0 1 2 2v1.344" />
286
+ <path d="m17 18 4-4-4-4" />
287
+ <path d="M8 4H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 1.793-1.113" />
288
+ <rect x="8" y="2" width="8" height="4" rx="1" />
289
+ </svg>
290
+ <span>{L.contextMenu.paste.replace(/📋\s*/, "")}</span>
291
+ </div>
292
+ <span class="text-[var(--color-text-muted)] text-[0.7rem]">{L.hotkeys.paste}</span>
293
+ </button>
294
+
295
+ <button
296
+ class={itemClass}
297
+ onclick={(e) => {
298
+ e.stopPropagation();
299
+ doDelete();
300
+ }}
301
+ title={L.hotkeys.delete}
302
+ >
303
+ <div class={itemContentClass}>
304
+ <svg
305
+ xmlns="http://www.w3.org/2000/svg"
306
+ width="24"
307
+ height="24"
308
+ viewBox="0 0 24 24"
309
+ fill="none"
310
+ stroke="currentColor"
311
+ stroke-width="2"
312
+ stroke-linecap="round"
313
+ stroke-linejoin="round"
314
+ class="w-4 h-4 opacity-80"
315
+ aria-hidden="true"
316
+ >
317
+ <path d="M10 11v6" />
318
+ <path d="M14 11v6" />
319
+ <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" />
320
+ <path d="M3 6h18" />
321
+ <path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
322
+ </svg>
323
+ <span>{L.contextMenu.delete}</span>
324
+ </div>
325
+ <span class="text-[var(--color-text-muted)] text-[0.7rem]">{L.hotkeys.delete}</span>
326
+ </button>
327
+ </div>
328
+ {/if}
@@ -0,0 +1,14 @@
1
+ interface Props {
2
+ onUndo?: () => void;
3
+ onRedo?: () => void;
4
+ onCopy?: () => void;
5
+ onCut?: () => void;
6
+ onPaste?: () => void;
7
+ onDelete?: () => void;
8
+ }
9
+ declare const ContextMenu: import("svelte").Component<Props, {
10
+ openAt: (event: MouseEvent) => void;
11
+ close: () => void;
12
+ }, "">;
13
+ type ContextMenu = ReturnType<typeof ContextMenu>;
14
+ export default ContextMenu;