svelte-comp 1.3.3 → 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 (120) hide show
  1. package/README.md +12 -11
  2. package/dist/App.svelte +540 -540
  3. package/dist/app.css +2 -3
  4. package/dist/app.d.ts +10 -0
  5. package/dist/lib/Accordion.svelte +14 -14
  6. package/dist/lib/Button.svelte +23 -8
  7. package/dist/lib/Card.svelte +6 -6
  8. package/dist/lib/Carousel.svelte +16 -16
  9. package/dist/lib/Carousel.svelte.d.ts +1 -1
  10. package/dist/lib/CheckBox.svelte +2 -2
  11. package/dist/lib/CodeView.svelte +6 -5
  12. package/dist/lib/ContextMenu.svelte +19 -13
  13. package/dist/lib/Dialog.svelte +3 -3
  14. package/dist/lib/Field.svelte +1 -1
  15. package/dist/lib/FilePicker.svelte +66 -11
  16. package/dist/lib/FilePicker.svelte.d.ts +6 -1
  17. package/dist/lib/Hamburger.svelte +12 -12
  18. package/dist/lib/Menu.svelte +18 -18
  19. package/dist/lib/NoticeBase.svelte +5 -5
  20. package/dist/lib/Select.svelte +2 -2
  21. package/dist/lib/Slider.svelte +1 -1
  22. package/dist/lib/Splitter.svelte +15 -6
  23. package/dist/lib/Switch.svelte +5 -4
  24. package/dist/lib/Tabs.svelte +6 -6
  25. package/dist/lib/ThemeToggle.svelte +1 -0
  26. package/dist/lib/TimePickerNew.svelte +634 -0
  27. package/dist/lib/TimePickerNew.svelte.d.ts +49 -0
  28. package/dist/lib/Tooltip.svelte +7 -7
  29. package/dist/lib/Topbar.svelte +6 -6
  30. package/dist/lib/__tests__/Accordion.test.d.ts +1 -0
  31. package/dist/lib/__tests__/Accordion.test.js +171 -0
  32. package/dist/lib/__tests__/Badge.test.d.ts +1 -0
  33. package/dist/lib/__tests__/Badge.test.js +41 -0
  34. package/dist/lib/__tests__/Button.test.d.ts +1 -0
  35. package/dist/lib/__tests__/Button.test.js +269 -0
  36. package/dist/lib/__tests__/Calendar.test.d.ts +1 -0
  37. package/dist/lib/__tests__/Calendar.test.js +171 -0
  38. package/dist/lib/__tests__/Card.test.d.ts +1 -0
  39. package/dist/lib/__tests__/Card.test.js +148 -0
  40. package/dist/lib/__tests__/Carousel.test.d.ts +1 -0
  41. package/dist/lib/__tests__/Carousel.test.js +439 -0
  42. package/dist/lib/__tests__/CheckBox.test.d.ts +1 -0
  43. package/dist/lib/__tests__/CheckBox.test.js +152 -0
  44. package/dist/lib/__tests__/CodeView.test.d.ts +1 -0
  45. package/dist/lib/__tests__/CodeView.test.js +157 -0
  46. package/dist/lib/__tests__/ColorPicker.test.d.ts +1 -0
  47. package/dist/lib/__tests__/ColorPicker.test.js +93 -0
  48. package/dist/lib/__tests__/ContextMenu.test.d.ts +1 -0
  49. package/dist/lib/__tests__/ContextMenu.test.js +67 -0
  50. package/dist/lib/__tests__/DatePicker.test.d.ts +1 -0
  51. package/dist/lib/__tests__/DatePicker.test.js +108 -0
  52. package/dist/lib/__tests__/Dialog.test.d.ts +1 -0
  53. package/dist/lib/__tests__/Dialog.test.js +183 -0
  54. package/dist/lib/__tests__/Field.test.d.ts +1 -0
  55. package/dist/lib/__tests__/Field.test.js +190 -0
  56. package/dist/lib/__tests__/FilePicker.test.d.ts +1 -0
  57. package/dist/lib/__tests__/FilePicker.test.js +179 -0
  58. package/dist/lib/__tests__/Form.integration.test.d.ts +1 -0
  59. package/dist/lib/__tests__/Form.integration.test.js +158 -0
  60. package/dist/lib/__tests__/Form.test.d.ts +1 -0
  61. package/dist/lib/__tests__/Form.test.js +463 -0
  62. package/dist/lib/__tests__/Hamburger.test.d.ts +1 -0
  63. package/dist/lib/__tests__/Hamburger.test.js +161 -0
  64. package/dist/lib/__tests__/InstallPWA.test.d.ts +1 -0
  65. package/dist/lib/__tests__/InstallPWA.test.js +15 -0
  66. package/dist/lib/__tests__/Menu.test.d.ts +1 -0
  67. package/dist/lib/__tests__/Menu.test.js +285 -0
  68. package/dist/lib/__tests__/NoticeBase.test.d.ts +1 -0
  69. package/dist/lib/__tests__/NoticeBase.test.js +60 -0
  70. package/dist/lib/__tests__/PaginatedCard.test.d.ts +1 -0
  71. package/dist/lib/__tests__/PaginatedCard.test.js +89 -0
  72. package/dist/lib/__tests__/Pagination.test.d.ts +1 -0
  73. package/dist/lib/__tests__/Pagination.test.js +168 -0
  74. package/dist/lib/__tests__/PrimaryColorSelect.test.d.ts +1 -0
  75. package/dist/lib/__tests__/PrimaryColorSelect.test.js +92 -0
  76. package/dist/lib/__tests__/ProgressBar.test.d.ts +1 -0
  77. package/dist/lib/__tests__/ProgressBar.test.js +69 -0
  78. package/dist/lib/__tests__/ProgressCircle.test.d.ts +1 -0
  79. package/dist/lib/__tests__/ProgressCircle.test.js +71 -0
  80. package/dist/lib/__tests__/Radio.test.d.ts +1 -0
  81. package/dist/lib/__tests__/Radio.test.js +127 -0
  82. package/dist/lib/__tests__/SearchInput.test.d.ts +1 -0
  83. package/dist/lib/__tests__/SearchInput.test.js +80 -0
  84. package/dist/lib/__tests__/Select.test.d.ts +1 -0
  85. package/dist/lib/__tests__/Select.test.js +408 -0
  86. package/dist/lib/__tests__/Slider.test.d.ts +1 -0
  87. package/dist/lib/__tests__/Slider.test.js +213 -0
  88. package/dist/lib/__tests__/Splitter.test.d.ts +1 -0
  89. package/dist/lib/__tests__/Splitter.test.js +87 -0
  90. package/dist/lib/__tests__/Switch.test.d.ts +1 -0
  91. package/dist/lib/__tests__/Switch.test.js +97 -0
  92. package/dist/lib/__tests__/Table.test.d.ts +1 -0
  93. package/dist/lib/__tests__/Table.test.js +349 -0
  94. package/dist/lib/__tests__/Tabs.test.d.ts +1 -0
  95. package/dist/lib/__tests__/Tabs.test.js +262 -0
  96. package/dist/lib/__tests__/ThemeToggle.test.d.ts +1 -0
  97. package/dist/lib/__tests__/ThemeToggle.test.js +84 -0
  98. package/dist/lib/__tests__/TimePicker.test.d.ts +1 -0
  99. package/dist/lib/__tests__/TimePicker.test.js +146 -0
  100. package/dist/lib/__tests__/TimePickerNew.test.d.ts +1 -0
  101. package/dist/lib/__tests__/TimePickerNew.test.js +322 -0
  102. package/dist/lib/__tests__/Toast.test.d.ts +1 -0
  103. package/dist/lib/__tests__/Toast.test.js +135 -0
  104. package/dist/lib/__tests__/Tooltip.test.d.ts +1 -0
  105. package/dist/lib/__tests__/Tooltip.test.js +171 -0
  106. package/dist/lib/__tests__/Topbar.test.d.ts +1 -0
  107. package/dist/lib/__tests__/Topbar.test.js +25 -0
  108. package/dist/lib/__tests__/setupLangContext.d.ts +1 -0
  109. package/dist/lib/__tests__/setupLangContext.js +65 -0
  110. package/dist/lib/__tests__/storage.test.d.ts +1 -0
  111. package/dist/lib/__tests__/storage.test.js +124 -0
  112. package/dist/lib/__tests__/utils.test.d.ts +1 -0
  113. package/dist/lib/__tests__/utils.test.js +11 -0
  114. package/dist/lib/index.d.ts +1 -0
  115. package/dist/lib/index.js +1 -0
  116. package/dist/lib/lang.d.ts +4 -0
  117. package/dist/lib/lang.js +4 -0
  118. package/dist/styles.css +2 -0
  119. package/dist/utils/index.js +15 -4
  120. package/package.json +12 -12
package/dist/app.css CHANGED
@@ -1,9 +1,6 @@
1
1
  /* src/app.css */
2
-
3
2
  @import "tailwindcss";
4
3
 
5
- /* src/styles.css */
6
- @import "tailwindcss";
7
4
  @custom-variant dark (&:where(.dark, .dark *));
8
5
 
9
6
  @theme {
@@ -15,6 +12,7 @@
15
12
  --color-text-warning: oklch(95% 0.05 90deg);
16
13
  --color-text-link: oklch(35% 0.3 250deg);
17
14
  --color-text-red: oklch(50% 0.25 30deg);
15
+ --color-text-inverse: oklch(100% 0 0deg);
18
16
 
19
17
  /* COLORS — BG */
20
18
  --color-bg-page: oklch(98% 0 0deg);
@@ -108,6 +106,7 @@
108
106
  --color-text-warning: oklch(95% 0.05 90deg);
109
107
  --color-text-link: oklch(65% 0.3 250deg);
110
108
  --color-text-red: oklch(50% 0.25 30deg);
109
+ --color-text-inverse: oklch(100% 0 0deg);
111
110
 
112
111
  /* COLORS — BG */
113
112
  --color-bg-page: oklch(15% 0 0deg);
package/dist/app.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ // src/app.d.ts
2
+ declare module "*.css" {
3
+ const content: string;
4
+ export default content;
5
+ }
6
+
7
+ declare module "prismjs/themes/*.css" {
8
+ const content: string;
9
+ export default content;
10
+ }
@@ -51,22 +51,22 @@
51
51
  }: Props = $props();
52
52
 
53
53
  const base =
54
- "w-full border border-[var(--border-color-default)] bg-[var(--color-bg-surface)] shadow-sm";
54
+ "w-full border border-[var(--border-color-default)] bg-[var(--color-bg-surface)] shadow-[0_1px_2px_var(--shadow-color)]";
55
55
 
56
56
  const sizes: Record<SizeKey, string> = {
57
- xs: "rounded-[var(--radius-md)] text-sm",
58
- sm: "rounded-[var(--radius-md)] text-base",
59
- md: "rounded-[var(--radius-lg)] text-lg",
60
- lg: "rounded-[var(--radius-lg)] text-xl",
61
- xl: "rounded-[var(--radius-xl)] text-2xl",
57
+ xs: cx("rounded-[var(--radius-md)]", TEXT.xs),
58
+ sm: cx("rounded-[var(--radius-md)]", TEXT.sm),
59
+ md: cx("rounded-[var(--radius-lg)]", TEXT.md),
60
+ lg: cx("rounded-[var(--radius-lg)]", TEXT.lg),
61
+ xl: cx("rounded-[var(--radius-xl)]", TEXT.xl),
62
62
  };
63
63
 
64
64
  const contentSize: Record<SizeKey, string> = {
65
- xs: "px-4 pb-4 mt-1",
66
- sm: "px-5 pb-5 mt-2",
67
- md: "px-6 pb-6 mt-3",
68
- lg: "px-8 pb-8 mt-4",
69
- xl: "px-10 pb-10 mt-5",
65
+ xs: "px-[var(--spacing-md)] pb-[var(--spacing-md)] mt-[var(--spacing-xs)]",
66
+ sm: "px-[calc(var(--spacing-md)+var(--spacing-xs))] pb-[calc(var(--spacing-md)+var(--spacing-xs))] mt-[var(--spacing-sm)]",
67
+ md: "px-[calc(var(--spacing-md)+var(--spacing-sm))] pb-[calc(var(--spacing-md)+var(--spacing-sm))] mt-[calc(var(--spacing-sm)+var(--spacing-xs))]",
68
+ lg: "px-[var(--spacing-xl)] pb-[var(--spacing-xl)] mt-[var(--spacing-md)]",
69
+ xl: "px-[calc(var(--spacing-xl)+var(--spacing-sm))] pb-[calc(var(--spacing-xl)+var(--spacing-sm))] mt-[calc(var(--spacing-md)+var(--spacing-xs))]",
70
70
  };
71
71
 
72
72
  const iconSize: Record<SizeKey, string> = {
@@ -110,14 +110,14 @@
110
110
  <h3>
111
111
  <button
112
112
  type="button"
113
- class="flex w-full items-center justify-between gap-3 p-2 text-left font-medium text-[var(--color-text-default)] bg-transparent transition-colors hover:bg-[var(--color-bg-hover)] active:bg-[var(--color-bg-active)] focus:outline-none focus:ring-2 focus:ring-[var(--border-color-focus)] focus:ring-inset"
113
+ class="flex w-full items-center justify-between gap-[calc(var(--spacing-sm)+var(--spacing-xs))] p-[var(--spacing-sm)] text-left font-medium text-[var(--color-text-default)] bg-transparent transition-colors hover:bg-[var(--color-bg-hover)] active:bg-[var(--color-bg-active)] focus:outline-none focus:ring-2 focus:ring-[var(--border-color-focus)] focus:ring-inset"
114
114
  aria-expanded={isOpen(i)}
115
115
  onclick={() => toggle(i)}
116
116
  >
117
117
  <span>{item.title}</span>
118
118
  <svg
119
119
  class={cx(
120
- "shrink-0 transition-transform duration-200 text-[var(--color-text-muted)]",
120
+ "shrink-0 transition-transform duration-[var(--transition-fast)] text-[var(--color-text-muted)]",
121
121
  iconClass
122
122
  )}
123
123
  class:rotate-180={isOpen(i)}
@@ -135,7 +135,7 @@
135
135
  </h3>
136
136
 
137
137
  <div
138
- class="grid overflow-hidden transition-[grid-template-rows] duration-200"
138
+ class="grid overflow-hidden transition-[grid-template-rows] duration-[var(--transition-fast)]"
139
139
  class:grid-rows-[1fr]={isOpen(i)}
140
140
  class:grid-rows-[0fr]={!isOpen(i)}
141
141
  >
@@ -70,6 +70,7 @@
70
70
  relative inline-flex items-center justify-center gap-2 rounded-[var(--radius-md)] border font-medium
71
71
  transition-all duration-[var(--transition-fast)] ease-[var(--timing-default)] whitespace-nowrap select-none
72
72
  focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--border-color-focus)]
73
+ [@media(pointer:coarse)]:min-h-11 [@media(pointer:coarse)]:min-w-11
73
74
  disabled:opacity-[var(--opacity-disabled)]
74
75
  disabled:cursor-not-allowed
75
76
  disabled:brightness-100
@@ -87,20 +88,20 @@
87
88
 
88
89
  const variants: Record<ButtonVariant, string> = {
89
90
  primary:
90
- "bg-[var(--color-bg-primary)] text-white border-[var(--border-color-primary)] hover:brightness-110 active:scale-95",
91
+ "bg-[var(--color-bg-primary)] text-[var(--color-text-inverse,#fff)] border-[var(--border-color-primary)] hover:brightness-110 active:scale-95",
91
92
  secondary:
92
93
  "bg-[var(--color-bg-secondary)] [color:var(--color-text-default)] border-[var(--border-color-default)] hover:bg-[var(--color-bg-hover)] active:scale-95",
93
- pill: "bg-[var(--color-bg-primary)] text-white border-[var(--border-color-primary)] rounded-full hover:brightness-110 active:scale-95",
94
+ pill: "bg-[var(--color-bg-primary)] text-[var(--color-text-inverse,#fff)] border-[var(--border-color-primary)] rounded-full hover:brightness-110 active:scale-95",
94
95
  danger:
95
- "bg-[var(--color-bg-danger)] text-white border-[var(--color-bg-danger)] hover:brightness-110 active:scale-95",
96
+ "bg-[var(--color-bg-danger)] text-[var(--color-text-inverse,#fff)] border-[var(--color-bg-danger)] hover:brightness-110 active:scale-95",
96
97
  success:
97
- "bg-[var(--color-bg-success)] text-white border-[var(--color-bg-success)] hover:brightness-110 active:scale-95",
98
+ "bg-[var(--color-bg-success)] text-[var(--color-text-inverse,#fff)] border-[var(--color-bg-success)] hover:brightness-110 active:scale-95",
98
99
  warning:
99
- "bg-[var(--color-bg-warning)] text-white border-[var(--color-bg-warning)] hover:brightness-110 active:scale-95",
100
+ "bg-[var(--color-bg-warning)] text-[var(--color-text-inverse,#fff)] border-[var(--color-bg-warning)] hover:brightness-110 active:scale-95",
100
101
  ghost:
101
102
  "bg-transparent [color:var(--color-text-default)] border-transparent hover:bg-[var(--color-bg-hover)] active:bg-[var(--color-bg-active)] active:scale-95",
102
103
  link: "bg-transparent underline border-transparent [color:var(--color-text-link)] hover:brightness-110 active:scale-95 transition-transform ",
103
- info: "bg-[var(--color-bg-secondary)] text-white border-[var(--border-color-default)] hover:bg-[var(--color-bg-hover)] active:scale-95",
104
+ info: "bg-[var(--color-bg-secondary)] text-[var(--color-text-inverse,#fff)] border-[var(--border-color-default)] hover:bg-[var(--color-bg-hover)] active:scale-95",
104
105
  };
105
106
 
106
107
  const buttonClass = $derived(
@@ -132,15 +133,29 @@
132
133
 
133
134
  function navigateToLink() {
134
135
  if (!link || typeof window === "undefined") return;
136
+ const safeLink = getSafeLink(link);
137
+ if (!safeLink) return;
135
138
 
136
139
  const restAttrs = rest as Record<string, unknown>;
137
140
  const target =
138
141
  typeof restAttrs.target === "string" ? restAttrs.target : undefined;
139
142
 
140
143
  if (target === "_blank") {
141
- window.open(link, "_blank", "noopener,noreferrer");
144
+ window.open(safeLink, "_blank", "noopener,noreferrer");
142
145
  } else {
143
- window.location.assign(link);
146
+ window.location.assign(safeLink);
147
+ }
148
+ }
149
+
150
+ function getSafeLink(value: string) {
151
+ try {
152
+ const url = new URL(value, window.location.href);
153
+ if (!["http:", "https:", "mailto:", "tel:"].includes(url.protocol)) {
154
+ return null;
155
+ }
156
+ return value;
157
+ } catch {
158
+ return null;
144
159
  }
145
160
  }
146
161
  </script>
@@ -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;
@@ -120,25 +120,31 @@
120
120
  const doCut = () => (onCut(), close());
121
121
  const doPaste = () => (onPaste(), close());
122
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)]";
123
129
  </script>
124
130
 
125
131
  {#if visible}
126
132
  <div
127
133
  id="ctx-menu"
128
- class="fixed bg-[var(--color-bg-surface)] border border-[var(--border-color-default)] rounded-[var(--radius-md)] min-w-[160px] max-w-[260px] py-1 z-[9999] box-border text-[var(--text-sm)] shadow-md font-[var(--font-sans)] text-[var(--color-text-default)] m-0 scale-90 origin-top-left"
134
+ class={menuPanelClass}
129
135
  style="top: {y}px; left: {x}px;"
130
136
  role="menu"
131
137
  tabindex="-1"
132
138
  >
133
139
  <button
134
- class="w-full flex items-center justify-between bg-transparent border-0 text-[var(--color-text-default)] px-3 py-2 m-0 font-inherit cursor-pointer rounded-[var(--radius-sm)] whitespace-nowrap leading-[1.3] gap-3 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-150"
140
+ class={itemClass}
135
141
  onclick={(e) => {
136
142
  e.stopPropagation();
137
143
  doUndo();
138
144
  }}
139
145
  title={L.hotkeys.undo}
140
146
  >
141
- <div class="flex items-center gap-2.5">
147
+ <div class={itemContentClass}>
142
148
  <svg
143
149
  xmlns="http://www.w3.org/2000/svg"
144
150
  width="24"
@@ -161,14 +167,14 @@
161
167
  </button>
162
168
 
163
169
  <button
164
- class="w-full flex items-center justify-between bg-transparent border-0 text-[var(--color-text-default)] px-3 py-2 m-0 font-inherit cursor-pointer rounded-[var(--radius-sm)] whitespace-nowrap leading-[1.3] gap-3 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-150"
170
+ class={itemClass}
165
171
  onclick={(e) => {
166
172
  e.stopPropagation();
167
173
  doRedo();
168
174
  }}
169
175
  title={L.hotkeys.redo}
170
176
  >
171
- <div class="flex items-center gap-2.5">
177
+ <div class={itemContentClass}>
172
178
  <svg
173
179
  xmlns="http://www.w3.org/2000/svg"
174
180
  width="24"
@@ -191,14 +197,14 @@
191
197
  </button>
192
198
 
193
199
  <button
194
- class="w-full flex items-center justify-between bg-transparent border-0 text-[var(--color-text-default)] px-3 py-2 m-0 font-inherit cursor-pointer rounded-[var(--radius-sm)] whitespace-nowrap leading-[1.3] gap-3 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-150"
200
+ class={itemClass}
195
201
  onclick={(e) => {
196
202
  e.stopPropagation();
197
203
  doCopy();
198
204
  }}
199
205
  title={L.hotkeys.copy}
200
206
  >
201
- <div class="flex items-center gap-2.5">
207
+ <div class={itemContentClass}>
202
208
  <svg
203
209
  xmlns="http://www.w3.org/2000/svg"
204
210
  width="24"
@@ -221,14 +227,14 @@
221
227
  </button>
222
228
 
223
229
  <button
224
- class="w-full flex items-center justify-between bg-transparent border-0 text-[var(--color-text-default)] px-3 py-2 m-0 font-inherit cursor-pointer rounded-[var(--radius-sm)] whitespace-nowrap leading-[1.3] gap-3 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-150"
230
+ class={itemClass}
225
231
  onclick={(e) => {
226
232
  e.stopPropagation();
227
233
  doCut();
228
234
  }}
229
235
  title={L.hotkeys.cut}
230
236
  >
231
- <div class="flex items-center gap-2.5">
237
+ <div class={itemContentClass}>
232
238
  <svg
233
239
  xmlns="http://www.w3.org/2000/svg"
234
240
  width="24"
@@ -254,14 +260,14 @@
254
260
  </button>
255
261
 
256
262
  <button
257
- class="w-full flex items-center justify-between bg-transparent border-0 text-[var(--color-text-default)] px-3 py-2 m-0 font-inherit cursor-pointer rounded-[var(--radius-sm)] whitespace-nowrap leading-[1.3] gap-3 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-150"
263
+ class={itemClass}
258
264
  onclick={(e) => {
259
265
  e.stopPropagation();
260
266
  doPaste();
261
267
  }}
262
268
  title={L.hotkeys.paste}
263
269
  >
264
- <div class="flex items-center gap-2.5">
270
+ <div class={itemContentClass}>
265
271
  <svg
266
272
  xmlns="http://www.w3.org/2000/svg"
267
273
  width="24"
@@ -287,14 +293,14 @@
287
293
  </button>
288
294
 
289
295
  <button
290
- class="w-full flex items-center justify-between bg-transparent border-0 text-[var(--color-text-default)] px-3 py-2 m-0 font-inherit cursor-pointer rounded-[var(--radius-sm)] whitespace-nowrap leading-[1.3] gap-3 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-150"
296
+ class={itemClass}
291
297
  onclick={(e) => {
292
298
  e.stopPropagation();
293
299
  doDelete();
294
300
  }}
295
301
  title={L.hotkeys.delete}
296
302
  >
297
- <div class="flex items-center gap-2.5">
303
+ <div class={itemContentClass}>
298
304
  <svg
299
305
  xmlns="http://www.w3.org/2000/svg"
300
306
  width="24"
@@ -98,7 +98,7 @@
98
98
  }
99
99
 
100
100
  const panelBase =
101
- "fusion-dialog bg-[var(--color-bg-surface)] rounded-[var(--radius-lg)] shadow-lg min-w-80 max-w-md w-full border border-[var(--border-color-default)]";
101
+ "fusion-dialog bg-[var(--color-bg-surface)] rounded-[var(--radius-lg)] shadow-[0_8px_24px_var(--shadow-color)] min-w-0 max-w-[min(100%,28rem)] max-h-[calc(100svh-var(--spacing-lg)*2)] overflow-auto w-full border border-[var(--border-color-default)]";
102
102
 
103
103
  const paddingBySize: Record<SizeKey, string> = {
104
104
  xs: "p-[var(--spacing-sm)]",
@@ -164,7 +164,7 @@
164
164
  {#if open}
165
165
  {#if modal}
166
166
  <div
167
- class="fixed inset-0 z-[var(--z-modal)] bg-oklch(0_0_0/var(--opacity-overlay)) flex items-center justify-center p-4"
167
+ class="fixed inset-0 z-[var(--z-modal)] bg-[oklch(0_0_0/var(--opacity-overlay))] flex items-center justify-center p-[var(--spacing-md)]"
168
168
  role="presentation"
169
169
  tabindex="-1"
170
170
  onkeydown={handleKeydown}
@@ -195,7 +195,7 @@
195
195
  </div>
196
196
  {:else}
197
197
  <div
198
- class="fixed top-4 right-4 z-[var(--z-modal)]"
198
+ class="fixed top-[var(--spacing-md)] right-[var(--spacing-md)] z-[var(--z-modal)] max-w-[calc(100vw-var(--spacing-md)*2)]"
199
199
  role="dialog"
200
200
  aria-modal="false"
201
201
  aria-label={title}
@@ -86,7 +86,7 @@
86
86
  }: Props = $props();
87
87
 
88
88
  const base =
89
- "w-full outline-none transition-colors duration-[var(--transition-fast)] ease-[var(--timing-default)] box-border rounded-[var(--radius-md)] border focus:border-[var(--border-color-focus)] focus:ring-2 focus:ring-[var(--border-color-focus)] disabled:opacity-[var(--opacity-disabled)] disabled:cursor-not-allowed";
89
+ "w-full outline-none transition-colors duration-[var(--transition-fast)] ease-[var(--timing-default)] box-border rounded-[var(--radius-md)] border focus:border-[var(--border-color-focus)] focus:ring-2 focus:ring-[var(--border-color-focus)] disabled:opacity-[var(--opacity-disabled)] disabled:cursor-not-allowed [@media(pointer:coarse)]:min-h-11";
90
90
 
91
91
  const sizes: Record<SizeKey, string> = {
92
92
  xs: "px-2 h-6",
@@ -18,6 +18,10 @@
18
18
  * @prop clearable {boolean} - Shows a clear button to reset selection
19
19
  * @default true
20
20
  *
21
+ * @prop maxBytes {number} - Maximum allowed file size in bytes
22
+ *
23
+ * @prop onError {(error: string) => void} - Fired when selected files are rejected
24
+ *
21
25
  * @prop placeholder {string} - Placeholder text for the drop zone
22
26
  *
23
27
  * @prop value {FileList | null} - Controlled selected files (bindable)
@@ -30,7 +34,7 @@
30
34
  *
31
35
  * @note The entire area is clickable and supports drag-and-drop.
32
36
  * @note After a selection, the underlying input resets its value, so choosing the same file twice still triggers updates.
33
- * @note `accept` does not apply to dropped files, only to the picker UI; validate files inside `onFilesSelected`.
37
+ * @note `accept` and `maxBytes` are enforced for both input and dropped files.
34
38
  * @note When `clearable=true`, the user can clear selected files and the callback receives `null`.
35
39
  * @note When `disabled=true`, clicks, drag events, focus, and keyboard input are blocked.
36
40
  */
@@ -47,6 +51,7 @@
47
51
  clearable?: boolean;
48
52
  placeholder?: string;
49
53
  value?: FileList | null;
54
+ maxBytes?: number;
50
55
  onFilesSelected?: (files: FileList | null) => void;
51
56
  onError?: (error: string) => void;
52
57
  class?: string;
@@ -60,7 +65,9 @@
60
65
  clearable = true,
61
66
  placeholder,
62
67
  value = $bindable<FileList | null>(null),
68
+ maxBytes = Number.POSITIVE_INFINITY,
63
69
  onFilesSelected,
70
+ onError,
64
71
  class: externalClass = "",
65
72
  ...rest
66
73
  }: Props = $props();
@@ -97,11 +104,7 @@
97
104
 
98
105
  function handleFileChange(event: Event) {
99
106
  const target = event.target as HTMLInputElement;
100
- const files = target.files;
101
- value = files;
102
- if (files && files.length > 0) {
103
- onFilesSelected?.(files);
104
- }
107
+ selectFiles(target.files);
105
108
  if (inputEl) {
106
109
  inputEl.value = "";
107
110
  }
@@ -111,11 +114,7 @@
111
114
  event.preventDefault();
112
115
  isDragOver = false;
113
116
  if (disabled) return;
114
- const files = event.dataTransfer?.files;
115
- value = files || null;
116
- if (files && files.length > 0) {
117
- onFilesSelected?.(files);
118
- }
117
+ selectFiles(event.dataTransfer?.files ?? null);
119
118
  if (inputEl) {
120
119
  inputEl.value = "";
121
120
  }
@@ -153,6 +152,62 @@
153
152
  }
154
153
  onFilesSelected?.(null);
155
154
  }
155
+
156
+ function selectFiles(files: FileList | null) {
157
+ const acceptedFiles = filterFiles(files);
158
+ value = acceptedFiles;
159
+ if (acceptedFiles && acceptedFiles.length > 0) {
160
+ onFilesSelected?.(acceptedFiles);
161
+ }
162
+ }
163
+
164
+ function filterFiles(files: FileList | null) {
165
+ if (!files || files.length === 0) return null;
166
+
167
+ const selected = Array.from(files);
168
+ const accepted = selected.filter(isAllowedFile);
169
+
170
+ if (accepted.length !== selected.length) {
171
+ onError?.("Some files were rejected by type or size constraints.");
172
+ }
173
+
174
+ if (accepted.length === 0) return null;
175
+ if (accepted.length === selected.length) return files;
176
+
177
+ return toFileList(accepted);
178
+ }
179
+
180
+ function isAllowedFile(file: File) {
181
+ if (Number.isFinite(maxBytes) && file.size > maxBytes) return false;
182
+ return matchesAccept(file, accept);
183
+ }
184
+
185
+ function matchesAccept(file: File, acceptValue: string) {
186
+ const rules = acceptValue
187
+ .split(",")
188
+ .map((rule) => rule.trim().toLowerCase())
189
+ .filter(Boolean);
190
+
191
+ if (rules.length === 0 || rules.includes("*/*")) return true;
192
+
193
+ const fileName = file.name.toLowerCase();
194
+ const fileType = file.type.toLowerCase();
195
+
196
+ return rules.some((rule) => {
197
+ if (rule.startsWith(".")) return fileName.endsWith(rule);
198
+ if (rule.endsWith("/*")) return fileType.startsWith(rule.slice(0, -1));
199
+ return fileType === rule;
200
+ });
201
+ }
202
+
203
+ function toFileList(files: File[]) {
204
+ if (typeof DataTransfer === "undefined") return null;
205
+ const transfer = new DataTransfer();
206
+ for (const file of files) {
207
+ transfer.items.add(file);
208
+ }
209
+ return transfer.files;
210
+ }
156
211
  </script>
157
212
 
158
213
  <div class={pickerClass} {...rest}>
@@ -16,6 +16,10 @@
16
16
  * @prop clearable {boolean} - Shows a clear button to reset selection
17
17
  * @default true
18
18
  *
19
+ * @prop maxBytes {number} - Maximum allowed file size in bytes
20
+ *
21
+ * @prop onError {(error: string) => void} - Fired when selected files are rejected
22
+ *
19
23
  * @prop placeholder {string} - Placeholder text for the drop zone
20
24
  *
21
25
  * @prop value {FileList | null} - Controlled selected files (bindable)
@@ -28,7 +32,7 @@
28
32
  *
29
33
  * @note The entire area is clickable and supports drag-and-drop.
30
34
  * @note After a selection, the underlying input resets its value, so choosing the same file twice still triggers updates.
31
- * @note `accept` does not apply to dropped files, only to the picker UI; validate files inside `onFilesSelected`.
35
+ * @note `accept` and `maxBytes` are enforced for both input and dropped files.
32
36
  * @note When `clearable=true`, the user can clear selected files and the callback receives `null`.
33
37
  * @note When `disabled=true`, clicks, drag events, focus, and keyboard input are blocked.
34
38
  */
@@ -41,6 +45,7 @@ type Props = HTMLAttributes<HTMLDivElement> & {
41
45
  clearable?: boolean;
42
46
  placeholder?: string;
43
47
  value?: FileList | null;
48
+ maxBytes?: number;
44
49
  onFilesSelected?: (files: FileList | null) => void;
45
50
  onError?: (error: string) => void;
46
51
  class?: string;