@loidolt/theme-svelte 0.2.0 → 0.3.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 (55) hide show
  1. package/dist/components/Accordion.svelte +100 -0
  2. package/dist/components/Accordion.svelte.d.ts +50 -0
  3. package/dist/components/Alert.svelte +6 -3
  4. package/dist/components/Alert.svelte.d.ts +3 -2
  5. package/dist/components/AlertDialog.svelte +109 -0
  6. package/dist/components/AlertDialog.svelte.d.ts +32 -0
  7. package/dist/components/Badge.svelte +9 -2
  8. package/dist/components/Badge.svelte.d.ts +2 -1
  9. package/dist/components/Brand.svelte +29 -15
  10. package/dist/components/Brand.svelte.d.ts +8 -4
  11. package/dist/components/Breadcrumbs.svelte +53 -0
  12. package/dist/components/Breadcrumbs.svelte.d.ts +18 -0
  13. package/dist/components/Button.svelte +43 -21
  14. package/dist/components/Button.svelte.d.ts +9 -4
  15. package/dist/components/Drawer.svelte +90 -0
  16. package/dist/components/Drawer.svelte.d.ts +28 -0
  17. package/dist/components/DropdownMenu.svelte +2 -2
  18. package/dist/components/DropdownMenu.svelte.d.ts +1 -1
  19. package/dist/components/EmptyState.svelte +40 -0
  20. package/dist/components/EmptyState.svelte.d.ts +19 -0
  21. package/dist/components/Fieldset.svelte +60 -0
  22. package/dist/components/Fieldset.svelte.d.ts +19 -0
  23. package/dist/components/IconButton.svelte +1 -1
  24. package/dist/components/Input.svelte.d.ts +1 -1
  25. package/dist/components/NavMenu.svelte +6 -1
  26. package/dist/components/NavMenu.svelte.d.ts +2 -1
  27. package/dist/components/NumberField.svelte +12 -1
  28. package/dist/components/NumberField.svelte.d.ts +1 -1
  29. package/dist/components/Pagination.svelte +86 -0
  30. package/dist/components/Pagination.svelte.d.ts +22 -0
  31. package/dist/components/RadioGroup.svelte.d.ts +2 -2
  32. package/dist/components/Select.svelte.d.ts +2 -2
  33. package/dist/components/Table.svelte +30 -3
  34. package/dist/components/Table.svelte.d.ts +13 -0
  35. package/dist/components/TableHeader.svelte +72 -0
  36. package/dist/components/TableHeader.svelte.d.ts +24 -0
  37. package/dist/components/Tabs.svelte +15 -7
  38. package/dist/components/Tabs.svelte.d.ts +4 -3
  39. package/dist/components/Textarea.svelte.d.ts +1 -1
  40. package/dist/components/ThemeToggle.svelte +62 -0
  41. package/dist/components/ThemeToggle.svelte.d.ts +25 -0
  42. package/dist/components/ToggleGroup.svelte +107 -0
  43. package/dist/components/ToggleGroup.svelte.d.ts +46 -0
  44. package/dist/components/TooltipProvider.svelte +1 -0
  45. package/dist/components/Workspace.svelte +18 -5
  46. package/dist/components/Workspace.svelte.d.ts +8 -0
  47. package/dist/index.d.ts +16 -2
  48. package/dist/index.js +13 -1
  49. package/dist/media.svelte.d.ts +48 -0
  50. package/dist/media.svelte.js +69 -0
  51. package/dist/theme.svelte.d.ts +86 -0
  52. package/dist/theme.svelte.js +156 -0
  53. package/dist/toaster.svelte.js +22 -8
  54. package/dist/types.d.ts +21 -0
  55. package/package.json +5 -4
@@ -0,0 +1,100 @@
1
+ <script lang="ts" generics="T extends string = string">
2
+ import { Accordion as AccordionPrimitive } from 'bits-ui';
3
+ import type { Snippet } from 'svelte';
4
+ import type { HTMLAttributes } from 'svelte/elements';
5
+ import type { Disclosure, HeadingLevel } from '../types.js';
6
+ import { cx } from '../utils.js';
7
+
8
+ interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
9
+ items: Disclosure<T>[];
10
+ /** Narrowed from the DOM attribute: the Bits root this spreads onto rejects `null`. */
11
+ id?: string;
12
+ /** Lets more than one panel stay open. `value` is then an array. */
13
+ multiple?: boolean;
14
+ /** Open panel, or panels when `multiple`. Bindable. */
15
+ value?: T | T[];
16
+ /**
17
+ * Heading level of every trigger's wrapper. Panels sit at one level below whatever heading
18
+ * introduces the accordion, so keep this in step with the surrounding outline.
19
+ */
20
+ headingLevel?: HeadingLevel;
21
+ disabled?: boolean;
22
+ /** Panel body, rendered for each item with the item's own value. */
23
+ children: Snippet<[{ value: T }]>;
24
+ class?: string;
25
+ itemClass?: string;
26
+ contentClass?: string;
27
+ onValueChange?: (value: T | T[]) => void;
28
+ ref?: HTMLDivElement | null;
29
+ }
30
+
31
+ let {
32
+ items,
33
+ multiple = false,
34
+ value = $bindable(),
35
+ headingLevel = 3,
36
+ disabled = false,
37
+ children,
38
+ class: className,
39
+ itemClass,
40
+ contentClass,
41
+ onValueChange,
42
+ ref = $bindable(null),
43
+ ...rest
44
+ }: Props = $props();
45
+
46
+ // Bits discriminates single/multiple at the type level with a different `value` shape for
47
+ // each. The panels are identical either way, so the markup lives in one snippet and only the
48
+ // Root differs.
49
+ const change = (next: string | string[]) => {
50
+ value = next as T | T[];
51
+ onValueChange?.(next as T | T[]);
52
+ };
53
+ </script>
54
+
55
+ {#snippet panels()}
56
+ {#each items as item (item.value)}
57
+ <AccordionPrimitive.Item
58
+ value={item.value}
59
+ disabled={item.disabled}
60
+ class={cx('ldt-accordion__item', itemClass)}
61
+ >
62
+ <AccordionPrimitive.Header level={headingLevel} class="ldt-accordion__heading">
63
+ <AccordionPrimitive.Trigger class="ldt-accordion__trigger">
64
+ <span class="ldt-accordion__label">{item.title}</span>
65
+ <!-- Decorative: the trigger's `aria-expanded` already carries open state. -->
66
+ <span class="ldt-accordion__indicator" aria-hidden="true"></span>
67
+ </AccordionPrimitive.Trigger>
68
+ </AccordionPrimitive.Header>
69
+ <AccordionPrimitive.Content class={cx('ldt-accordion__content', contentClass)}>
70
+ <div class="ldt-accordion__body">{@render children({ value: item.value })}</div>
71
+ </AccordionPrimitive.Content>
72
+ </AccordionPrimitive.Item>
73
+ {/each}
74
+ {/snippet}
75
+
76
+ {#if multiple}
77
+ <AccordionPrimitive.Root
78
+ bind:ref
79
+ type="multiple"
80
+ value={(value ?? []) as string[]}
81
+ onValueChange={change}
82
+ {disabled}
83
+ class={cx('ldt-accordion', className)}
84
+ {...rest}
85
+ >
86
+ {@render panels()}
87
+ </AccordionPrimitive.Root>
88
+ {:else}
89
+ <AccordionPrimitive.Root
90
+ bind:ref
91
+ type="single"
92
+ value={(value ?? '') as string}
93
+ onValueChange={change}
94
+ {disabled}
95
+ class={cx('ldt-accordion', className)}
96
+ {...rest}
97
+ >
98
+ {@render panels()}
99
+ </AccordionPrimitive.Root>
100
+ {/if}
@@ -0,0 +1,50 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ import type { Disclosure, HeadingLevel } from '../types.js';
4
+ declare function $$render<T extends string = string>(): {
5
+ props: Omit<HTMLAttributes<HTMLDivElement>, "children"> & {
6
+ items: Disclosure<T>[];
7
+ /** Narrowed from the DOM attribute: the Bits root this spreads onto rejects `null`. */
8
+ id?: string;
9
+ /** Lets more than one panel stay open. `value` is then an array. */
10
+ multiple?: boolean;
11
+ /** Open panel, or panels when `multiple`. Bindable. */
12
+ value?: T | T[];
13
+ /**
14
+ * Heading level of every trigger's wrapper. Panels sit at one level below whatever heading
15
+ * introduces the accordion, so keep this in step with the surrounding outline.
16
+ */
17
+ headingLevel?: HeadingLevel;
18
+ disabled?: boolean;
19
+ /** Panel body, rendered for each item with the item's own value. */
20
+ children: Snippet<[{
21
+ value: T;
22
+ }]>;
23
+ class?: string;
24
+ itemClass?: string;
25
+ contentClass?: string;
26
+ onValueChange?: (value: T | T[]) => void;
27
+ ref?: HTMLDivElement | null;
28
+ };
29
+ exports: {};
30
+ bindings: "value" | "ref";
31
+ slots: {};
32
+ events: {};
33
+ };
34
+ declare class __sveltets_Render<T extends string = string> {
35
+ props(): ReturnType<typeof $$render<T>>['props'];
36
+ events(): ReturnType<typeof $$render<T>>['events'];
37
+ slots(): ReturnType<typeof $$render<T>>['slots'];
38
+ bindings(): "value" | "ref";
39
+ exports(): {};
40
+ }
41
+ interface $$IsomorphicComponent {
42
+ new <T extends string = string>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
43
+ $$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
44
+ } & ReturnType<__sveltets_Render<T>['exports']>;
45
+ <T extends string = string>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
46
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
47
+ }
48
+ declare const Accordion: $$IsomorphicComponent;
49
+ type Accordion<T extends string = string> = InstanceType<typeof Accordion<T>>;
50
+ export default Accordion;
@@ -4,9 +4,10 @@
4
4
  import type { HeadingLevel, StatusVariant } from '../types.js';
5
5
  import { cx } from '../utils.js';
6
6
 
7
- interface Props extends HTMLAttributes<HTMLDivElement> {
7
+ interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'title'> {
8
8
  variant?: StatusVariant;
9
- title: string;
9
+ /** Optional heading. Accepts a snippet for rich content; omit it for a body-only alert. */
10
+ title?: string | Snippet;
10
11
  /** Heading element used for the title, so the alert fits the page outline. */
11
12
  headingLevel?: HeadingLevel;
12
13
  class?: string;
@@ -31,6 +32,8 @@
31
32
  role={variant === 'error' ? 'alert' : 'status'}
32
33
  {...rest}
33
34
  >
34
- <svelte:element this={`h${headingLevel}`} class="ldt-alert__title">{title}</svelte:element>
35
+ {#if title}<svelte:element this={`h${headingLevel}`} class="ldt-alert__title"
36
+ >{#if typeof title === 'string'}{title}{:else}{@render title()}{/if}</svelte:element
37
+ >{/if}
35
38
  {#if children}<div class="ldt-alert__description">{@render children()}</div>{/if}
36
39
  </div>
@@ -1,9 +1,10 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  import type { HTMLAttributes } from 'svelte/elements';
3
3
  import type { HeadingLevel, StatusVariant } from '../types.js';
4
- interface Props extends HTMLAttributes<HTMLDivElement> {
4
+ interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'title'> {
5
5
  variant?: StatusVariant;
6
- title: string;
6
+ /** Optional heading. Accepts a snippet for rich content; omit it for a body-only alert. */
7
+ title?: string | Snippet;
7
8
  /** Heading element used for the title, so the alert fits the page outline. */
8
9
  headingLevel?: HeadingLevel;
9
10
  class?: string;
@@ -0,0 +1,109 @@
1
+ <script lang="ts">
2
+ import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
3
+ import type { Snippet } from 'svelte';
4
+ import type { AlertDialogContentProps } from 'bits-ui';
5
+ import type { ActionVariant, DialogSize, HeadingLevel, TriggerChildProps } from '../types.js';
6
+ import { cx } from '../utils.js';
7
+
8
+ type Props = Omit<AlertDialogContentProps, 'children' | 'class' | 'child'> & {
9
+ open?: boolean;
10
+ title: string;
11
+ /** What confirming will do. Say the consequence here, not in the button. */
12
+ description?: string;
13
+ /** Trigger contents, rendered inside a button styled with `triggerClass`. */
14
+ trigger?: Snippet;
15
+ /** Full control of the trigger element — receives the props Bits needs on it. */
16
+ triggerChild?: Snippet<[TriggerChildProps]>;
17
+ triggerClass?: string;
18
+ /** Extra body content between the description and the buttons. */
19
+ children?: Snippet;
20
+ confirmLabel?: string;
21
+ cancelLabel?: string;
22
+ /** Colour of the confirm button. Use `danger` for anything destructive. */
23
+ confirmVariant?: ActionVariant;
24
+ onConfirm?: () => void;
25
+ onCancel?: () => void;
26
+ size?: DialogSize;
27
+ headingLevel?: HeadingLevel;
28
+ /** `class` lands on the portaled Content element. */
29
+ class?: string;
30
+ overlayClass?: string;
31
+ onOpenChange?: (open: boolean) => void;
32
+ ref?: HTMLElement | null;
33
+ };
34
+
35
+ let {
36
+ open = $bindable(false),
37
+ title,
38
+ description,
39
+ trigger,
40
+ triggerChild,
41
+ triggerClass = 'ldt-button',
42
+ children,
43
+ confirmLabel = 'Confirm',
44
+ cancelLabel = 'Cancel',
45
+ confirmVariant = 'primary',
46
+ onConfirm,
47
+ onCancel,
48
+ size = 'sm',
49
+ headingLevel = 2,
50
+ class: className,
51
+ overlayClass,
52
+ onOpenChange,
53
+ ref = $bindable(null),
54
+ ...rest
55
+ }: Props = $props();
56
+ </script>
57
+
58
+ <!--
59
+ A decision the user has to answer before anything else continues. Unlike `Dialog` it has no
60
+ close affordance in the corner and does not dismiss on an outside click — Bits' alertdialog
61
+ role enforces that, and it is the point: a confirmation you can dismiss by missing is not one.
62
+ Focus opens on Cancel, so a stray Enter cannot confirm a destructive action.
63
+ -->
64
+ <AlertDialogPrimitive.Root bind:open {onOpenChange}>
65
+ {#if triggerChild}<AlertDialogPrimitive.Trigger
66
+ >{#snippet child(childProps)}{@render triggerChild(
67
+ childProps
68
+ )}{/snippet}</AlertDialogPrimitive.Trigger
69
+ >{:else if trigger}<AlertDialogPrimitive.Trigger class={triggerClass}
70
+ >{@render trigger()}</AlertDialogPrimitive.Trigger
71
+ >{/if}
72
+ <AlertDialogPrimitive.Portal>
73
+ <AlertDialogPrimitive.Overlay class={cx('ldt-overlay', overlayClass)} />
74
+ <AlertDialogPrimitive.Content
75
+ bind:ref
76
+ class={cx('ldt-dialog', size && `ldt-dialog--${size}`, className)}
77
+ {...rest}
78
+ >
79
+ <header class="ldt-dialog__header">
80
+ <div>
81
+ <AlertDialogPrimitive.Title level={headingLevel} class="ldt-dialog__title"
82
+ >{title}</AlertDialogPrimitive.Title
83
+ >{#if description}<AlertDialogPrimitive.Description class="ldt-dialog__description"
84
+ >{description}</AlertDialogPrimitive.Description
85
+ >{/if}
86
+ </div>
87
+ </header>
88
+ {#if children}<div class="ldt-dialog__body">{@render children()}</div>{/if}
89
+ <footer class="ldt-dialog__footer">
90
+ <AlertDialogPrimitive.Cancel
91
+ class="ldt-button ldt-button--quiet"
92
+ onclick={() => onCancel?.()}>{cancelLabel}</AlertDialogPrimitive.Cancel
93
+ >
94
+ <!--
95
+ Bits leaves the Action button open on purpose, for callers awaiting a request. This
96
+ wrapper is the declarative form, so it closes: `open` is bindable, and work that has to
97
+ keep the dialog up can set it back to `true` from `onConfirm`.
98
+ -->
99
+ <AlertDialogPrimitive.Action
100
+ class={cx('ldt-button', confirmVariant !== 'default' && `ldt-button--${confirmVariant}`)}
101
+ onclick={() => {
102
+ onConfirm?.();
103
+ open = false;
104
+ }}>{confirmLabel}</AlertDialogPrimitive.Action
105
+ >
106
+ </footer>
107
+ </AlertDialogPrimitive.Content>
108
+ </AlertDialogPrimitive.Portal>
109
+ </AlertDialogPrimitive.Root>
@@ -0,0 +1,32 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { AlertDialogContentProps } from 'bits-ui';
3
+ import type { ActionVariant, DialogSize, HeadingLevel, TriggerChildProps } from '../types.js';
4
+ type Props = Omit<AlertDialogContentProps, 'children' | 'class' | 'child'> & {
5
+ open?: boolean;
6
+ title: string;
7
+ /** What confirming will do. Say the consequence here, not in the button. */
8
+ description?: string;
9
+ /** Trigger contents, rendered inside a button styled with `triggerClass`. */
10
+ trigger?: Snippet;
11
+ /** Full control of the trigger element — receives the props Bits needs on it. */
12
+ triggerChild?: Snippet<[TriggerChildProps]>;
13
+ triggerClass?: string;
14
+ /** Extra body content between the description and the buttons. */
15
+ children?: Snippet;
16
+ confirmLabel?: string;
17
+ cancelLabel?: string;
18
+ /** Colour of the confirm button. Use `danger` for anything destructive. */
19
+ confirmVariant?: ActionVariant;
20
+ onConfirm?: () => void;
21
+ onCancel?: () => void;
22
+ size?: DialogSize;
23
+ headingLevel?: HeadingLevel;
24
+ /** `class` lands on the portaled Content element. */
25
+ class?: string;
26
+ overlayClass?: string;
27
+ onOpenChange?: (open: boolean) => void;
28
+ ref?: HTMLElement | null;
29
+ };
30
+ declare const AlertDialog: import("svelte").Component<Props, {}, "ref" | "open">;
31
+ type AlertDialog = ReturnType<typeof AlertDialog>;
32
+ export default AlertDialog;
@@ -1,11 +1,12 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
3
  import type { HTMLAttributes } from 'svelte/elements';
4
- import type { BadgeVariant } from '../types.js';
4
+ import type { BadgeSize, BadgeVariant } from '../types.js';
5
5
  import { cx } from '../utils.js';
6
6
 
7
7
  interface Props extends HTMLAttributes<HTMLSpanElement> {
8
8
  variant?: BadgeVariant;
9
+ size?: BadgeSize;
9
10
  class?: string;
10
11
  children: Snippet;
11
12
  ref?: HTMLSpanElement | null;
@@ -13,6 +14,7 @@
13
14
 
14
15
  let {
15
16
  variant = 'default',
17
+ size = 'inline',
16
18
  class: className,
17
19
  children,
18
20
  ref = $bindable(null),
@@ -22,6 +24,11 @@
22
24
 
23
25
  <span
24
26
  bind:this={ref}
25
- class={cx('ldt-badge', variant !== 'default' && `ldt-badge--${variant}`, className)}
27
+ class={cx(
28
+ 'ldt-badge',
29
+ variant !== 'default' && `ldt-badge--${variant}`,
30
+ size !== 'inline' && `ldt-badge--${size}`,
31
+ className
32
+ )}
26
33
  {...rest}>{@render children()}</span
27
34
  >
@@ -1,8 +1,9 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  import type { HTMLAttributes } from 'svelte/elements';
3
- import type { BadgeVariant } from '../types.js';
3
+ import type { BadgeSize, BadgeVariant } from '../types.js';
4
4
  interface Props extends HTMLAttributes<HTMLSpanElement> {
5
5
  variant?: BadgeVariant;
6
+ size?: BadgeSize;
6
7
  class?: string;
7
8
  children: Snippet;
8
9
  ref?: HTMLSpanElement | null;
@@ -3,17 +3,24 @@
3
3
  import type { HTMLAnchorAttributes, HTMLAttributes } from 'svelte/elements';
4
4
  import { cx } from '../utils.js';
5
5
 
6
- type Props = HTMLAnchorAttributes &
7
- HTMLAttributes<HTMLSpanElement> & {
8
- name: string;
9
- /** Secondary line, e.g. an environment or version marker. */
10
- meta?: string;
11
- /** Renders an `<a>` instead of a `<span>`. */
12
- href?: string;
13
- mark?: Snippet;
14
- class?: string;
15
- ref?: HTMLElement | null;
16
- };
6
+ interface CommonProps {
7
+ name: string;
8
+ /** Secondary line, e.g. an environment or version marker. */
9
+ meta?: string;
10
+ mark?: Snippet;
11
+ class?: string;
12
+ ref?: HTMLElement | null;
13
+ }
14
+
15
+ // Discriminated on `href`: an `<a>` with anchor attributes, or a plain `<span>`.
16
+ type Props = CommonProps &
17
+ (
18
+ | (Omit<HTMLAnchorAttributes, 'class'> & {
19
+ /** Renders an `<a>` instead of a `<span>`. */
20
+ href: string;
21
+ })
22
+ | (Omit<HTMLAttributes<HTMLSpanElement>, 'class'> & { href?: never })
23
+ );
17
24
 
18
25
  let {
19
26
  name,
@@ -28,8 +35,15 @@
28
35
 
29
36
  {#snippet content()}{#if mark}{@render mark()}{/if}<strong class="ldt-brand__name">{name}</strong
30
37
  >{#if meta}<span class="ldt-brand__meta">{meta}</span>{/if}{/snippet}
31
- {#if href}<a bind:this={ref} {href} class={cx('ldt-brand', className)} {...rest}
32
- >{@render content()}</a
33
- >{:else}<span bind:this={ref} class={cx('ldt-brand', className)} {...rest}
34
- >{@render content()}</span
38
+ <!-- TS cannot narrow the destructured `rest` by `href`, so each branch casts to its side
39
+ of the union. The public Props type still rejects mismatched attributes. -->
40
+ {#if href}<a
41
+ bind:this={ref}
42
+ {href}
43
+ class={cx('ldt-brand', className)}
44
+ {...rest as HTMLAnchorAttributes}>{@render content()}</a
45
+ >{:else}<span
46
+ bind:this={ref}
47
+ class={cx('ldt-brand', className)}
48
+ {...rest as HTMLAttributes<HTMLSpanElement>}>{@render content()}</span
35
49
  >{/if}
@@ -1,15 +1,19 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  import type { HTMLAnchorAttributes, HTMLAttributes } from 'svelte/elements';
3
- type Props = HTMLAnchorAttributes & HTMLAttributes<HTMLSpanElement> & {
3
+ interface CommonProps {
4
4
  name: string;
5
5
  /** Secondary line, e.g. an environment or version marker. */
6
6
  meta?: string;
7
- /** Renders an `<a>` instead of a `<span>`. */
8
- href?: string;
9
7
  mark?: Snippet;
10
8
  class?: string;
11
9
  ref?: HTMLElement | null;
12
- };
10
+ }
11
+ type Props = CommonProps & ((Omit<HTMLAnchorAttributes, 'class'> & {
12
+ /** Renders an `<a>` instead of a `<span>`. */
13
+ href: string;
14
+ }) | (Omit<HTMLAttributes<HTMLSpanElement>, 'class'> & {
15
+ href?: never;
16
+ }));
13
17
  declare const Brand: import("svelte").Component<Props, {}, "ref">;
14
18
  type Brand = ReturnType<typeof Brand>;
15
19
  export default Brand;
@@ -0,0 +1,53 @@
1
+ <script lang="ts">
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ import type { Crumb } from '../types.js';
4
+ import { cx } from '../utils.js';
5
+
6
+ interface Props extends HTMLAttributes<HTMLElement> {
7
+ /** Ordered trail, root first. The last entry is the current page and is never a link. */
8
+ items: Crumb[];
9
+ /** Accessible name of the landmark. Several trails on one page need distinct names. */
10
+ label?: string;
11
+ /**
12
+ * Separator drawn between crumbs. Decorative — it lives in a `::before` on the list item, so
13
+ * it is never read out or copied with the text.
14
+ */
15
+ separator?: string;
16
+ class?: string;
17
+ ref?: HTMLElement | null;
18
+ }
19
+
20
+ let {
21
+ items,
22
+ label = 'Breadcrumb',
23
+ separator = '/',
24
+ class: className,
25
+ ref = $bindable(null),
26
+ ...rest
27
+ }: Props = $props();
28
+ </script>
29
+
30
+ <nav
31
+ bind:this={ref}
32
+ class={cx('ldt-breadcrumbs', className)}
33
+ aria-label={label}
34
+ style={`--ldt-breadcrumb-separator: ${JSON.stringify(separator)}`}
35
+ {...rest}
36
+ >
37
+ <ol class="ldt-breadcrumbs__list">
38
+ {#each items as item, index (item.label)}
39
+ {@const current = index === items.length - 1}
40
+ <li class="ldt-breadcrumbs__item">
41
+ {#if item.href && !current}
42
+ <a class="ldt-breadcrumbs__link" href={item.href}>{item.label}</a>
43
+ {:else}
44
+ <!-- The current page is not a link, and says so. Marking it up as one that goes
45
+ nowhere is the most common breadcrumb defect. -->
46
+ <span class="ldt-breadcrumbs__current" aria-current={current ? 'page' : undefined}
47
+ >{item.label}</span
48
+ >
49
+ {/if}
50
+ </li>
51
+ {/each}
52
+ </ol>
53
+ </nav>
@@ -0,0 +1,18 @@
1
+ import type { HTMLAttributes } from 'svelte/elements';
2
+ import type { Crumb } from '../types.js';
3
+ interface Props extends HTMLAttributes<HTMLElement> {
4
+ /** Ordered trail, root first. The last entry is the current page and is never a link. */
5
+ items: Crumb[];
6
+ /** Accessible name of the landmark. Several trails on one page need distinct names. */
7
+ label?: string;
8
+ /**
9
+ * Separator drawn between crumbs. Decorative — it lives in a `::before` on the list item, so
10
+ * it is never read out or copied with the text.
11
+ */
12
+ separator?: string;
13
+ class?: string;
14
+ ref?: HTMLElement | null;
15
+ }
16
+ declare const Breadcrumbs: import("svelte").Component<Props, {}, "ref">;
17
+ type Breadcrumbs = ReturnType<typeof Breadcrumbs>;
18
+ export default Breadcrumbs;
@@ -4,22 +4,32 @@
4
4
  import type { ActionVariant, ControlSize } from '../types.js';
5
5
  import { cx } from '../utils.js';
6
6
 
7
- type Props = HTMLButtonAttributes &
8
- HTMLAnchorAttributes & {
9
- variant?: ActionVariant;
10
- size?: ControlSize;
11
- /** Renders an `<a>` styled as a button. */
12
- href?: string;
13
- disabled?: boolean;
14
- loading?: boolean;
15
- /** Visible text swapped in while loading. */
16
- loadingText?: string;
17
- /** Announced while loading when there is no visible `loadingText`. */
18
- loadingLabel?: string;
19
- class?: string;
20
- children: Snippet;
21
- ref?: HTMLButtonElement | HTMLAnchorElement | null;
22
- };
7
+ interface CommonProps {
8
+ variant?: ActionVariant;
9
+ size?: ControlSize;
10
+ disabled?: boolean;
11
+ loading?: boolean;
12
+ /** Visible text swapped in while loading. */
13
+ loadingText?: string;
14
+ /** Announced while loading when there is no visible `loadingText`. */
15
+ loadingLabel?: string;
16
+ class?: string;
17
+ children: Snippet;
18
+ ref?: HTMLButtonElement | HTMLAnchorElement | null;
19
+ }
20
+
21
+ // Discriminated on `href`: with it the component renders an `<a>` and accepts anchor
22
+ // attributes; without it, a `<button>` and button attributes. Button-only attributes
23
+ // (`formaction`, `type`, …) on an anchor are a type error instead of silent junk.
24
+ type Props = CommonProps &
25
+ (
26
+ | (Omit<HTMLAnchorAttributes, 'class' | 'type'> & {
27
+ /** Renders an `<a>` styled as a button. */
28
+ href: string;
29
+ type?: never;
30
+ })
31
+ | (Omit<HTMLButtonAttributes, 'class'> & { href?: never })
32
+ );
23
33
 
24
34
  let {
25
35
  variant = 'default',
@@ -38,7 +48,12 @@
38
48
  }: Props = $props();
39
49
 
40
50
  const classes = $derived(
41
- cx('ldt-button', `ldt-button--${variant}`, size !== 'md' && `ldt-button--${size}`, className)
51
+ cx(
52
+ 'ldt-button',
53
+ variant !== 'default' && `ldt-button--${variant}`,
54
+ size !== 'md' && `ldt-button--${size}`,
55
+ className
56
+ )
42
57
  );
43
58
  const inert = $derived(disabled || loading);
44
59
 
@@ -47,7 +62,8 @@
47
62
  event.preventDefault();
48
63
  return;
49
64
  }
50
- onclick?.(event as MouseEvent & { currentTarget: EventTarget & HTMLButtonElement });
65
+ // The union collapses the handler's currentTarget; the branch that rendered us fixes it.
66
+ (onclick as ((event: MouseEvent) => void) | undefined)?.(event);
51
67
  }
52
68
  </script>
53
69
 
@@ -69,17 +85,23 @@
69
85
  aria-disabled={inert || undefined}
70
86
  aria-busy={loading || undefined}
71
87
  tabindex={inert ? -1 : undefined}
72
- {...rest}
88
+ {...rest as HTMLAnchorAttributes}
73
89
  onclick={handleClick}>{@render content()}</a
74
90
  >
75
91
  {:else}
92
+ <!--
93
+ `loading` uses `aria-disabled` + a click guard instead of native `disabled`: disabling
94
+ the control mid-interaction would drop keyboard focus to <body> and can hide the busy
95
+ announcement from assistive tech. Explicit `disabled` keeps native semantics.
96
+ -->
76
97
  <button
77
98
  bind:this={ref}
78
99
  {type}
79
100
  class={classes}
80
- disabled={inert}
101
+ disabled={disabled || undefined}
102
+ aria-disabled={loading && !disabled ? true : undefined}
81
103
  aria-busy={loading || undefined}
82
- {...rest}
104
+ {...rest as HTMLButtonAttributes}
83
105
  onclick={handleClick}>{@render content()}</button
84
106
  >
85
107
  {/if}
@@ -1,11 +1,9 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
3
3
  import type { ActionVariant, ControlSize } from '../types.js';
4
- type Props = HTMLButtonAttributes & HTMLAnchorAttributes & {
4
+ interface CommonProps {
5
5
  variant?: ActionVariant;
6
6
  size?: ControlSize;
7
- /** Renders an `<a>` styled as a button. */
8
- href?: string;
9
7
  disabled?: boolean;
10
8
  loading?: boolean;
11
9
  /** Visible text swapped in while loading. */
@@ -15,7 +13,14 @@ type Props = HTMLButtonAttributes & HTMLAnchorAttributes & {
15
13
  class?: string;
16
14
  children: Snippet;
17
15
  ref?: HTMLButtonElement | HTMLAnchorElement | null;
18
- };
16
+ }
17
+ type Props = CommonProps & ((Omit<HTMLAnchorAttributes, 'class' | 'type'> & {
18
+ /** Renders an `<a>` styled as a button. */
19
+ href: string;
20
+ type?: never;
21
+ }) | (Omit<HTMLButtonAttributes, 'class'> & {
22
+ href?: never;
23
+ }));
19
24
  declare const Button: import("svelte").Component<Props, {}, "ref">;
20
25
  type Button = ReturnType<typeof Button>;
21
26
  export default Button;