@poodle64/ui 2026.8.15 → 2026.9.0

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 (36) hide show
  1. package/README.md +367 -14
  2. package/dist/components/ui/button/button.svelte +13 -8
  3. package/dist/components/ui/checkbox/checkbox.svelte +23 -1
  4. package/dist/components/ui/dropdown-menu/dropdown-menu-content.svelte +1 -1
  5. package/dist/components/ui/form/form-button.svelte +7 -0
  6. package/dist/components/ui/form/form-button.svelte.d.ts +4 -0
  7. package/dist/components/ui/form/form-description.svelte +18 -0
  8. package/dist/components/ui/form/form-description.svelte.d.ts +4 -0
  9. package/dist/components/ui/form/form-element-field.svelte +25 -0
  10. package/dist/components/ui/form/form-element-field.svelte.d.ts +28 -0
  11. package/dist/components/ui/form/form-field-errors.svelte +31 -0
  12. package/dist/components/ui/form/form-field-errors.svelte.d.ts +8 -0
  13. package/dist/components/ui/form/form-field.svelte +25 -0
  14. package/dist/components/ui/form/form-field.svelte.d.ts +28 -0
  15. package/dist/components/ui/form/form-fieldset.svelte +16 -0
  16. package/dist/components/ui/form/form-fieldset.svelte.d.ts +27 -0
  17. package/dist/components/ui/form/form-label.svelte +25 -0
  18. package/dist/components/ui/form/form-label.svelte.d.ts +4 -0
  19. package/dist/components/ui/form/form-legend.svelte +17 -0
  20. package/dist/components/ui/form/form-legend.svelte.d.ts +4 -0
  21. package/dist/components/ui/form/index.d.ts +11 -0
  22. package/dist/components/ui/form/index.js +13 -0
  23. package/dist/components/ui/input-group/input-group-input.svelte.d.ts +1 -1
  24. package/dist/components/ui/page-header/page-header.svelte +70 -33
  25. package/dist/components/ui/page-header/page-header.svelte.d.ts +4 -0
  26. package/dist/components/ui/switch/index.d.ts +2 -2
  27. package/dist/components/ui/switch/index.js +1 -1
  28. package/dist/components/ui/switch/switch.svelte +38 -5
  29. package/dist/components/ui/switch/switch.svelte.d.ts +7 -1
  30. package/dist/components/ui/tabs/tabs-trigger.svelte +1 -1
  31. package/dist/format.d.ts +222 -0
  32. package/dist/format.js +422 -0
  33. package/dist/styles.css +310 -15
  34. package/package.json +16 -2
  35. package/registry/component-map.json +74 -4
  36. package/registry/component-map.md +18 -2
@@ -0,0 +1,28 @@
1
+ import * as FormPrimitive from 'formsnap';
2
+ import type { FormPathLeaves } from 'sveltekit-superforms';
3
+ import type { HTMLAttributes } from 'svelte/elements';
4
+ import type { WithElementRef } from '../../../utils.js';
5
+ declare function $$render<T extends Record<string, unknown>, U extends FormPathLeaves<T>>(): {
6
+ props: Omit<WithElementRef<HTMLAttributes<HTMLDivElement>>, "children"> & FormPrimitive.ElementFieldProps<T, U, any>;
7
+ exports: {};
8
+ bindings: "ref";
9
+ slots: {};
10
+ events: {};
11
+ };
12
+ declare class __sveltets_Render<T extends Record<string, unknown>, U extends FormPathLeaves<T>> {
13
+ props(): ReturnType<typeof $$render<T, U>>['props'];
14
+ events(): ReturnType<typeof $$render<T, U>>['events'];
15
+ slots(): ReturnType<typeof $$render<T, U>>['slots'];
16
+ bindings(): "ref";
17
+ exports(): {};
18
+ }
19
+ interface $$IsomorphicComponent {
20
+ new <T extends Record<string, unknown>, U extends FormPathLeaves<T>>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T, U>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T, U>['props']>, ReturnType<__sveltets_Render<T, U>['events']>, ReturnType<__sveltets_Render<T, U>['slots']>> & {
21
+ $$bindings?: ReturnType<__sveltets_Render<T, U>['bindings']>;
22
+ } & ReturnType<__sveltets_Render<T, U>['exports']>;
23
+ <T extends Record<string, unknown>, U extends FormPathLeaves<T>>(internal: unknown, props: ReturnType<__sveltets_Render<T, U>['props']> & {}): ReturnType<__sveltets_Render<T, U>['exports']>;
24
+ z_$$bindings?: ReturnType<__sveltets_Render<any, any>['bindings']>;
25
+ }
26
+ declare const FormElementField: $$IsomorphicComponent;
27
+ type FormElementField<T extends Record<string, unknown>, U extends FormPathLeaves<T>> = InstanceType<typeof FormElementField<T, U>>;
28
+ export default FormElementField;
@@ -0,0 +1,31 @@
1
+ <script lang="ts">
2
+ import * as FormPrimitive from 'formsnap';
3
+ import { cn } from '../../../utils.js';
4
+ import type { WithoutChild } from '../../../utils.js';
5
+
6
+ let {
7
+ ref = $bindable(null),
8
+ class: className,
9
+ errorClasses,
10
+ children: childrenProp,
11
+ ...restProps
12
+ }: WithoutChild<FormPrimitive.FieldErrorsProps> & {
13
+ errorClasses?: string | undefined | null;
14
+ } = $props();
15
+ </script>
16
+
17
+ <FormPrimitive.FieldErrors
18
+ bind:ref
19
+ class={cn('text-destructive text-sm font-medium', className)}
20
+ {...restProps}
21
+ >
22
+ {#snippet children({ errors, errorProps })}
23
+ {#if childrenProp}
24
+ {@render childrenProp({ errors, errorProps })}
25
+ {:else}
26
+ {#each errors as error (error)}
27
+ <div {...errorProps} class={cn(errorClasses)}>{error}</div>
28
+ {/each}
29
+ {/if}
30
+ {/snippet}
31
+ </FormPrimitive.FieldErrors>
@@ -0,0 +1,8 @@
1
+ import * as FormPrimitive from 'formsnap';
2
+ import type { WithoutChild } from '../../../utils.js';
3
+ type $$ComponentProps = WithoutChild<FormPrimitive.FieldErrorsProps> & {
4
+ errorClasses?: string | undefined | null;
5
+ };
6
+ declare const FormFieldErrors: import("svelte").Component<$$ComponentProps, {}, "ref">;
7
+ type FormFieldErrors = ReturnType<typeof FormFieldErrors>;
8
+ export default FormFieldErrors;
@@ -0,0 +1,25 @@
1
+ <script lang="ts" generics="T extends Record<string, unknown>, U extends FormPath<T>">
2
+ import * as FormPrimitive from 'formsnap';
3
+ import type { FormPath } from 'sveltekit-superforms';
4
+ import { cn } from '../../../utils.js';
5
+ import type { WithElementRef, WithoutChildren } from '../../../utils.js';
6
+ import type { HTMLAttributes } from 'svelte/elements';
7
+
8
+ let {
9
+ ref = $bindable(null),
10
+ class: className,
11
+ form,
12
+ name,
13
+ children: childrenProp,
14
+ ...restProps
15
+ }: FormPrimitive.FieldProps<T, U> &
16
+ WithoutChildren<WithElementRef<HTMLAttributes<HTMLDivElement>>> = $props();
17
+ </script>
18
+
19
+ <FormPrimitive.Field {form} {name}>
20
+ {#snippet children({ constraints, errors, tainted, value })}
21
+ <div bind:this={ref} data-slot="form-item" class={cn('space-y-2', className)} {...restProps}>
22
+ {@render childrenProp?.({ constraints, errors, tainted, value: value as T[U] })}
23
+ </div>
24
+ {/snippet}
25
+ </FormPrimitive.Field>
@@ -0,0 +1,28 @@
1
+ import * as FormPrimitive from 'formsnap';
2
+ import type { FormPath } from 'sveltekit-superforms';
3
+ import type { WithElementRef } from '../../../utils.js';
4
+ import type { HTMLAttributes } from 'svelte/elements';
5
+ declare function $$render<T extends Record<string, unknown>, U extends FormPath<T>>(): {
6
+ props: FormPrimitive.FieldProps<T, U, any> & Omit<WithElementRef<HTMLAttributes<HTMLDivElement>>, "children">;
7
+ exports: {};
8
+ bindings: "ref";
9
+ slots: {};
10
+ events: {};
11
+ };
12
+ declare class __sveltets_Render<T extends Record<string, unknown>, U extends FormPath<T>> {
13
+ props(): ReturnType<typeof $$render<T, U>>['props'];
14
+ events(): ReturnType<typeof $$render<T, U>>['events'];
15
+ slots(): ReturnType<typeof $$render<T, U>>['slots'];
16
+ bindings(): "ref";
17
+ exports(): {};
18
+ }
19
+ interface $$IsomorphicComponent {
20
+ new <T extends Record<string, unknown>, U extends FormPath<T>>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T, U>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T, U>['props']>, ReturnType<__sveltets_Render<T, U>['events']>, ReturnType<__sveltets_Render<T, U>['slots']>> & {
21
+ $$bindings?: ReturnType<__sveltets_Render<T, U>['bindings']>;
22
+ } & ReturnType<__sveltets_Render<T, U>['exports']>;
23
+ <T extends Record<string, unknown>, U extends FormPath<T>>(internal: unknown, props: ReturnType<__sveltets_Render<T, U>['props']> & {}): ReturnType<__sveltets_Render<T, U>['exports']>;
24
+ z_$$bindings?: ReturnType<__sveltets_Render<any, any>['bindings']>;
25
+ }
26
+ declare const FormField: $$IsomorphicComponent;
27
+ type FormField<T extends Record<string, unknown>, U extends FormPath<T>> = InstanceType<typeof FormField<T, U>>;
28
+ export default FormField;
@@ -0,0 +1,16 @@
1
+ <script lang="ts" generics="T extends Record<string, unknown>, U extends FormPath<T>">
2
+ import * as FormPrimitive from 'formsnap';
3
+ import type { FormPath } from 'sveltekit-superforms';
4
+ import { cn } from '../../../utils.js';
5
+ import type { WithoutChild } from '../../../utils.js';
6
+
7
+ let {
8
+ ref = $bindable(null),
9
+ class: className,
10
+ form,
11
+ name,
12
+ ...restProps
13
+ }: WithoutChild<FormPrimitive.FieldsetProps<T, U>> = $props();
14
+ </script>
15
+
16
+ <FormPrimitive.Fieldset bind:ref {form} {name} class={cn('space-y-2', className)} {...restProps} />
@@ -0,0 +1,27 @@
1
+ import * as FormPrimitive from 'formsnap';
2
+ import type { FormPath } from 'sveltekit-superforms';
3
+ import type { WithoutChild } from '../../../utils.js';
4
+ declare function $$render<T extends Record<string, unknown>, U extends FormPath<T>>(): {
5
+ props: WithoutChild<FormPrimitive.FieldsetProps<T, U>>;
6
+ exports: {};
7
+ bindings: "ref";
8
+ slots: {};
9
+ events: {};
10
+ };
11
+ declare class __sveltets_Render<T extends Record<string, unknown>, U extends FormPath<T>> {
12
+ props(): ReturnType<typeof $$render<T, U>>['props'];
13
+ events(): ReturnType<typeof $$render<T, U>>['events'];
14
+ slots(): ReturnType<typeof $$render<T, U>>['slots'];
15
+ bindings(): "ref";
16
+ exports(): {};
17
+ }
18
+ interface $$IsomorphicComponent {
19
+ new <T extends Record<string, unknown>, U extends FormPath<T>>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T, U>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T, U>['props']>, ReturnType<__sveltets_Render<T, U>['events']>, ReturnType<__sveltets_Render<T, U>['slots']>> & {
20
+ $$bindings?: ReturnType<__sveltets_Render<T, U>['bindings']>;
21
+ } & ReturnType<__sveltets_Render<T, U>['exports']>;
22
+ <T extends Record<string, unknown>, U extends FormPath<T>>(internal: unknown, props: ReturnType<__sveltets_Render<T, U>['props']> & {}): ReturnType<__sveltets_Render<T, U>['exports']>;
23
+ z_$$bindings?: ReturnType<__sveltets_Render<any, any>['bindings']>;
24
+ }
25
+ declare const FormFieldset: $$IsomorphicComponent;
26
+ type FormFieldset<T extends Record<string, unknown>, U extends FormPath<T>> = InstanceType<typeof FormFieldset<T, U>>;
27
+ export default FormFieldset;
@@ -0,0 +1,25 @@
1
+ <script lang="ts">
2
+ import * as FormPrimitive from 'formsnap';
3
+ import { Label } from '../label/index.js';
4
+ import { cn } from '../../../utils.js';
5
+ import type { WithoutChild } from '../../../utils.js';
6
+
7
+ let {
8
+ ref = $bindable(null),
9
+ children,
10
+ class: className,
11
+ ...restProps
12
+ }: WithoutChild<FormPrimitive.LabelProps> = $props();
13
+ </script>
14
+
15
+ <FormPrimitive.Label {...restProps} bind:ref>
16
+ {#snippet child({ props })}
17
+ <Label
18
+ {...props}
19
+ data-slot="form-label"
20
+ class={cn('data-[fs-error]:text-destructive', className)}
21
+ >
22
+ {@render children?.()}
23
+ </Label>
24
+ {/snippet}
25
+ </FormPrimitive.Label>
@@ -0,0 +1,4 @@
1
+ import * as FormPrimitive from 'formsnap';
2
+ declare const FormLabel: import("svelte").Component<Omit<FormPrimitive.LabelProps, "child">, {}, "ref">;
3
+ type FormLabel = ReturnType<typeof FormLabel>;
4
+ export default FormLabel;
@@ -0,0 +1,17 @@
1
+ <script lang="ts">
2
+ import * as FormPrimitive from 'formsnap';
3
+ import { cn } from '../../../utils.js';
4
+ import type { WithoutChild } from '../../../utils.js';
5
+
6
+ let {
7
+ ref = $bindable(null),
8
+ class: className,
9
+ ...restProps
10
+ }: WithoutChild<FormPrimitive.LegendProps> = $props();
11
+ </script>
12
+
13
+ <FormPrimitive.Legend
14
+ bind:ref
15
+ class={cn('data-[fs-error]:text-destructive text-sm leading-none font-medium', className)}
16
+ {...restProps}
17
+ />
@@ -0,0 +1,4 @@
1
+ import * as FormPrimitive from 'formsnap';
2
+ declare const FormLegend: import("svelte").Component<Omit<FormPrimitive.LegendProps, "child">, {}, "ref">;
3
+ type FormLegend = ReturnType<typeof FormLegend>;
4
+ export default FormLegend;
@@ -0,0 +1,11 @@
1
+ import * as FormPrimitive from 'formsnap';
2
+ import Description from './form-description.svelte';
3
+ import Label from './form-label.svelte';
4
+ import FieldErrors from './form-field-errors.svelte';
5
+ import Field from './form-field.svelte';
6
+ import Fieldset from './form-fieldset.svelte';
7
+ import Legend from './form-legend.svelte';
8
+ import ElementField from './form-element-field.svelte';
9
+ import Button from './form-button.svelte';
10
+ declare const Control: import("svelte").Component<FormPrimitive.ControlProps, {}, "">;
11
+ export { Field, Control, Label, Button, FieldErrors, Description, Fieldset, Legend, ElementField, Field as FormField, Control as FormControl, Description as FormDescription, Label as FormLabel, FieldErrors as FormFieldErrors, Fieldset as FormFieldset, Legend as FormLegend, ElementField as FormElementField, Button as FormButton };
@@ -0,0 +1,13 @@
1
+ import * as FormPrimitive from 'formsnap';
2
+ import Description from './form-description.svelte';
3
+ import Label from './form-label.svelte';
4
+ import FieldErrors from './form-field-errors.svelte';
5
+ import Field from './form-field.svelte';
6
+ import Fieldset from './form-fieldset.svelte';
7
+ import Legend from './form-legend.svelte';
8
+ import ElementField from './form-element-field.svelte';
9
+ import Button from './form-button.svelte';
10
+ const Control = FormPrimitive.Control;
11
+ export { Field, Control, Label, Button, FieldErrors, Description, Fieldset, Legend, ElementField,
12
+ //
13
+ Field as FormField, Control as FormControl, Description as FormDescription, Label as FormLabel, FieldErrors as FormFieldErrors, Fieldset as FormFieldset, Legend as FormLegend, ElementField as FormElementField, Button as FormButton };
@@ -2,7 +2,7 @@ declare const InputGroupInput: import("svelte").Component<(Omit<import("svelte/e
2
2
  type: "file";
3
3
  files?: FileList;
4
4
  } | {
5
- type?: "number" | "hidden" | "color" | "submit" | "reset" | "button" | "search" | "checkbox" | "radio" | (string & {}) | "text" | "tel" | "url" | "email" | "date" | "time" | "image" | "datetime-local" | "month" | "password" | "range" | "week";
5
+ type?: "number" | "month" | "hidden" | "color" | "submit" | "reset" | "button" | "search" | "checkbox" | "radio" | (string & {}) | "text" | "tel" | "url" | "email" | "date" | "time" | "image" | "datetime-local" | "password" | "range" | "week";
6
6
  files?: undefined;
7
7
  })) & {
8
8
  ref?: HTMLElement | null | undefined;
@@ -27,13 +27,22 @@
27
27
  * of routed links, and a package with no SvelteKit runtime of its own cannot
28
28
  * own those (the same reasoning that makes AppShell take `currentPath` as a
29
29
  * prop rather than importing `$app/state`).
30
+ *
31
+ * `icon` and `meta` are snippets for the same reason and earn their place the
32
+ * same way `breadcrumbs` did: three apps had hand-rolled the meta row and two
33
+ * the icon square, one of them across 38 of its 49 page headers in a full
34
+ * local reimplementation of this component. The app supplies the GLYPH and
35
+ * the ITEMS; the package owns the treatment — the tinted square, its size,
36
+ * and the row's spacing — because the treatment is the half that drifts.
30
37
  */
31
38
  let {
32
39
  eyebrow,
33
40
  breadcrumbs,
41
+ icon,
34
42
  title,
35
43
  subtitle,
36
44
  info,
45
+ meta,
37
46
  actions,
38
47
  ref = $bindable(null),
39
48
  class: className,
@@ -43,10 +52,14 @@
43
52
  eyebrow?: string;
44
53
  /** The trail above the title — the app's own routed links. */
45
54
  breadcrumbs?: Snippet;
55
+ /** The glyph alone. The tinted square around it is this component's. */
56
+ icon?: Snippet;
46
57
  /** Omit for a header that is a breadcrumb bar with actions. */
47
58
  title?: string;
48
59
  subtitle?: string;
49
60
  info?: string;
61
+ /** Facts about the page, under the title: badges, dates, a status. */
62
+ meta?: Snippet;
50
63
  actions?: Snippet;
51
64
  } = $props();
52
65
  </script>
@@ -56,43 +69,67 @@
56
69
  class={cn('mb-6 flex flex-wrap items-start justify-between gap-4', className)}
57
70
  {...restProps}
58
71
  >
59
- <div class="min-w-0">
60
- {#if breadcrumbs}
61
- <div class="text-muted-foreground mb-1.5 flex min-w-0 items-center text-sm">
62
- {@render breadcrumbs()}
63
- </div>
64
- {/if}
65
- {#if eyebrow}
66
- <div class="text-primary text-2xs tracking-eyebrow mb-1 font-medium uppercase">
67
- {eyebrow}
68
- </div>
72
+ <div class="flex min-w-0 items-start gap-3">
73
+ {#if icon}
74
+ <!-- aria-hidden: the glyph restates the title beside it, so a reader
75
+ already has the fact. `shrink-0` so it never collapses when the
76
+ title is long, and the square is the package's rather than the
77
+ app's precisely so two apps cannot pick two sizes for it. -->
78
+ <span
79
+ class="bg-primary/10 text-primary mt-0.5 inline-flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg]:size-5"
80
+ aria-hidden="true"
81
+ >
82
+ {@render icon()}
83
+ </span>
69
84
  {/if}
70
- {#if title}
71
- <div class="flex items-center gap-2">
72
- <h1 class="font-display text-display leading-tight font-semibold tracking-[-0.02em]">
73
- {title}
74
- </h1>
75
- {#if info}
85
+ <div class="min-w-0">
86
+ {#if breadcrumbs}
87
+ <div class="text-muted-foreground mb-1.5 flex min-w-0 items-center text-sm">
88
+ {@render breadcrumbs()}
89
+ </div>
90
+ {/if}
91
+ {#if eyebrow}
92
+ <div class="text-primary text-2xs tracking-eyebrow mb-1 font-medium uppercase">
93
+ {eyebrow}
94
+ </div>
95
+ {/if}
96
+ {#if title}
97
+ <div class="flex items-center gap-2">
98
+ <h1 class="font-display text-display leading-tight font-semibold tracking-[-0.02em]">
99
+ {title}
100
+ </h1>
101
+ {#if info}
102
+ <InfoTip
103
+ text={info}
104
+ class="text-muted-foreground/50 hover:text-muted-foreground mt-0.5 size-4 transition-colors"
105
+ />
106
+ {/if}
107
+ </div>
108
+ {:else if info}
109
+ <!-- Title-less, but the page still has something to explain. The tip
110
+ keeps a row of its own rather than being dropped along with the
111
+ heading it usually sits beside. -->
112
+ <div class="flex items-center gap-2">
76
113
  <InfoTip
77
114
  text={info}
78
- class="text-muted-foreground/50 hover:text-muted-foreground mt-0.5 size-4 transition-colors"
115
+ class="text-muted-foreground/50 hover:text-muted-foreground size-4 transition-colors"
79
116
  />
80
- {/if}
81
- </div>
82
- {:else if info}
83
- <!-- Title-less, but the page still has something to explain. The tip
84
- keeps a row of its own rather than being dropped along with the
85
- heading it usually sits beside. -->
86
- <div class="flex items-center gap-2">
87
- <InfoTip
88
- text={info}
89
- class="text-muted-foreground/50 hover:text-muted-foreground size-4 transition-colors"
90
- />
91
- </div>
92
- {/if}
93
- {#if subtitle}
94
- <p class="text-muted-foreground mt-1.5 line-clamp-1 max-w-4xl text-sm">{subtitle}</p>
95
- {/if}
117
+ </div>
118
+ {/if}
119
+ {#if subtitle}
120
+ <p class="text-muted-foreground mt-1.5 line-clamp-1 max-w-4xl text-sm">{subtitle}</p>
121
+ {/if}
122
+ {#if meta}
123
+ <!-- `min-w-0` for the reason the actions row carries it: this is a flex
124
+ item and would otherwise floor at its own min-content width, which
125
+ wrapping cannot get below. -->
126
+ <div
127
+ class="text-muted-foreground mt-2 flex min-w-0 flex-wrap items-center gap-x-4 gap-y-1.5 text-xs"
128
+ >
129
+ {@render meta()}
130
+ </div>
131
+ {/if}
132
+ </div>
96
133
  </div>
97
134
  {#if actions}
98
135
  <!--
@@ -6,10 +6,14 @@ type $$ComponentProps = WithElementRef<HTMLAttributes<HTMLDivElement>> & {
6
6
  eyebrow?: string;
7
7
  /** The trail above the title — the app's own routed links. */
8
8
  breadcrumbs?: Snippet;
9
+ /** The glyph alone. The tinted square around it is this component's. */
10
+ icon?: Snippet;
9
11
  /** Omit for a header that is a breadcrumb bar with actions. */
10
12
  title?: string;
11
13
  subtitle?: string;
12
14
  info?: string;
15
+ /** Facts about the page, under the title: badges, dates, a status. */
16
+ meta?: Snippet;
13
17
  actions?: Snippet;
14
18
  };
15
19
  declare const PageHeader: import("svelte").Component<$$ComponentProps, {}, "ref">;
@@ -1,2 +1,2 @@
1
- import Root from './switch.svelte';
2
- export { Root, Root as Switch };
1
+ import Root, { type SwitchSize } from './switch.svelte';
2
+ export { Root, Root as Switch, type SwitchSize };
@@ -1,4 +1,4 @@
1
- import Root from './switch.svelte';
1
+ import Root, {} from './switch.svelte';
2
2
  export { Root,
3
3
  //
4
4
  Root as Switch };
@@ -1,3 +1,8 @@
1
+ <script lang="ts" module>
2
+ /** The two track sizes. `sm` lines up with a `size="sm"` control row. */
3
+ export type SwitchSize = 'default' | 'sm';
4
+ </script>
5
+
1
6
  <script lang="ts">
2
7
  import { Switch as SwitchPrimitive } from 'bits-ui';
3
8
  import { cn, type WithoutChildrenOrChild } from '../../../utils.js';
@@ -5,9 +10,23 @@
5
10
  let {
6
11
  ref = $bindable(null),
7
12
  checked = $bindable(false),
13
+ size = 'default',
8
14
  class: className,
9
15
  ...restProps
10
- }: WithoutChildrenOrChild<SwitchPrimitive.RootProps> = $props();
16
+ }: WithoutChildrenOrChild<SwitchPrimitive.RootProps> & { size?: SwitchSize } = $props();
17
+
18
+ // Track, thumb and travel are one decision, so they are one table rather
19
+ // than three prop reads at three call sites. The travel is not a picked
20
+ // number: the track carries a 2px transparent border, so the thumb's run is
21
+ // (width - 4) - thumb, which is 16px on the default and 12px on `sm`.
22
+ const TRACK = {
23
+ default: 'h-5 w-9',
24
+ sm: 'h-4 w-7'
25
+ } as const;
26
+ const THUMB = {
27
+ default: 'size-4 data-checked:translate-x-4',
28
+ sm: 'size-3 data-checked:translate-x-3'
29
+ } as const;
11
30
  </script>
12
31
 
13
32
  <!--
@@ -17,23 +36,37 @@
17
36
  ball stays light on the dark-first theme. The track colour comes from the
18
37
  project tokens. An app with a non-standard root font-size (a dense console
19
38
  running an 18px root, say) compensates in its own override layer, not here.
39
+
40
+ The track is 20px tall (16px at `sm`), under WCAG 2.5.8's 24px minimum, so a
41
+ transparent `::after` skirt lifts the pointer target over it. It grows the
42
+ BLOCK axis only: the track is already 36px wide, so there is nothing to win
43
+ horizontally and an inline skirt would reach into the label beside it.
44
+
45
+ The skirt is inset from the PADDING box, and this track carries a 2px
46
+ transparent border, so its real reach is the inset minus 2 on each side —
47
+ which is why the number here is not the number in the target. The target is
48
+ measured in `harness/drive.mjs`, not calculated.
20
49
  -->
21
50
  <SwitchPrimitive.Root
22
51
  bind:ref
23
52
  bind:checked
24
53
  class={cn(
25
- 'peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors',
54
+ 'peer relative inline-flex shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors',
55
+ 'after:absolute after:inset-x-0 after:-inset-y-2',
26
56
  'focus-visible:ring-ring focus-visible:ring-offset-background focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none',
57
+ 'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 aria-invalid:ring-3',
27
58
  'disabled:cursor-not-allowed disabled:opacity-50',
28
- 'data-[state=checked]:bg-primary data-[state=unchecked]:bg-input',
59
+ 'data-checked:bg-primary data-unchecked:bg-input',
60
+ TRACK[size],
29
61
  className
30
62
  )}
31
63
  {...restProps}
32
64
  >
33
65
  <SwitchPrimitive.Thumb
34
66
  class={cn(
35
- 'bg-background dark:bg-foreground pointer-events-none block h-4 w-4 rounded-full shadow-lg ring-0 transition-transform',
36
- 'data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0'
67
+ 'bg-background dark:bg-foreground pointer-events-none block rounded-full shadow-lg ring-0 transition-transform',
68
+ 'data-unchecked:translate-x-0',
69
+ THUMB[size]
37
70
  )}
38
71
  />
39
72
  </SwitchPrimitive.Root>
@@ -1,4 +1,10 @@
1
+ /** The two track sizes. `sm` lines up with a `size="sm"` control row. */
2
+ export type SwitchSize = 'default' | 'sm';
1
3
  import { Switch as SwitchPrimitive } from 'bits-ui';
2
- declare const Switch: import("svelte").Component<Omit<Omit<SwitchPrimitive.RootProps, "child">, "children">, {}, "ref" | "checked">;
4
+ import { type WithoutChildrenOrChild } from '../../../utils.js';
5
+ type $$ComponentProps = WithoutChildrenOrChild<SwitchPrimitive.RootProps> & {
6
+ size?: SwitchSize;
7
+ };
8
+ declare const Switch: import("svelte").Component<$$ComponentProps, {}, "ref" | "checked">;
3
9
  type Switch = ReturnType<typeof Switch>;
4
10
  export default Switch;
@@ -9,7 +9,7 @@
9
9
  bind:ref
10
10
  data-slot="tabs-trigger"
11
11
  class={cn(
12
- "data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
12
+ "data-active:bg-background dark:data-active:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 dark:data-active:border-input dark:data-active:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-active:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
13
13
  className
14
14
  )}
15
15
  {...restProps}