@classic-homes/theme-svelte 0.1.48 → 0.1.50
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.
- package/dist/lib/components/Button.svelte +34 -49
- package/dist/lib/components/Checkbox.svelte +2 -2
- package/dist/lib/components/DateTimePicker.svelte +4 -5
- package/dist/lib/components/FileUpload.svelte +4 -2
- package/dist/lib/components/Label.svelte +1 -1
- package/dist/lib/components/Select.svelte +126 -231
- package/dist/lib/components/Separator.svelte +1 -1
- package/dist/lib/components/Signature.svelte +4 -3
- package/dist/lib/components/Switch.svelte +7 -20
- package/dist/lib/components/Switch.svelte.d.ts +0 -1
- package/dist/lib/components/Tabs.svelte +4 -2
- package/dist/lib/components/layout/FormPageLayout.svelte +3 -2
- package/dist/lib/components/layout/FormPageLayout.svelte.d.ts +1 -1
- package/dist/lib/utils.d.ts +8 -0
- package/dist/lib/utils.js +19 -0
- package/package.json +4 -1
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
class?: string;
|
|
45
45
|
onclick?: (e: MouseEvent) => void;
|
|
46
46
|
children: Snippet;
|
|
47
|
+
/** Allow passing additional HTML attributes (aria-*, data-*, etc.) */
|
|
47
48
|
[key: string]: unknown;
|
|
48
49
|
}
|
|
49
50
|
|
|
@@ -64,33 +65,41 @@
|
|
|
64
65
|
const classes = $derived(cn(buttonVariants({ variant, size }), className));
|
|
65
66
|
</script>
|
|
66
67
|
|
|
67
|
-
{#
|
|
68
|
-
<
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
68
|
+
{#snippet loadingSpinner()}
|
|
69
|
+
<svg
|
|
70
|
+
class="h-4 w-4 animate-spin"
|
|
71
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
72
|
+
fill="none"
|
|
73
|
+
viewBox="0 0 24 24"
|
|
74
|
+
aria-hidden="true"
|
|
75
|
+
>
|
|
76
|
+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"
|
|
77
|
+
></circle>
|
|
78
|
+
<path
|
|
79
|
+
class="opacity-75"
|
|
80
|
+
fill="currentColor"
|
|
81
|
+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
82
|
+
></path>
|
|
83
|
+
</svg>
|
|
84
|
+
{/snippet}
|
|
85
|
+
|
|
86
|
+
{#snippet buttonContent()}
|
|
87
|
+
{#if loading}
|
|
88
|
+
{@render loadingSpinner()}
|
|
89
|
+
{#if loadingText}
|
|
90
|
+
<span>{loadingText}</span>
|
|
91
|
+
<span class="sr-only">Loading</span>
|
|
91
92
|
{:else}
|
|
92
93
|
{@render children()}
|
|
93
94
|
{/if}
|
|
95
|
+
{:else}
|
|
96
|
+
{@render children()}
|
|
97
|
+
{/if}
|
|
98
|
+
{/snippet}
|
|
99
|
+
|
|
100
|
+
{#if href}
|
|
101
|
+
<a {href} class={classes} {...restProps}>
|
|
102
|
+
{@render buttonContent()}
|
|
94
103
|
</a>
|
|
95
104
|
{:else}
|
|
96
105
|
<button
|
|
@@ -102,30 +111,6 @@
|
|
|
102
111
|
{onclick}
|
|
103
112
|
{...restProps}
|
|
104
113
|
>
|
|
105
|
-
{
|
|
106
|
-
<svg
|
|
107
|
-
class="h-4 w-4 animate-spin"
|
|
108
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
109
|
-
fill="none"
|
|
110
|
-
viewBox="0 0 24 24"
|
|
111
|
-
aria-hidden="true"
|
|
112
|
-
>
|
|
113
|
-
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"
|
|
114
|
-
></circle>
|
|
115
|
-
<path
|
|
116
|
-
class="opacity-75"
|
|
117
|
-
fill="currentColor"
|
|
118
|
-
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
119
|
-
></path>
|
|
120
|
-
</svg>
|
|
121
|
-
{#if loadingText}
|
|
122
|
-
<span>{loadingText}</span>
|
|
123
|
-
<span class="sr-only">Loading</span>
|
|
124
|
-
{:else}
|
|
125
|
-
{@render children()}
|
|
126
|
-
{/if}
|
|
127
|
-
{:else}
|
|
128
|
-
{@render children()}
|
|
129
|
-
{/if}
|
|
114
|
+
{@render buttonContent()}
|
|
130
115
|
</button>
|
|
131
116
|
{/if}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import { Checkbox as CheckboxPrimitive } from 'bits-ui';
|
|
4
|
-
import { cn } from '../utils.js';
|
|
4
|
+
import { cn, useId } from '../utils.js';
|
|
5
5
|
|
|
6
6
|
interface Props {
|
|
7
7
|
checked?: boolean;
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
}: Props = $props();
|
|
33
33
|
|
|
34
34
|
// Generate a stable unique ID if none provided (for accessibility)
|
|
35
|
-
const generatedId =
|
|
35
|
+
const generatedId = useId('checkbox');
|
|
36
36
|
const effectiveId = $derived(id ?? generatedId);
|
|
37
37
|
|
|
38
38
|
function handleCheckedChange(newChecked: boolean | 'indeterminate') {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { Popover, Calendar } from 'bits-ui';
|
|
3
3
|
import { tick } from 'svelte';
|
|
4
|
-
import { cn } from '../utils.js';
|
|
4
|
+
import { cn, useId } from '../utils.js';
|
|
5
5
|
import { parseDate, toISOString } from '../utils/date.js';
|
|
6
6
|
import Label from './Label.svelte';
|
|
7
7
|
|
|
@@ -65,10 +65,9 @@
|
|
|
65
65
|
hint,
|
|
66
66
|
}: Props = $props();
|
|
67
67
|
|
|
68
|
-
// Generate unique IDs for accessibility
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
const componentId = $derived(id || `datetimepicker-${randomSuffix}`);
|
|
68
|
+
// Generate unique IDs for accessibility using sequential counter
|
|
69
|
+
const generatedId = useId('datetimepicker');
|
|
70
|
+
const componentId = $derived(id || generatedId);
|
|
72
71
|
const hintId = $derived(`${componentId}-hint`);
|
|
73
72
|
const errorId = $derived(`${componentId}-error`);
|
|
74
73
|
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* - Accessible with keyboard navigation
|
|
13
13
|
*/
|
|
14
14
|
import type { FileMetadata } from '../types/components.js';
|
|
15
|
-
import { cn } from '../utils.js';
|
|
15
|
+
import { cn, useId } from '../utils.js';
|
|
16
16
|
import Button from './Button.svelte';
|
|
17
17
|
|
|
18
18
|
interface Props {
|
|
@@ -42,8 +42,10 @@
|
|
|
42
42
|
class?: string;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
const generatedId = useId('file-upload');
|
|
46
|
+
|
|
45
47
|
let {
|
|
46
|
-
id =
|
|
48
|
+
id = generatedId,
|
|
47
49
|
files = $bindable([]),
|
|
48
50
|
maxFiles = 10,
|
|
49
51
|
maxSizeBytes = 10 * 1024 * 1024, // 10MB
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
<LabelPrimitive.Root
|
|
24
24
|
for={htmlFor}
|
|
25
25
|
class={cn(
|
|
26
|
-
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-
|
|
26
|
+
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-foreground',
|
|
27
27
|
disabled && 'opacity-50 cursor-not-allowed',
|
|
28
28
|
className
|
|
29
29
|
)}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { Select as SelectPrimitive } from 'bits-ui';
|
|
3
|
-
import { cn } from '../utils.js';
|
|
3
|
+
import { cn, useId } from '../utils.js';
|
|
4
4
|
import { validateNonEmptyArray } from '../validation.js';
|
|
5
5
|
import Label from './Label.svelte';
|
|
6
6
|
|
|
@@ -57,10 +57,9 @@
|
|
|
57
57
|
hint,
|
|
58
58
|
}: Props = $props();
|
|
59
59
|
|
|
60
|
-
// Generate unique IDs for accessibility
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
const componentId = $derived(id || `select-${randomSuffix}`);
|
|
60
|
+
// Generate unique IDs for accessibility using sequential counter
|
|
61
|
+
const generatedId = useId('select');
|
|
62
|
+
const componentId = $derived(id || generatedId);
|
|
64
63
|
const hintId = $derived(`${componentId}-hint`);
|
|
65
64
|
const errorId = $derived(`${componentId}-error`);
|
|
66
65
|
|
|
@@ -116,8 +115,128 @@
|
|
|
116
115
|
|
|
117
116
|
// Determine if we need a container wrapper
|
|
118
117
|
const hasWrapper = $derived(!!label || !!hint || !!error);
|
|
118
|
+
|
|
119
|
+
// Common trigger classes
|
|
120
|
+
const triggerClasses = $derived(
|
|
121
|
+
cn(
|
|
122
|
+
'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
|
|
123
|
+
error && 'border-destructive focus:ring-destructive',
|
|
124
|
+
className
|
|
125
|
+
)
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
// Common content classes
|
|
129
|
+
const contentClasses =
|
|
130
|
+
'isolate relative z-50 min-w-[var(--bits-floating-anchor-width)] overflow-hidden rounded-md border border-black bg-popover text-popover-foreground shadow-md data-[side=bottom]:border-t-0 data-[side=bottom]:rounded-t-none data-[side=top]:border-b-0 data-[side=top]:rounded-b-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2';
|
|
131
|
+
|
|
132
|
+
// Common item classes
|
|
133
|
+
const itemClasses =
|
|
134
|
+
'relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[disabled]:cursor-default';
|
|
119
135
|
</script>
|
|
120
136
|
|
|
137
|
+
{#snippet chevronIcon()}
|
|
138
|
+
<svg
|
|
139
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
140
|
+
width="24"
|
|
141
|
+
height="24"
|
|
142
|
+
viewBox="0 0 24 24"
|
|
143
|
+
fill="none"
|
|
144
|
+
stroke="currentColor"
|
|
145
|
+
stroke-width="2"
|
|
146
|
+
stroke-linecap="round"
|
|
147
|
+
stroke-linejoin="round"
|
|
148
|
+
class="h-4 w-4 opacity-50"
|
|
149
|
+
>
|
|
150
|
+
<path d="m6 9 6 6 6-6" />
|
|
151
|
+
</svg>
|
|
152
|
+
{/snippet}
|
|
153
|
+
|
|
154
|
+
{#snippet checkIcon()}
|
|
155
|
+
<svg
|
|
156
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
157
|
+
width="16"
|
|
158
|
+
height="16"
|
|
159
|
+
viewBox="0 0 24 24"
|
|
160
|
+
fill="none"
|
|
161
|
+
stroke="currentColor"
|
|
162
|
+
stroke-width="2"
|
|
163
|
+
stroke-linecap="round"
|
|
164
|
+
stroke-linejoin="round"
|
|
165
|
+
class="h-4 w-4"
|
|
166
|
+
>
|
|
167
|
+
<polyline points="20 6 9 17 4 12" />
|
|
168
|
+
</svg>
|
|
169
|
+
{/snippet}
|
|
170
|
+
|
|
171
|
+
{#snippet selectItem(option: SelectOption)}
|
|
172
|
+
<SelectPrimitive.Item
|
|
173
|
+
value={option.value}
|
|
174
|
+
label={option.label}
|
|
175
|
+
disabled={option.disabled}
|
|
176
|
+
class={itemClasses}
|
|
177
|
+
>
|
|
178
|
+
{#if option.value === value}
|
|
179
|
+
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
180
|
+
{@render checkIcon()}
|
|
181
|
+
</span>
|
|
182
|
+
{/if}
|
|
183
|
+
{option.label}
|
|
184
|
+
</SelectPrimitive.Item>
|
|
185
|
+
{/snippet}
|
|
186
|
+
|
|
187
|
+
{#snippet selectContent()}
|
|
188
|
+
<SelectPrimitive.Content
|
|
189
|
+
class={contentClasses}
|
|
190
|
+
sideOffset={-1}
|
|
191
|
+
collisionPadding={8}
|
|
192
|
+
avoidCollisions={true}
|
|
193
|
+
>
|
|
194
|
+
<div class="p-1 max-h-[300px] overflow-y-auto">
|
|
195
|
+
{#each options as item}
|
|
196
|
+
{#if isGroup(item)}
|
|
197
|
+
<SelectPrimitive.Group>
|
|
198
|
+
<div class="py-1.5 pl-8 pr-2 text-sm font-semibold">
|
|
199
|
+
{item.label}
|
|
200
|
+
</div>
|
|
201
|
+
{#each item.options as option}
|
|
202
|
+
{@render selectItem(option)}
|
|
203
|
+
{/each}
|
|
204
|
+
</SelectPrimitive.Group>
|
|
205
|
+
{:else}
|
|
206
|
+
{@render selectItem(item)}
|
|
207
|
+
{/if}
|
|
208
|
+
{/each}
|
|
209
|
+
</div>
|
|
210
|
+
</SelectPrimitive.Content>
|
|
211
|
+
{/snippet}
|
|
212
|
+
|
|
213
|
+
{#snippet selectRoot(ariaLabel: string | undefined)}
|
|
214
|
+
<SelectPrimitive.Root
|
|
215
|
+
type="single"
|
|
216
|
+
{name}
|
|
217
|
+
{disabled}
|
|
218
|
+
{required}
|
|
219
|
+
{value}
|
|
220
|
+
items={flatItems}
|
|
221
|
+
onValueChange={handleValueChange}
|
|
222
|
+
>
|
|
223
|
+
<SelectPrimitive.Trigger
|
|
224
|
+
id={componentId}
|
|
225
|
+
class={triggerClasses}
|
|
226
|
+
aria-label={ariaLabel}
|
|
227
|
+
aria-invalid={error ? 'true' : undefined}
|
|
228
|
+
aria-describedby={describedBy}
|
|
229
|
+
>
|
|
230
|
+
<span class={value ? '' : 'text-muted-foreground'}>
|
|
231
|
+
{selectedLabel || placeholder}
|
|
232
|
+
</span>
|
|
233
|
+
{@render chevronIcon()}
|
|
234
|
+
</SelectPrimitive.Trigger>
|
|
235
|
+
|
|
236
|
+
{@render selectContent()}
|
|
237
|
+
</SelectPrimitive.Root>
|
|
238
|
+
{/snippet}
|
|
239
|
+
|
|
121
240
|
{#if hasWrapper}
|
|
122
241
|
<div class="space-y-2">
|
|
123
242
|
{#if label}
|
|
@@ -130,119 +249,7 @@
|
|
|
130
249
|
</Label>
|
|
131
250
|
{/if}
|
|
132
251
|
|
|
133
|
-
|
|
134
|
-
type="single"
|
|
135
|
-
{name}
|
|
136
|
-
{disabled}
|
|
137
|
-
{required}
|
|
138
|
-
{value}
|
|
139
|
-
items={flatItems}
|
|
140
|
-
onValueChange={handleValueChange}
|
|
141
|
-
>
|
|
142
|
-
<SelectPrimitive.Trigger
|
|
143
|
-
id={componentId}
|
|
144
|
-
class={cn(
|
|
145
|
-
'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
|
|
146
|
-
error && 'border-destructive focus:ring-destructive',
|
|
147
|
-
className
|
|
148
|
-
)}
|
|
149
|
-
aria-label={!label ? placeholder : undefined}
|
|
150
|
-
aria-invalid={error ? 'true' : undefined}
|
|
151
|
-
aria-describedby={describedBy}
|
|
152
|
-
>
|
|
153
|
-
<span class={value ? '' : 'text-muted-foreground'}>
|
|
154
|
-
{selectedLabel || placeholder}
|
|
155
|
-
</span>
|
|
156
|
-
<svg
|
|
157
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
158
|
-
width="24"
|
|
159
|
-
height="24"
|
|
160
|
-
viewBox="0 0 24 24"
|
|
161
|
-
fill="none"
|
|
162
|
-
stroke="currentColor"
|
|
163
|
-
stroke-width="2"
|
|
164
|
-
stroke-linecap="round"
|
|
165
|
-
stroke-linejoin="round"
|
|
166
|
-
class="h-4 w-4 opacity-50"
|
|
167
|
-
>
|
|
168
|
-
<path d="m6 9 6 6 6-6" />
|
|
169
|
-
</svg>
|
|
170
|
-
</SelectPrimitive.Trigger>
|
|
171
|
-
|
|
172
|
-
<SelectPrimitive.Content
|
|
173
|
-
class="isolate relative z-50 min-w-[var(--bits-floating-anchor-width)] overflow-hidden rounded-md border border-black bg-popover text-popover-foreground shadow-md data-[side=bottom]:border-t-0 data-[side=bottom]:rounded-t-none data-[side=top]:border-b-0 data-[side=top]:rounded-b-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2"
|
|
174
|
-
sideOffset={-1}
|
|
175
|
-
collisionPadding={8}
|
|
176
|
-
avoidCollisions={true}
|
|
177
|
-
>
|
|
178
|
-
<div class="p-1 max-h-[300px] overflow-y-auto">
|
|
179
|
-
{#each options as item}
|
|
180
|
-
{#if isGroup(item)}
|
|
181
|
-
<SelectPrimitive.Group>
|
|
182
|
-
<div class="py-1.5 pl-8 pr-2 text-sm font-semibold">
|
|
183
|
-
{item.label}
|
|
184
|
-
</div>
|
|
185
|
-
{#each item.options as option}
|
|
186
|
-
<SelectPrimitive.Item
|
|
187
|
-
value={option.value}
|
|
188
|
-
label={option.label}
|
|
189
|
-
disabled={option.disabled}
|
|
190
|
-
class="relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[disabled]:cursor-default"
|
|
191
|
-
>
|
|
192
|
-
{#if option.value === value}
|
|
193
|
-
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
194
|
-
<svg
|
|
195
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
196
|
-
width="16"
|
|
197
|
-
height="16"
|
|
198
|
-
viewBox="0 0 24 24"
|
|
199
|
-
fill="none"
|
|
200
|
-
stroke="currentColor"
|
|
201
|
-
stroke-width="2"
|
|
202
|
-
stroke-linecap="round"
|
|
203
|
-
stroke-linejoin="round"
|
|
204
|
-
class="h-4 w-4"
|
|
205
|
-
>
|
|
206
|
-
<polyline points="20 6 9 17 4 12" />
|
|
207
|
-
</svg>
|
|
208
|
-
</span>
|
|
209
|
-
{/if}
|
|
210
|
-
{option.label}
|
|
211
|
-
</SelectPrimitive.Item>
|
|
212
|
-
{/each}
|
|
213
|
-
</SelectPrimitive.Group>
|
|
214
|
-
{:else}
|
|
215
|
-
<SelectPrimitive.Item
|
|
216
|
-
value={item.value}
|
|
217
|
-
label={item.label}
|
|
218
|
-
disabled={item.disabled}
|
|
219
|
-
class="relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[disabled]:cursor-default"
|
|
220
|
-
>
|
|
221
|
-
{#if item.value === value}
|
|
222
|
-
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
223
|
-
<svg
|
|
224
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
225
|
-
width="16"
|
|
226
|
-
height="16"
|
|
227
|
-
viewBox="0 0 24 24"
|
|
228
|
-
fill="none"
|
|
229
|
-
stroke="currentColor"
|
|
230
|
-
stroke-width="2"
|
|
231
|
-
stroke-linecap="round"
|
|
232
|
-
stroke-linejoin="round"
|
|
233
|
-
class="h-4 w-4"
|
|
234
|
-
>
|
|
235
|
-
<polyline points="20 6 9 17 4 12" />
|
|
236
|
-
</svg>
|
|
237
|
-
</span>
|
|
238
|
-
{/if}
|
|
239
|
-
{item.label}
|
|
240
|
-
</SelectPrimitive.Item>
|
|
241
|
-
{/if}
|
|
242
|
-
{/each}
|
|
243
|
-
</div>
|
|
244
|
-
</SelectPrimitive.Content>
|
|
245
|
-
</SelectPrimitive.Root>
|
|
252
|
+
{@render selectRoot(!label ? placeholder : undefined)}
|
|
246
253
|
|
|
247
254
|
{#if hint && !error}
|
|
248
255
|
<p id={hintId} class="text-sm text-muted-foreground">
|
|
@@ -257,117 +264,5 @@
|
|
|
257
264
|
{/if}
|
|
258
265
|
</div>
|
|
259
266
|
{:else}
|
|
260
|
-
|
|
261
|
-
type="single"
|
|
262
|
-
{name}
|
|
263
|
-
{disabled}
|
|
264
|
-
{required}
|
|
265
|
-
{value}
|
|
266
|
-
items={flatItems}
|
|
267
|
-
onValueChange={handleValueChange}
|
|
268
|
-
>
|
|
269
|
-
<SelectPrimitive.Trigger
|
|
270
|
-
id={componentId}
|
|
271
|
-
class={cn(
|
|
272
|
-
'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
|
|
273
|
-
error && 'border-destructive focus:ring-destructive',
|
|
274
|
-
className
|
|
275
|
-
)}
|
|
276
|
-
aria-label={placeholder}
|
|
277
|
-
aria-invalid={error ? 'true' : undefined}
|
|
278
|
-
aria-describedby={describedBy}
|
|
279
|
-
>
|
|
280
|
-
<span class={value ? '' : 'text-muted-foreground'}>
|
|
281
|
-
{selectedLabel || placeholder}
|
|
282
|
-
</span>
|
|
283
|
-
<svg
|
|
284
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
285
|
-
width="24"
|
|
286
|
-
height="24"
|
|
287
|
-
viewBox="0 0 24 24"
|
|
288
|
-
fill="none"
|
|
289
|
-
stroke="currentColor"
|
|
290
|
-
stroke-width="2"
|
|
291
|
-
stroke-linecap="round"
|
|
292
|
-
stroke-linejoin="round"
|
|
293
|
-
class="h-4 w-4 opacity-50"
|
|
294
|
-
>
|
|
295
|
-
<path d="m6 9 6 6 6-6" />
|
|
296
|
-
</svg>
|
|
297
|
-
</SelectPrimitive.Trigger>
|
|
298
|
-
|
|
299
|
-
<SelectPrimitive.Content
|
|
300
|
-
class="isolate relative z-50 min-w-[var(--bits-floating-anchor-width)] overflow-hidden rounded-md border border-black bg-popover text-popover-foreground shadow-md data-[side=bottom]:border-t-0 data-[side=bottom]:rounded-t-none data-[side=top]:border-b-0 data-[side=top]:rounded-b-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2"
|
|
301
|
-
sideOffset={-1}
|
|
302
|
-
collisionPadding={8}
|
|
303
|
-
avoidCollisions={true}
|
|
304
|
-
>
|
|
305
|
-
<div class="p-1 max-h-[300px] overflow-y-auto">
|
|
306
|
-
{#each options as item}
|
|
307
|
-
{#if isGroup(item)}
|
|
308
|
-
<SelectPrimitive.Group>
|
|
309
|
-
<div class="py-1.5 pl-8 pr-2 text-sm font-semibold">
|
|
310
|
-
{item.label}
|
|
311
|
-
</div>
|
|
312
|
-
{#each item.options as option}
|
|
313
|
-
<SelectPrimitive.Item
|
|
314
|
-
value={option.value}
|
|
315
|
-
label={option.label}
|
|
316
|
-
disabled={option.disabled}
|
|
317
|
-
class="relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[disabled]:cursor-default"
|
|
318
|
-
>
|
|
319
|
-
{#if option.value === value}
|
|
320
|
-
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
321
|
-
<svg
|
|
322
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
323
|
-
width="16"
|
|
324
|
-
height="16"
|
|
325
|
-
viewBox="0 0 24 24"
|
|
326
|
-
fill="none"
|
|
327
|
-
stroke="currentColor"
|
|
328
|
-
stroke-width="2"
|
|
329
|
-
stroke-linecap="round"
|
|
330
|
-
stroke-linejoin="round"
|
|
331
|
-
class="h-4 w-4"
|
|
332
|
-
>
|
|
333
|
-
<polyline points="20 6 9 17 4 12" />
|
|
334
|
-
</svg>
|
|
335
|
-
</span>
|
|
336
|
-
{/if}
|
|
337
|
-
{option.label}
|
|
338
|
-
</SelectPrimitive.Item>
|
|
339
|
-
{/each}
|
|
340
|
-
</SelectPrimitive.Group>
|
|
341
|
-
{:else}
|
|
342
|
-
<SelectPrimitive.Item
|
|
343
|
-
value={item.value}
|
|
344
|
-
label={item.label}
|
|
345
|
-
disabled={item.disabled}
|
|
346
|
-
class="relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[disabled]:cursor-default"
|
|
347
|
-
>
|
|
348
|
-
{#if item.value === value}
|
|
349
|
-
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
350
|
-
<svg
|
|
351
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
352
|
-
width="16"
|
|
353
|
-
height="16"
|
|
354
|
-
viewBox="0 0 24 24"
|
|
355
|
-
fill="none"
|
|
356
|
-
stroke="currentColor"
|
|
357
|
-
stroke-width="2"
|
|
358
|
-
stroke-linecap="round"
|
|
359
|
-
stroke-linejoin="round"
|
|
360
|
-
class="h-4 w-4"
|
|
361
|
-
>
|
|
362
|
-
<polyline points="20 6 9 17 4 12" />
|
|
363
|
-
</svg>
|
|
364
|
-
</span>
|
|
365
|
-
{/if}
|
|
366
|
-
{item.label}
|
|
367
|
-
</SelectPrimitive.Item>
|
|
368
|
-
{/if}
|
|
369
|
-
{/each}
|
|
370
|
-
</div>
|
|
371
|
-
</SelectPrimitive.Content>
|
|
372
|
-
</SelectPrimitive.Root>
|
|
267
|
+
{@render selectRoot(placeholder)}
|
|
373
268
|
{/if}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { untrack } from 'svelte';
|
|
3
|
-
import { cn } from '../utils.js';
|
|
3
|
+
import { cn, useId } from '../utils.js';
|
|
4
4
|
import { tv, type VariantProps } from 'tailwind-variants';
|
|
5
5
|
import type {
|
|
6
6
|
SignatureData,
|
|
@@ -841,8 +841,9 @@
|
|
|
841
841
|
}
|
|
842
842
|
});
|
|
843
843
|
|
|
844
|
-
// Computed IDs
|
|
845
|
-
const
|
|
844
|
+
// Computed IDs (using stable sequential ID generation)
|
|
845
|
+
const generatedInputId = useId('signature');
|
|
846
|
+
const inputId = $derived(id || generatedInputId);
|
|
846
847
|
const errorId = $derived(`${inputId}-error`);
|
|
847
848
|
const hintId = $derived(`${inputId}-hint`);
|
|
848
849
|
const ariaDescribedBy = $derived(
|
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
id?: string;
|
|
12
12
|
class?: string;
|
|
13
13
|
onchange?: (checked: boolean) => void;
|
|
14
|
-
[key: string]: unknown;
|
|
15
14
|
}
|
|
16
15
|
|
|
17
16
|
let {
|
|
@@ -26,36 +25,24 @@
|
|
|
26
25
|
...restProps
|
|
27
26
|
}: Props = $props();
|
|
28
27
|
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
function handleClick() {
|
|
33
|
-
isUserInteraction = true;
|
|
34
|
-
}
|
|
35
|
-
|
|
28
|
+
// Handle checked state changes from user interaction
|
|
29
|
+
// The primitive handles the actual state management via bind:checked
|
|
36
30
|
function handleCheckedChange(newChecked: boolean) {
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return;
|
|
31
|
+
// Only fire callback if value actually changed (prevents unnecessary calls)
|
|
32
|
+
if (newChecked !== checked) {
|
|
33
|
+
checked = newChecked;
|
|
34
|
+
onchange?.(newChecked);
|
|
42
35
|
}
|
|
43
|
-
|
|
44
|
-
// Only update state and notify if genuine user-initiated change
|
|
45
|
-
checked = newChecked;
|
|
46
|
-
onchange?.(newChecked);
|
|
47
|
-
isUserInteraction = false;
|
|
48
36
|
}
|
|
49
37
|
</script>
|
|
50
38
|
|
|
51
39
|
<SwitchPrimitive.Root
|
|
52
|
-
|
|
40
|
+
{checked}
|
|
53
41
|
{disabled}
|
|
54
42
|
{required}
|
|
55
43
|
{name}
|
|
56
44
|
{value}
|
|
57
45
|
{id}
|
|
58
|
-
onclick={handleClick}
|
|
59
46
|
onCheckedChange={handleCheckedChange}
|
|
60
47
|
class={cn(
|
|
61
48
|
'peer relative inline-flex h-7 w-12 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input',
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import { setContext } from 'svelte';
|
|
4
|
-
import { cn } from '../utils.js';
|
|
4
|
+
import { cn, useId } from '../utils.js';
|
|
5
5
|
import type { Tab } from '../types/components.js';
|
|
6
6
|
|
|
7
7
|
interface Props {
|
|
@@ -19,10 +19,12 @@
|
|
|
19
19
|
onValueChange?: (value: string) => void;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
const generatedId = useId('tabs');
|
|
23
|
+
|
|
22
24
|
let {
|
|
23
25
|
tabs,
|
|
24
26
|
value = $bindable(tabs[0]?.id ?? ''),
|
|
25
|
-
id =
|
|
27
|
+
id = generatedId,
|
|
26
28
|
class: className,
|
|
27
29
|
children,
|
|
28
30
|
onValueChange,
|
|
@@ -6,13 +6,14 @@
|
|
|
6
6
|
* - Centered title and description header
|
|
7
7
|
* - Optional notice cards section
|
|
8
8
|
* - Two-column layout with optional sidebar
|
|
9
|
-
* - Help text footer
|
|
9
|
+
* - Help text footer (supports safe HTML - sanitized automatically)
|
|
10
10
|
* - Responsive design (sidebar above main on mobile)
|
|
11
11
|
* - Transparent background by default (inherits from parent layout)
|
|
12
12
|
* - Optional sage background via sageBackground prop
|
|
13
13
|
*/
|
|
14
14
|
import type { Snippet } from 'svelte';
|
|
15
15
|
import { cn } from '../../utils.js';
|
|
16
|
+
import { sanitizeHtml } from '../../notices/sanitize.js';
|
|
16
17
|
import PageHeader from '../PageHeader.svelte';
|
|
17
18
|
|
|
18
19
|
interface Props {
|
|
@@ -82,7 +83,7 @@
|
|
|
82
83
|
{#if helpText}
|
|
83
84
|
<footer class="mt-8 text-center">
|
|
84
85
|
<p class="text-sm text-gray-500">
|
|
85
|
-
{@html helpText}
|
|
86
|
+
{@html sanitizeHtml(helpText)}
|
|
86
87
|
</p>
|
|
87
88
|
</footer>
|
|
88
89
|
{/if}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* - Centered title and description header
|
|
6
6
|
* - Optional notice cards section
|
|
7
7
|
* - Two-column layout with optional sidebar
|
|
8
|
-
* - Help text footer
|
|
8
|
+
* - Help text footer (supports safe HTML - sanitized automatically)
|
|
9
9
|
* - Responsive design (sidebar above main on mobile)
|
|
10
10
|
* - Transparent background by default (inherits from parent layout)
|
|
11
11
|
* - Optional sage background via sageBackground prop
|
package/dist/lib/utils.d.ts
CHANGED
|
@@ -4,3 +4,11 @@ import { type ClassValue } from 'clsx';
|
|
|
4
4
|
* Uses clsx for conditional classes and tailwind-merge to handle conflicts
|
|
5
5
|
*/
|
|
6
6
|
export declare function cn(...inputs: ClassValue[]): string;
|
|
7
|
+
/**
|
|
8
|
+
* Generate a unique ID with an optional prefix.
|
|
9
|
+
* IDs are sequential and stable, preventing SSR hydration mismatches.
|
|
10
|
+
*
|
|
11
|
+
* @param prefix - Optional prefix for the ID (e.g., 'checkbox', 'tabs')
|
|
12
|
+
* @returns A unique ID string like 'checkbox-1' or 'ct-1' (ct = classic-theme)
|
|
13
|
+
*/
|
|
14
|
+
export declare function useId(prefix?: string): string;
|
package/dist/lib/utils.js
CHANGED
|
@@ -7,3 +7,22 @@ import { twMerge } from 'tailwind-merge';
|
|
|
7
7
|
export function cn(...inputs) {
|
|
8
8
|
return twMerge(clsx(inputs));
|
|
9
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* Counter for generating unique sequential IDs.
|
|
12
|
+
* Uses a sequential counter instead of Math.random() to ensure:
|
|
13
|
+
* - Deterministic IDs that match between SSR and hydration
|
|
14
|
+
* - Stable IDs across component re-renders
|
|
15
|
+
* - Accessibility-friendly ID associations
|
|
16
|
+
*/
|
|
17
|
+
let idCounter = 0;
|
|
18
|
+
/**
|
|
19
|
+
* Generate a unique ID with an optional prefix.
|
|
20
|
+
* IDs are sequential and stable, preventing SSR hydration mismatches.
|
|
21
|
+
*
|
|
22
|
+
* @param prefix - Optional prefix for the ID (e.g., 'checkbox', 'tabs')
|
|
23
|
+
* @returns A unique ID string like 'checkbox-1' or 'ct-1' (ct = classic-theme)
|
|
24
|
+
*/
|
|
25
|
+
export function useId(prefix) {
|
|
26
|
+
const id = ++idCounter;
|
|
27
|
+
return prefix ? `${prefix}-${id}` : `ct-${id}`;
|
|
28
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@classic-homes/theme-svelte",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.50",
|
|
4
4
|
"description": "Svelte components for the Classic theme system",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"svelte": "./dist/lib/index.js",
|
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
},
|
|
19
19
|
"./styles.css": "./dist/styles.css"
|
|
20
20
|
},
|
|
21
|
+
"sideEffects": [
|
|
22
|
+
"*.css"
|
|
23
|
+
],
|
|
21
24
|
"files": [
|
|
22
25
|
"dist"
|
|
23
26
|
],
|