@miozu/jera 0.0.2 → 0.4.2

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 (82) hide show
  1. package/CLAUDE.md +734 -0
  2. package/README.md +219 -1
  3. package/llms.txt +97 -0
  4. package/package.json +54 -14
  5. package/src/actions/index.js +375 -0
  6. package/src/components/docs/CodeBlock.svelte +203 -0
  7. package/src/components/docs/DocSection.svelte +120 -0
  8. package/src/components/docs/PropsTable.svelte +136 -0
  9. package/src/components/docs/SplitPane.svelte +98 -0
  10. package/src/components/docs/index.js +14 -0
  11. package/src/components/feedback/Alert.svelte +234 -0
  12. package/src/components/feedback/EmptyState.svelte +179 -0
  13. package/src/components/feedback/ProgressBar.svelte +116 -0
  14. package/src/components/feedback/Skeleton.svelte +107 -0
  15. package/src/components/feedback/Spinner.svelte +77 -0
  16. package/src/components/feedback/Toast.svelte +261 -0
  17. package/src/components/forms/Checkbox.svelte +147 -0
  18. package/src/components/forms/Dropzone.svelte +248 -0
  19. package/src/components/forms/FileUpload.svelte +266 -0
  20. package/src/components/forms/IconInput.svelte +184 -0
  21. package/src/components/forms/Input.svelte +121 -0
  22. package/src/components/forms/NumberInput.svelte +225 -0
  23. package/src/components/forms/PinInput.svelte +169 -0
  24. package/src/components/forms/Radio.svelte +143 -0
  25. package/src/components/forms/RadioGroup.svelte +62 -0
  26. package/src/components/forms/RangeSlider.svelte +212 -0
  27. package/src/components/forms/SearchInput.svelte +175 -0
  28. package/src/components/forms/Select.svelte +324 -0
  29. package/src/components/forms/Switch.svelte +159 -0
  30. package/src/components/forms/Textarea.svelte +122 -0
  31. package/src/components/navigation/Accordion.svelte +65 -0
  32. package/src/components/navigation/AccordionItem.svelte +146 -0
  33. package/src/components/navigation/NavigationContainer.svelte +344 -0
  34. package/src/components/navigation/Sidebar.svelte +334 -0
  35. package/src/components/navigation/SidebarAccountGroup.svelte +495 -0
  36. package/src/components/navigation/SidebarAccountItem.svelte +492 -0
  37. package/src/components/navigation/SidebarGroup.svelte +230 -0
  38. package/src/components/navigation/SidebarGroupSwitcher.svelte +262 -0
  39. package/src/components/navigation/SidebarItem.svelte +210 -0
  40. package/src/components/navigation/SidebarNavigationItem.svelte +470 -0
  41. package/src/components/navigation/SidebarPopover.svelte +145 -0
  42. package/src/components/navigation/SidebarSearch.svelte +236 -0
  43. package/src/components/navigation/SidebarSection.svelte +158 -0
  44. package/src/components/navigation/SidebarToggle.svelte +86 -0
  45. package/src/components/navigation/Tabs.svelte +239 -0
  46. package/src/components/navigation/WorkspaceMenu.svelte +416 -0
  47. package/src/components/navigation/blocks/NavigationAccountGroup.svelte +396 -0
  48. package/src/components/navigation/blocks/NavigationCustomBlock.svelte +74 -0
  49. package/src/components/navigation/blocks/NavigationGroupSwitcher.svelte +277 -0
  50. package/src/components/navigation/blocks/NavigationSearch.svelte +300 -0
  51. package/src/components/navigation/blocks/NavigationSection.svelte +230 -0
  52. package/src/components/navigation/index.js +22 -0
  53. package/src/components/overlays/ConfirmDialog.svelte +272 -0
  54. package/src/components/overlays/Dropdown.svelte +153 -0
  55. package/src/components/overlays/DropdownDivider.svelte +23 -0
  56. package/src/components/overlays/DropdownItem.svelte +97 -0
  57. package/src/components/overlays/Modal.svelte +232 -0
  58. package/src/components/overlays/Popover.svelte +206 -0
  59. package/src/components/primitives/Avatar.svelte +132 -0
  60. package/src/components/primitives/Badge.svelte +118 -0
  61. package/src/components/primitives/Button.svelte +214 -0
  62. package/src/components/primitives/Card.svelte +104 -0
  63. package/src/components/primitives/Divider.svelte +105 -0
  64. package/src/components/primitives/LazyImage.svelte +104 -0
  65. package/src/components/primitives/Link.svelte +122 -0
  66. package/src/components/primitives/Stat.svelte +197 -0
  67. package/src/components/primitives/StatusBadge.svelte +122 -0
  68. package/src/index.js +183 -0
  69. package/src/tokens/colors.css +157 -0
  70. package/src/tokens/effects.css +128 -0
  71. package/src/tokens/index.css +81 -0
  72. package/src/tokens/spacing.css +49 -0
  73. package/src/tokens/typography.css +79 -0
  74. package/src/utils/cn.svelte.js +175 -0
  75. package/src/utils/highlighter.js +124 -0
  76. package/src/utils/index.js +22 -0
  77. package/src/utils/navigation.svelte.js +423 -0
  78. package/src/utils/reactive.svelte.js +328 -0
  79. package/src/utils/sidebar.svelte.js +211 -0
  80. package/jera.js +0 -135
  81. package/www/components/jera/Input/Input.svelte +0 -63
  82. package/www/components/jera/Input/index.js +0 -1
@@ -0,0 +1,116 @@
1
+ <!--
2
+ @component ProgressBar
3
+
4
+ Visual progress indicator with optional label.
5
+
6
+ @example Basic usage
7
+ <ProgressBar value={65} />
8
+
9
+ @example With label
10
+ <ProgressBar value={65} showLabel />
11
+
12
+ @example Custom color
13
+ <ProgressBar value={80} variant="success" />
14
+ -->
15
+ <script>
16
+ let {
17
+ value = 0,
18
+ max = 100,
19
+ size = 'md',
20
+ variant = 'primary',
21
+ showLabel = false,
22
+ label = null,
23
+ indeterminate = false,
24
+ class: className = ''
25
+ } = $props();
26
+
27
+ const percentage = $derived(Math.min(100, Math.max(0, (value / max) * 100)));
28
+
29
+ const variantColors = {
30
+ primary: 'var(--color-base0D)',
31
+ success: 'var(--color-base0B)',
32
+ warning: 'var(--color-base0A)',
33
+ error: 'var(--color-base08)',
34
+ info: 'var(--color-base0D)'
35
+ };
36
+
37
+ const barColor = $derived(variantColors[variant] || variantColors.primary);
38
+ </script>
39
+
40
+ <div class="progress-container {className}">
41
+ {#if showLabel || label}
42
+ <div class="progress-header">
43
+ {#if label}
44
+ <span class="progress-label">{label}</span>
45
+ {/if}
46
+ {#if showLabel && !indeterminate}
47
+ <span class="progress-value">{Math.round(percentage)}%</span>
48
+ {/if}
49
+ </div>
50
+ {/if}
51
+
52
+ <div
53
+ class="progress progress-{size}"
54
+ role="progressbar"
55
+ aria-valuenow={indeterminate ? undefined : value}
56
+ aria-valuemin="0"
57
+ aria-valuemax={max}
58
+ >
59
+ <div
60
+ class="progress-bar"
61
+ class:progress-indeterminate={indeterminate}
62
+ style="width: {indeterminate ? '100%' : percentage + '%'}; background: {barColor};"
63
+ ></div>
64
+ </div>
65
+ </div>
66
+
67
+ <style>
68
+ .progress-container {
69
+ width: 100%;
70
+ }
71
+
72
+ .progress-header {
73
+ display: flex;
74
+ justify-content: space-between;
75
+ align-items: center;
76
+ margin-bottom: 0.375rem;
77
+ }
78
+
79
+ .progress-label {
80
+ font-size: 0.875rem;
81
+ color: var(--color-base05);
82
+ }
83
+
84
+ .progress-value {
85
+ font-size: 0.875rem;
86
+ font-weight: 500;
87
+ color: var(--color-base07);
88
+ }
89
+
90
+ .progress {
91
+ width: 100%;
92
+ background: var(--color-base02);
93
+ border-radius: 9999px;
94
+ overflow: hidden;
95
+ }
96
+
97
+ .progress-sm { height: 0.25rem; }
98
+ .progress-md { height: 0.5rem; }
99
+ .progress-lg { height: 0.75rem; }
100
+
101
+ .progress-bar {
102
+ height: 100%;
103
+ border-radius: 9999px;
104
+ transition: width 0.3s ease-out;
105
+ }
106
+
107
+ .progress-indeterminate {
108
+ width: 30% !important;
109
+ animation: indeterminate 1.5s ease-in-out infinite;
110
+ }
111
+
112
+ @keyframes indeterminate {
113
+ 0% { transform: translateX(-100%); }
114
+ 100% { transform: translateX(400%); }
115
+ }
116
+ </style>
@@ -0,0 +1,107 @@
1
+ <!--
2
+ @component Skeleton
3
+
4
+ Loading placeholder with shimmer animation.
5
+
6
+ @example Text line
7
+ <Skeleton width="80%" />
8
+
9
+ @example Circle avatar
10
+ <Skeleton variant="circle" size="48px" />
11
+
12
+ @example Rectangle card
13
+ <Skeleton variant="rect" width="100%" height="200px" />
14
+ -->
15
+ <script>
16
+ let {
17
+ variant = 'text',
18
+ width = '100%',
19
+ height = null,
20
+ size = null,
21
+ lines = 1,
22
+ animate = true,
23
+ class: className = ''
24
+ } = $props();
25
+
26
+ // Compute dimensions based on variant
27
+ const computedHeight = $derived.by(() => {
28
+ if (height) return height;
29
+ if (size) return size;
30
+ switch (variant) {
31
+ case 'text': return '1rem';
32
+ case 'heading': return '1.5rem';
33
+ case 'circle': return size || '2.5rem';
34
+ case 'rect': return '4rem';
35
+ default: return '1rem';
36
+ }
37
+ });
38
+
39
+ const computedWidth = $derived.by(() => {
40
+ if (width) return width;
41
+ if (size && variant === 'circle') return size;
42
+ return '100%';
43
+ });
44
+ </script>
45
+
46
+ {#if lines > 1}
47
+ <div class="skeleton-lines {className}">
48
+ {#each Array(lines) as _, i}
49
+ <div
50
+ class="skeleton skeleton-{variant}"
51
+ class:skeleton-animate={animate}
52
+ style="width: {i === lines - 1 ? '60%' : computedWidth}; height: {computedHeight};"
53
+ ></div>
54
+ {/each}
55
+ </div>
56
+ {:else}
57
+ <div
58
+ class="skeleton skeleton-{variant} {className}"
59
+ class:skeleton-animate={animate}
60
+ style="width: {computedWidth}; height: {computedHeight};"
61
+ ></div>
62
+ {/if}
63
+
64
+ <style>
65
+ .skeleton {
66
+ background: var(--color-base02);
67
+ border-radius: 0.25rem;
68
+ }
69
+
70
+ .skeleton-text {
71
+ border-radius: 0.25rem;
72
+ }
73
+
74
+ .skeleton-heading {
75
+ border-radius: 0.25rem;
76
+ }
77
+
78
+ .skeleton-circle {
79
+ border-radius: 50%;
80
+ }
81
+
82
+ .skeleton-rect {
83
+ border-radius: 0.5rem;
84
+ }
85
+
86
+ .skeleton-lines {
87
+ display: flex;
88
+ flex-direction: column;
89
+ gap: 0.5rem;
90
+ }
91
+
92
+ .skeleton-animate {
93
+ background: linear-gradient(
94
+ 90deg,
95
+ var(--color-base02) 0%,
96
+ var(--color-base03) 50%,
97
+ var(--color-base02) 100%
98
+ );
99
+ background-size: 200% 100%;
100
+ animation: skeleton-shimmer 1.5s ease-in-out infinite;
101
+ }
102
+
103
+ @keyframes skeleton-shimmer {
104
+ 0% { background-position: 200% 0; }
105
+ 100% { background-position: -200% 0; }
106
+ }
107
+ </style>
@@ -0,0 +1,77 @@
1
+ <!--
2
+ @component Spinner
3
+
4
+ Loading spinner indicator.
5
+
6
+ @example Basic usage
7
+ <Spinner />
8
+
9
+ @example With size
10
+ <Spinner size="lg" />
11
+
12
+ @example Custom color
13
+ <Spinner color="var(--color-base0B)" />
14
+ -->
15
+ <script>
16
+ let {
17
+ size = 'md',
18
+ color = 'currentColor',
19
+ label = 'Loading...',
20
+ class: className = ''
21
+ } = $props();
22
+
23
+ const sizes = {
24
+ xs: 12,
25
+ sm: 16,
26
+ md: 24,
27
+ lg: 32,
28
+ xl: 48
29
+ };
30
+
31
+ const sizeValue = $derived(sizes[size] || sizes.md);
32
+ </script>
33
+
34
+ <svg
35
+ class="spinner {className}"
36
+ width={sizeValue}
37
+ height={sizeValue}
38
+ viewBox="0 0 24 24"
39
+ fill="none"
40
+ aria-label={label}
41
+ role="status"
42
+ >
43
+ <circle
44
+ class="spinner-track"
45
+ cx="12"
46
+ cy="12"
47
+ r="10"
48
+ stroke="currentColor"
49
+ stroke-width="3"
50
+ opacity="0.2"
51
+ />
52
+ <circle
53
+ class="spinner-indicator"
54
+ cx="12"
55
+ cy="12"
56
+ r="10"
57
+ stroke={color}
58
+ stroke-width="3"
59
+ stroke-linecap="round"
60
+ stroke-dasharray="60 200"
61
+ />
62
+ </svg>
63
+
64
+ <style>
65
+ .spinner {
66
+ animation: spin 1s linear infinite;
67
+ }
68
+
69
+ .spinner-track {
70
+ opacity: 0.2;
71
+ }
72
+
73
+ @keyframes spin {
74
+ from { transform: rotate(0deg); }
75
+ to { transform: rotate(360deg); }
76
+ }
77
+ </style>
@@ -0,0 +1,261 @@
1
+ <!--
2
+ @component Toast
3
+
4
+ A toast notification system with stacking, auto-dismiss, and animations.
5
+
6
+ @example
7
+ // In your root layout
8
+ <script>
9
+ import { Toast, createToastContext } from '@miozu/jera';
10
+ const toast = createToastContext();
11
+ </script>
12
+ <Toast />
13
+
14
+ // In any component
15
+ import { getToastContext } from '@miozu/jera';
16
+ const toast = getToastContext();
17
+ toast.success('Saved successfully!');
18
+ -->
19
+ <script module>
20
+ import { getContext, setContext } from 'svelte';
21
+
22
+ const TOAST_KEY = Symbol('jera-toast');
23
+
24
+ /**
25
+ * Toast Controller Class
26
+ */
27
+ export class ToastController {
28
+ toasts = $state.raw([]);
29
+ position = $state('bottom-right');
30
+ defaultDuration = 4000;
31
+ maxToasts = 5;
32
+ #counter = 0;
33
+
34
+ show(options) {
35
+ const id = `toast-${++this.#counter}`;
36
+ const toast = {
37
+ id,
38
+ type: options.type ?? 'info',
39
+ title: options.title,
40
+ message: options.message,
41
+ duration: options.duration ?? this.defaultDuration,
42
+ createdAt: Date.now()
43
+ };
44
+ this.toasts = [toast, ...this.toasts].slice(0, this.maxToasts);
45
+ return id;
46
+ }
47
+
48
+ dismiss(id) {
49
+ this.toasts = this.toasts.filter(t => t.id !== id);
50
+ }
51
+
52
+ dismissAll() {
53
+ this.toasts = [];
54
+ }
55
+
56
+ pause(id) {
57
+ this.toasts = this.toasts.map(t =>
58
+ t.id === id ? { ...t, pausedAt: Date.now() } : t
59
+ );
60
+ }
61
+
62
+ resume(id) {
63
+ this.toasts = this.toasts.map(t => {
64
+ if (t.id !== id || !t.pausedAt) return t;
65
+ const pausedDuration = Date.now() - t.pausedAt;
66
+ return { ...t, createdAt: t.createdAt + pausedDuration, pausedAt: undefined };
67
+ });
68
+ }
69
+
70
+ info = (message, options = {}) => this.show({ ...options, message, type: 'info' });
71
+ success = (message, options = {}) => this.show({ ...options, message, type: 'success' });
72
+ warning = (message, options = {}) => this.show({ ...options, message, type: 'warning' });
73
+ error = (message, options = {}) => this.show({ ...options, message, type: 'error' });
74
+ }
75
+
76
+ export function createToastContext() {
77
+ const controller = new ToastController();
78
+ setContext(TOAST_KEY, controller);
79
+ return controller;
80
+ }
81
+
82
+ export function getToastContext() {
83
+ return getContext(TOAST_KEY);
84
+ }
85
+ </script>
86
+
87
+ <script>
88
+ import { cn } from '../../utils/cn.svelte.js';
89
+ import { portal } from '../../actions/index.js';
90
+
91
+ const toast = getToastContext();
92
+
93
+ const icons = {
94
+ info: `<circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/>`,
95
+ success: `<circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/>`,
96
+ warning: `<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/><path d="M12 9v4"/><path d="M12 17h.01"/>`,
97
+ error: `<circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/>`
98
+ };
99
+
100
+ const positionClass = $derived({
101
+ 'top-left': 'toast-top-left',
102
+ 'top-center': 'toast-top-center',
103
+ 'top-right': 'toast-top-right',
104
+ 'bottom-left': 'toast-bottom-left',
105
+ 'bottom-center': 'toast-bottom-center',
106
+ 'bottom-right': 'toast-bottom-right'
107
+ }[toast.position]);
108
+ </script>
109
+
110
+ {#if toast.toasts.length > 0}
111
+ <div use:portal class={cn('toast-container', positionClass)} role="region" aria-label="Notifications">
112
+ {#each toast.toasts as item (item.id)}
113
+ {@const remaining = item.duration - (Date.now() - item.createdAt)}
114
+ <div
115
+ class={cn('toast-item', `toast-${item.type}`)}
116
+ role="alert"
117
+ aria-live="polite"
118
+ onmouseenter={() => toast.pause(item.id)}
119
+ onmouseleave={() => toast.resume(item.id)}
120
+ >
121
+ <span class="toast-icon">
122
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
123
+ {@html icons[item.type]}
124
+ </svg>
125
+ </span>
126
+ <div class="toast-content">
127
+ {#if item.title}
128
+ <p class="toast-title">{item.title}</p>
129
+ {/if}
130
+ <p class={cn('toast-message', item.title && 'has-title')}>{item.message}</p>
131
+ </div>
132
+ <button type="button" class="toast-close" onclick={() => toast.dismiss(item.id)} aria-label="Dismiss">
133
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
134
+ <path d="M18 6 6 18" /><path d="m6 6 12 12" />
135
+ </svg>
136
+ </button>
137
+ {#if item.duration > 0 && !item.pausedAt}
138
+ {@const _ = setTimeout(() => toast.dismiss(item.id), remaining)}
139
+ {/if}
140
+ </div>
141
+ {/each}
142
+ </div>
143
+ {/if}
144
+
145
+ <style>
146
+ .toast-container {
147
+ position: fixed;
148
+ z-index: 9999;
149
+ display: flex;
150
+ gap: 0.5rem;
151
+ pointer-events: none;
152
+ }
153
+
154
+ .toast-top-left { top: 1rem; left: 1rem; flex-direction: column; }
155
+ .toast-top-center { top: 1rem; left: 50%; transform: translateX(-50%); flex-direction: column; }
156
+ .toast-top-right { top: 1rem; right: 1rem; flex-direction: column; }
157
+ .toast-bottom-left { bottom: 1rem; left: 1rem; flex-direction: column-reverse; }
158
+ .toast-bottom-center { bottom: 1rem; left: 50%; transform: translateX(-50%); flex-direction: column-reverse; }
159
+ .toast-bottom-right { bottom: 1rem; right: 1rem; flex-direction: column-reverse; }
160
+
161
+ .toast-item {
162
+ display: flex;
163
+ align-items: flex-start;
164
+ gap: 0.75rem;
165
+ width: 100%;
166
+ max-width: 24rem;
167
+ padding: 1rem;
168
+ border-radius: 0.5rem;
169
+ border: 1px solid;
170
+ box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.3);
171
+ pointer-events: auto;
172
+ animation: toast-in 200ms ease-out;
173
+ }
174
+
175
+ .toast-info {
176
+ background-color: var(--color-base01);
177
+ border-color: var(--color-base03);
178
+ color: var(--color-base05);
179
+ }
180
+
181
+ .toast-success {
182
+ background-color: color-mix(in srgb, var(--color-base0B) 10%, var(--color-base00));
183
+ border-color: color-mix(in srgb, var(--color-base0B) 30%, transparent);
184
+ color: var(--color-base0B);
185
+ }
186
+
187
+ .toast-warning {
188
+ background-color: color-mix(in srgb, var(--color-base0A) 10%, var(--color-base00));
189
+ border-color: color-mix(in srgb, var(--color-base0A) 30%, transparent);
190
+ color: var(--color-base0A);
191
+ }
192
+
193
+ .toast-error {
194
+ background-color: color-mix(in srgb, var(--color-base08) 10%, var(--color-base00));
195
+ border-color: color-mix(in srgb, var(--color-base08) 30%, transparent);
196
+ color: var(--color-base08);
197
+ }
198
+
199
+ .toast-icon {
200
+ flex-shrink: 0;
201
+ width: 1.25rem;
202
+ height: 1.25rem;
203
+ }
204
+
205
+ .toast-icon svg {
206
+ width: 100%;
207
+ height: 100%;
208
+ }
209
+
210
+ .toast-content {
211
+ flex: 1;
212
+ min-width: 0;
213
+ }
214
+
215
+ .toast-title {
216
+ font-weight: 500;
217
+ font-size: 0.875rem;
218
+ margin: 0;
219
+ }
220
+
221
+ .toast-message {
222
+ font-size: 0.875rem;
223
+ margin: 0;
224
+ }
225
+
226
+ .toast-message.has-title {
227
+ margin-top: 0.25rem;
228
+ opacity: 0.9;
229
+ }
230
+
231
+ .toast-close {
232
+ flex-shrink: 0;
233
+ padding: 0.25rem;
234
+ border-radius: 0.375rem;
235
+ background: transparent;
236
+ border: none;
237
+ cursor: pointer;
238
+ opacity: 0.6;
239
+ transition: opacity 150ms;
240
+ }
241
+
242
+ .toast-close:hover {
243
+ opacity: 1;
244
+ }
245
+
246
+ .toast-close svg {
247
+ width: 1rem;
248
+ height: 1rem;
249
+ }
250
+
251
+ @keyframes toast-in {
252
+ from {
253
+ opacity: 0;
254
+ transform: translateX(1rem);
255
+ }
256
+ to {
257
+ opacity: 1;
258
+ transform: translateX(0);
259
+ }
260
+ }
261
+ </style>
@@ -0,0 +1,147 @@
1
+ <!--
2
+ @component Checkbox
3
+
4
+ An accessible checkbox input with custom styling.
5
+
6
+ @example
7
+ <Checkbox bind:checked={agreed}>I agree to the terms</Checkbox>
8
+
9
+ @example
10
+ <Checkbox bind:checked={enabled} disabled>Disabled option</Checkbox>
11
+ -->
12
+ <script>
13
+ import { cn } from '../../utils/cn.svelte.js';
14
+ import { generateId } from '../../utils/reactive.svelte.js';
15
+
16
+ let {
17
+ checked = $bindable(false),
18
+ disabled = false,
19
+ required = false,
20
+ error = false,
21
+ name = '',
22
+ id,
23
+ class: className = '',
24
+ children,
25
+ onchange,
26
+ ...rest
27
+ } = $props();
28
+
29
+ const inputId = id || generateId();
30
+ const checkboxClass = $derived(cn(
31
+ 'checkbox-box',
32
+ error && 'checkbox-error'
33
+ ));
34
+ </script>
35
+
36
+ <label
37
+ class={cn('checkbox-wrapper', disabled && 'checkbox-disabled', className)}
38
+ for={inputId}
39
+ >
40
+ <input
41
+ type="checkbox"
42
+ id={inputId}
43
+ {name}
44
+ {disabled}
45
+ {required}
46
+ bind:checked
47
+ {onchange}
48
+ class="checkbox-input"
49
+ aria-invalid={error || undefined}
50
+ {...rest}
51
+ />
52
+
53
+ <span class={checkboxClass}>
54
+ {#if checked}
55
+ <svg
56
+ class="checkbox-icon"
57
+ viewBox="0 0 24 24"
58
+ fill="none"
59
+ stroke="currentColor"
60
+ stroke-width="3"
61
+ stroke-linecap="round"
62
+ stroke-linejoin="round"
63
+ >
64
+ <path d="M20 6 9 17l-5-5" />
65
+ </svg>
66
+ {/if}
67
+ </span>
68
+
69
+ {#if children}
70
+ <span class="checkbox-label">
71
+ {@render children()}
72
+ </span>
73
+ {/if}
74
+ </label>
75
+
76
+ <style>
77
+ .checkbox-wrapper {
78
+ display: inline-flex;
79
+ align-items: center;
80
+ gap: var(--space-2);
81
+ cursor: pointer;
82
+ user-select: none;
83
+ }
84
+
85
+ .checkbox-disabled {
86
+ opacity: 0.5;
87
+ cursor: not-allowed;
88
+ }
89
+
90
+ .checkbox-input {
91
+ position: absolute;
92
+ width: 1px;
93
+ height: 1px;
94
+ padding: 0;
95
+ margin: -1px;
96
+ overflow: hidden;
97
+ clip: rect(0, 0, 0, 0);
98
+ white-space: nowrap;
99
+ border: 0;
100
+ }
101
+
102
+ .checkbox-box {
103
+ display: flex;
104
+ align-items: center;
105
+ justify-content: center;
106
+ width: 18px;
107
+ height: 18px;
108
+ border: 2px solid var(--color-base03);
109
+ border-radius: var(--radius-default);
110
+ background-color: var(--color-base00);
111
+ transition: var(--transition-all);
112
+ }
113
+
114
+ .checkbox-input:checked + .checkbox-box {
115
+ background-color: var(--color-base0D);
116
+ border-color: var(--color-base0D);
117
+ }
118
+
119
+ .checkbox-error {
120
+ border-color: var(--color-base08);
121
+ }
122
+
123
+ .checkbox-input:checked + .checkbox-error {
124
+ background-color: var(--color-base08);
125
+ border-color: var(--color-base08);
126
+ }
127
+
128
+ .checkbox-input:focus-visible + .checkbox-box {
129
+ outline: 2px solid var(--color-base0D);
130
+ outline-offset: 2px;
131
+ }
132
+
133
+ .checkbox-input:focus-visible + .checkbox-error {
134
+ outline-color: var(--color-base08);
135
+ }
136
+
137
+ .checkbox-icon {
138
+ width: 12px;
139
+ height: 12px;
140
+ color: white;
141
+ }
142
+
143
+ .checkbox-label {
144
+ font-size: var(--text-sm);
145
+ color: var(--color-base05);
146
+ }
147
+ </style>