@orochibraru/ui 1.0.0 → 1.0.1

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 (112) hide show
  1. package/README.md +84 -3
  2. package/dist/components/blocks/accent-toggle.svelte +7 -3
  3. package/dist/components/blocks/accent-toggle.svelte.d.ts +1 -1
  4. package/dist/components/blocks/bottom-nav.svelte +1 -1
  5. package/dist/components/blocks/command-palette.svelte +268 -0
  6. package/dist/components/blocks/command-palette.svelte.d.ts +37 -0
  7. package/dist/components/blocks/error-page.svelte +23 -7
  8. package/dist/components/blocks/error-page.svelte.d.ts +4 -0
  9. package/dist/components/blocks/fab.svelte +1 -1
  10. package/dist/components/blocks/install-prompt.svelte +14 -8
  11. package/dist/components/blocks/install-prompt.svelte.d.ts +2 -1
  12. package/dist/components/blocks/music-player/labelled-slider.svelte +3 -12
  13. package/dist/components/blocks/music-player/labelled-slider.svelte.d.ts +1 -2
  14. package/dist/components/blocks/music-player.svelte +13 -12
  15. package/dist/components/blocks/otp-field.svelte +77 -33
  16. package/dist/components/blocks/otp-field.svelte.d.ts +7 -0
  17. package/dist/components/blocks/password-input.svelte +56 -16
  18. package/dist/components/blocks/password-input.svelte.d.ts +3 -0
  19. package/dist/components/blocks/stepper/stepper-phone-header.svelte +93 -0
  20. package/dist/components/blocks/stepper/stepper-phone-header.svelte.d.ts +13 -0
  21. package/dist/components/blocks/stepper.svelte +175 -0
  22. package/dist/components/blocks/stepper.svelte.d.ts +30 -0
  23. package/dist/components/blocks/time-range-picker.svelte +229 -0
  24. package/dist/components/blocks/time-range-picker.svelte.d.ts +33 -0
  25. package/dist/components/blocks/time-series-chart/data-table.svelte +43 -0
  26. package/dist/components/blocks/time-series-chart/data-table.svelte.d.ts +15 -0
  27. package/dist/components/blocks/time-series-chart/legend.svelte +23 -0
  28. package/dist/components/blocks/time-series-chart/legend.svelte.d.ts +10 -0
  29. package/dist/components/blocks/time-series-chart/marks.svelte +118 -0
  30. package/dist/components/blocks/time-series-chart/marks.svelte.d.ts +29 -0
  31. package/dist/components/blocks/time-series-chart/messages.d.ts +13 -0
  32. package/dist/components/blocks/time-series-chart/messages.js +12 -0
  33. package/dist/components/blocks/time-series-chart/readout.svelte +44 -0
  34. package/dist/components/blocks/time-series-chart/readout.svelte.d.ts +15 -0
  35. package/dist/components/blocks/time-series-chart/scale.d.ts +24 -0
  36. package/dist/components/blocks/time-series-chart/scale.js +136 -0
  37. package/dist/components/blocks/time-series-chart/time.d.ts +7 -0
  38. package/dist/components/blocks/time-series-chart/time.js +96 -0
  39. package/dist/components/blocks/time-series-chart.svelte +492 -0
  40. package/dist/components/blocks/time-series-chart.svelte.d.ts +18 -0
  41. package/dist/components/blocks/top-loading-bar.svelte +1 -1
  42. package/dist/components/blocks/video-player/center-controls.svelte +7 -7
  43. package/dist/components/blocks/video-player/control-bar.svelte +41 -35
  44. package/dist/components/blocks/video-player/control-button.svelte +1 -1
  45. package/dist/components/blocks/video-player/option-menu.svelte +1 -1
  46. package/dist/components/blocks/video-player/scrub-bar.svelte +39 -12
  47. package/dist/components/blocks/video-player/scrub-bar.svelte.d.ts +2 -0
  48. package/dist/components/blocks/video-player.svelte +2 -2
  49. package/dist/components/kit/accordion.svelte +70 -0
  50. package/dist/components/kit/accordion.svelte.d.ts +37 -0
  51. package/dist/components/kit/avatar-group.svelte +84 -0
  52. package/dist/components/kit/avatar-group.svelte.d.ts +14 -0
  53. package/dist/components/kit/bar-list.svelte +104 -0
  54. package/dist/components/kit/bar-list.svelte.d.ts +22 -0
  55. package/dist/components/kit/breadcrumbs.svelte +102 -0
  56. package/dist/components/kit/breadcrumbs.svelte.d.ts +14 -0
  57. package/dist/components/kit/checkbox-list.svelte +98 -0
  58. package/dist/components/kit/checkbox-list.svelte.d.ts +15 -0
  59. package/dist/components/kit/checkbox.svelte +84 -0
  60. package/dist/components/kit/checkbox.svelte.d.ts +8 -0
  61. package/dist/components/kit/confirm-dialog.svelte +127 -0
  62. package/dist/components/kit/confirm-dialog.svelte.d.ts +19 -0
  63. package/dist/components/kit/data-table.svelte +280 -0
  64. package/dist/components/kit/data-table.svelte.d.ts +46 -0
  65. package/dist/components/kit/date-field.svelte +210 -0
  66. package/dist/components/kit/date-field.svelte.d.ts +14 -0
  67. package/dist/components/kit/field-frame.svelte +71 -0
  68. package/dist/components/kit/field-frame.svelte.d.ts +26 -0
  69. package/dist/components/kit/fieldset-frame.svelte +69 -0
  70. package/dist/components/kit/fieldset-frame.svelte.d.ts +14 -0
  71. package/dist/components/kit/file-dropzone/files.d.ts +14 -0
  72. package/dist/components/kit/file-dropzone/files.js +69 -0
  73. package/dist/components/kit/file-dropzone.svelte +253 -0
  74. package/dist/components/kit/file-dropzone.svelte.d.ts +25 -0
  75. package/dist/components/kit/form.svelte +335 -0
  76. package/dist/components/kit/form.svelte.d.ts +61 -0
  77. package/dist/components/kit/input.svelte +73 -0
  78. package/dist/components/kit/input.svelte.d.ts +9 -0
  79. package/dist/components/kit/menu.svelte +163 -0
  80. package/dist/components/kit/menu.svelte.d.ts +28 -0
  81. package/dist/components/kit/pagination.svelte +134 -0
  82. package/dist/components/kit/pagination.svelte.d.ts +17 -0
  83. package/dist/components/kit/radio-list.svelte +131 -0
  84. package/dist/components/kit/radio-list.svelte.d.ts +15 -0
  85. package/dist/components/kit/segmented-control/segment.svelte +47 -0
  86. package/dist/components/kit/segmented-control/segment.svelte.d.ts +11 -0
  87. package/dist/components/kit/segmented-control.svelte +70 -0
  88. package/dist/components/kit/segmented-control.svelte.d.ts +16 -0
  89. package/dist/components/kit/select.svelte +244 -0
  90. package/dist/components/kit/select.svelte.d.ts +26 -0
  91. package/dist/components/kit/status-badge.svelte +54 -0
  92. package/dist/components/kit/status-badge.svelte.d.ts +31 -0
  93. package/dist/components/kit/switch.svelte +84 -0
  94. package/dist/components/kit/switch.svelte.d.ts +8 -0
  95. package/dist/components/kit/tabs.svelte +86 -0
  96. package/dist/components/kit/tabs.svelte.d.ts +20 -0
  97. package/dist/components/kit/textarea.svelte +92 -0
  98. package/dist/components/kit/textarea.svelte.d.ts +11 -0
  99. package/dist/components/ui/drawer/drawer-content.svelte +1 -1
  100. package/dist/components/ui/input-group/input-group-input.svelte.d.ts +1 -1
  101. package/dist/components/ui/sidebar/sidebar-input.svelte.d.ts +1 -1
  102. package/dist/components/ui/sonner/sonner.svelte +2 -4
  103. package/dist/mcp-content.json +1 -1
  104. package/dist/mcp.js +11 -11
  105. package/dist/server/hooks.d.ts +9 -0
  106. package/dist/server/hooks.js +41 -0
  107. package/dist/server/logger.d.ts +39 -0
  108. package/dist/server/logger.js +144 -0
  109. package/dist/server/telemetry.d.ts +23 -0
  110. package/dist/server/telemetry.js +82 -0
  111. package/dist/theme.css +18 -6
  112. package/package.json +40 -1
@@ -0,0 +1,73 @@
1
+ <script lang="ts">
2
+ import type { HTMLInputAttributes } from "svelte/elements";
3
+
4
+ import PasswordInput from "../blocks/password-input.svelte";
5
+ import { Input } from "../ui/input/index.js";
6
+
7
+ import FieldFrame, { errorList, type FieldProps } from "./field-frame.svelte";
8
+
9
+ type Props = FieldProps &
10
+ Omit<HTMLInputAttributes, "value" | "files" | "class" | "id" | "name"> & {
11
+ value?: string | number;
12
+ };
13
+
14
+ let {
15
+ value = $bindable(""),
16
+ name,
17
+ label,
18
+ description,
19
+ error,
20
+ required = false,
21
+ disabled = false,
22
+ id,
23
+ class: className,
24
+ type = "text",
25
+ ...rest
26
+ }: Props = $props();
27
+
28
+ const generatedId = $props.id();
29
+ const inputId = $derived(id ?? generatedId);
30
+ const invalid = $derived(errorList(error).length > 0);
31
+ const describedBy = $derived(
32
+ invalid
33
+ ? `${inputId}-error`
34
+ : description
35
+ ? `${inputId}-description`
36
+ : undefined,
37
+ );
38
+ </script>
39
+
40
+ <FieldFrame
41
+ id={inputId}
42
+ {label}
43
+ {description}
44
+ {error}
45
+ {required}
46
+ class={className}
47
+ >
48
+ {#if type === "password"}
49
+ <PasswordInput
50
+ {...rest}
51
+ id={inputId}
52
+ {name}
53
+ {required}
54
+ {disabled}
55
+ aria-invalid={invalid || undefined}
56
+ aria-describedby={describedBy}
57
+ bind:value={() => String(value ?? ""), (next) => (value = next)}
58
+ />
59
+ {:else}
60
+ <Input
61
+ {...rest}
62
+ id={inputId}
63
+ {name}
64
+ type={type ?? "text"}
65
+ {required}
66
+ {disabled}
67
+ aria-invalid={invalid || undefined}
68
+ aria-describedby={describedBy}
69
+ bind:value
70
+ class="h-11"
71
+ />
72
+ {/if}
73
+ </FieldFrame>
@@ -0,0 +1,9 @@
1
+ import type { HTMLInputAttributes } from "svelte/elements";
2
+ import { Input } from "../ui/input/index.js";
3
+ import { type FieldProps } from "./field-frame.svelte";
4
+ type Props = FieldProps & Omit<HTMLInputAttributes, "value" | "files" | "class" | "id" | "name"> & {
5
+ value?: string | number;
6
+ };
7
+ declare const Input: import("svelte").Component<Props, {}, "value">;
8
+ type Input = ReturnType<typeof Input>;
9
+ export default Input;
@@ -0,0 +1,163 @@
1
+ <script lang="ts" module>
2
+ import type { Component, Snippet } from "svelte";
3
+
4
+ export interface MenuItem {
5
+ label: string;
6
+ icon?: Component<{ class?: string }>;
7
+ href?: string;
8
+ onSelect?: () => void;
9
+ shortcut?: string;
10
+ destructive?: boolean;
11
+ disabled?: boolean;
12
+ }
13
+
14
+ export type MenuEntry = MenuItem | "separator";
15
+
16
+ export type MenuTrigger = Snippet<[{ props: Record<string, unknown> }]>;
17
+ </script>
18
+
19
+ <script lang="ts">
20
+ import EllipsisIcon from "@lucide/svelte/icons/ellipsis";
21
+ import { MediaQuery } from "svelte/reactivity";
22
+
23
+ import IconButton from "../blocks/icon-button.svelte";
24
+ import { Button } from "../ui/button/index.js";
25
+ import * as Drawer from "../ui/drawer/index.js";
26
+ import * as DropdownMenu from "../ui/dropdown-menu/index.js";
27
+ import { Separator } from "../ui/separator/index.js";
28
+ import { cn } from "../../utils.js";
29
+
30
+ const defaultMessages = {
31
+ label: "More actions",
32
+ };
33
+
34
+ interface Props {
35
+ items: MenuEntry[];
36
+ trigger?: MenuTrigger;
37
+ label?: string;
38
+ title?: string;
39
+ align?: "start" | "center" | "end";
40
+ messages?: Partial<typeof defaultMessages>;
41
+ }
42
+
43
+ const {
44
+ items,
45
+ trigger,
46
+ label,
47
+ title,
48
+ align = "end",
49
+ messages = {},
50
+ }: Props = $props();
51
+
52
+ const desktop = new MediaQuery("(min-width: 768px)");
53
+ const text = $derived({ ...defaultMessages, ...messages });
54
+ const accessibleName = $derived(label ?? text.label);
55
+
56
+ let open = $state(false);
57
+
58
+ function select(item: MenuItem) {
59
+ open = false;
60
+ item.onSelect?.();
61
+ }
62
+ </script>
63
+
64
+ {#snippet defaultTrigger(props: Record<string, unknown>)}
65
+ <IconButton
66
+ {...props}
67
+ label={accessibleName}
68
+ icon={EllipsisIcon}
69
+ class="text-muted-foreground"
70
+ />
71
+ {/snippet}
72
+
73
+ {#snippet itemContent(item: MenuItem)}
74
+ {#if item.icon}
75
+ <item.icon class={desktop.current ? "size-4" : "size-5"} />
76
+ {/if}
77
+ <span class="truncate">{item.label}</span>
78
+ {/snippet}
79
+
80
+ {#if desktop.current}
81
+ <DropdownMenu.Root>
82
+ <DropdownMenu.Trigger>
83
+ {#snippet child({ props })}
84
+ {#if trigger}
85
+ {@render trigger({ props })}
86
+ {:else}
87
+ {@render defaultTrigger(props)}
88
+ {/if}
89
+ {/snippet}
90
+ </DropdownMenu.Trigger>
91
+ <DropdownMenu.Content {align} class="min-w-48">
92
+ {#each items as item, index (index)}
93
+ {#if item === "separator"}
94
+ <DropdownMenu.Separator />
95
+ {:else if item.href && !item.disabled}
96
+ <DropdownMenu.Item
97
+ variant={item.destructive ? "destructive" : "default"}
98
+ onSelect={() => select(item)}
99
+ >
100
+ {#snippet child({ props })}
101
+ <a href={item.href} {...props}>
102
+ {@render itemContent(item)}
103
+ {#if item.shortcut}
104
+ <DropdownMenu.Shortcut>{item.shortcut}</DropdownMenu.Shortcut>
105
+ {/if}
106
+ </a>
107
+ {/snippet}
108
+ </DropdownMenu.Item>
109
+ {:else}
110
+ <DropdownMenu.Item
111
+ variant={item.destructive ? "destructive" : "default"}
112
+ disabled={item.disabled}
113
+ onSelect={() => select(item)}
114
+ >
115
+ {@render itemContent(item)}
116
+ {#if item.shortcut}
117
+ <DropdownMenu.Shortcut>{item.shortcut}</DropdownMenu.Shortcut>
118
+ {/if}
119
+ </DropdownMenu.Item>
120
+ {/if}
121
+ {/each}
122
+ </DropdownMenu.Content>
123
+ </DropdownMenu.Root>
124
+ {:else}
125
+ {@const props = {
126
+ type: "button",
127
+ "aria-haspopup": "dialog",
128
+ "aria-expanded": open,
129
+ onclick: () => (open = true),
130
+ }}
131
+ {#if trigger}
132
+ {@render trigger({ props })}
133
+ {:else}
134
+ {@render defaultTrigger(props)}
135
+ {/if}
136
+ <Drawer.Root bind:open>
137
+ <Drawer.Content>
138
+ <Drawer.Header class={cn("px-5 text-left", !title && "sr-only")}>
139
+ <Drawer.Title class="truncate">{title ?? accessibleName}</Drawer.Title>
140
+ </Drawer.Header>
141
+ <div class="flex flex-col gap-1 px-3 pb-4">
142
+ {#each items as item, index (index)}
143
+ {#if item === "separator"}
144
+ <Separator class="my-1" />
145
+ {:else}
146
+ <Button
147
+ variant="ghost"
148
+ href={item.href}
149
+ disabled={item.disabled}
150
+ class={cn(
151
+ "h-12 justify-start gap-3 rounded-xl px-3 text-base",
152
+ item.destructive && "text-destructive hover:text-destructive",
153
+ )}
154
+ onclick={() => select(item)}
155
+ >
156
+ {@render itemContent(item)}
157
+ </Button>
158
+ {/if}
159
+ {/each}
160
+ </div>
161
+ </Drawer.Content>
162
+ </Drawer.Root>
163
+ {/if}
@@ -0,0 +1,28 @@
1
+ import type { Component, Snippet } from "svelte";
2
+ export interface MenuItem {
3
+ label: string;
4
+ icon?: Component<{
5
+ class?: string;
6
+ }>;
7
+ href?: string;
8
+ onSelect?: () => void;
9
+ shortcut?: string;
10
+ destructive?: boolean;
11
+ disabled?: boolean;
12
+ }
13
+ export type MenuEntry = MenuItem | "separator";
14
+ export type MenuTrigger = Snippet<[{
15
+ props: Record<string, unknown>;
16
+ }]>;
17
+ declare const Menu: Component<{
18
+ items: MenuEntry[];
19
+ trigger?: MenuTrigger;
20
+ label?: string;
21
+ title?: string;
22
+ align?: "start" | "center" | "end";
23
+ messages?: Partial<{
24
+ label: string;
25
+ }>;
26
+ }, {}, "">;
27
+ type Menu = ReturnType<typeof Menu>;
28
+ export default Menu;
@@ -0,0 +1,134 @@
1
+ <script lang="ts">
2
+ import ChevronLeftIcon from "@lucide/svelte/icons/chevron-left";
3
+ import ChevronRightIcon from "@lucide/svelte/icons/chevron-right";
4
+ import EllipsisIcon from "@lucide/svelte/icons/ellipsis";
5
+ import { MediaQuery } from "svelte/reactivity";
6
+
7
+ import { Button } from "../ui/button/index.js";
8
+ import { cn } from "../../utils.js";
9
+
10
+ const defaultMessages = {
11
+ label: "Pagination",
12
+ previous: "Previous",
13
+ next: "Next",
14
+ pageOf: (page: number, pageCount: number) => `Page ${page} of ${pageCount}`,
15
+ range: (first: number, last: number, total: number) =>
16
+ `${first}–${last} of ${total}`,
17
+ };
18
+
19
+ interface Props {
20
+ page?: number;
21
+ total: number;
22
+ perPage?: number;
23
+ onchange?: (page: number) => void;
24
+ href?: (page: number) => string;
25
+ class?: string;
26
+ messages?: Partial<typeof defaultMessages>;
27
+ }
28
+
29
+ let {
30
+ page = $bindable(1),
31
+ total,
32
+ perPage = 20,
33
+ onchange,
34
+ href,
35
+ class: className,
36
+ messages = {},
37
+ }: Props = $props();
38
+
39
+ const desktop = new MediaQuery("(min-width: 768px)");
40
+ const text = $derived({ ...defaultMessages, ...messages });
41
+
42
+ const pageCount = $derived(Math.max(1, Math.ceil(total / perPage)));
43
+ const first = $derived(total === 0 ? 0 : (page - 1) * perPage + 1);
44
+ const last = $derived(Math.min(page * perPage, total));
45
+
46
+ const pages = $derived.by<(number | "gap")[]>(() => {
47
+ if (pageCount <= 7) {
48
+ return Array.from({ length: pageCount }, (_, index) => index + 1);
49
+ }
50
+ if (page <= 4) {
51
+ return [1, 2, 3, 4, 5, "gap", pageCount];
52
+ }
53
+ if (page >= pageCount - 3) {
54
+ return [
55
+ 1,
56
+ "gap",
57
+ ...Array.from({ length: 5 }, (_, index) => pageCount - 4 + index),
58
+ ];
59
+ }
60
+ return [1, "gap", page - 1, page, page + 1, "gap", pageCount];
61
+ });
62
+
63
+ function go(next: number) {
64
+ if (next < 1 || next > pageCount || next === page) {
65
+ return;
66
+ }
67
+ page = next;
68
+ onchange?.(next);
69
+ }
70
+ </script>
71
+
72
+ {#if total > perPage}
73
+ <nav
74
+ aria-label={text.label}
75
+ class={cn("flex w-full items-center justify-between gap-3", className)}
76
+ >
77
+ {#if desktop.current}
78
+ <p class="text-muted-foreground text-sm tabular-nums">
79
+ {text.range(first, last, total)}
80
+ </p>
81
+ {/if}
82
+ <div
83
+ class="flex w-full items-center justify-between gap-1 md:w-auto md:justify-end"
84
+ >
85
+ <Button
86
+ variant="outline"
87
+ class="h-11 rounded-xl md:h-9"
88
+ href={href?.(page - 1)}
89
+ disabled={page <= 1}
90
+ onclick={() => go(page - 1)}
91
+ >
92
+ <ChevronLeftIcon />
93
+ {text.previous}
94
+ </Button>
95
+ {#if desktop.current}
96
+ {#each pages as entry, index (index)}
97
+ {#if entry === "gap"}
98
+ <span
99
+ aria-hidden="true"
100
+ class="text-muted-foreground flex size-9 items-center justify-center"
101
+ >
102
+ <EllipsisIcon class="size-4" />
103
+ </span>
104
+ {:else}
105
+ <Button
106
+ variant={entry === page ? "outline" : "ghost"}
107
+ size="icon"
108
+ class="tabular-nums"
109
+ href={href?.(entry)}
110
+ aria-current={entry === page ? "page" : undefined}
111
+ onclick={() => go(entry)}
112
+ >
113
+ {entry}
114
+ </Button>
115
+ {/if}
116
+ {/each}
117
+ {:else}
118
+ <span class="text-muted-foreground text-sm tabular-nums">
119
+ {text.pageOf(page, pageCount)}
120
+ </span>
121
+ {/if}
122
+ <Button
123
+ variant="outline"
124
+ class="h-11 rounded-xl md:h-9"
125
+ href={href?.(page + 1)}
126
+ disabled={page >= pageCount}
127
+ onclick={() => go(page + 1)}
128
+ >
129
+ {text.next}
130
+ <ChevronRightIcon />
131
+ </Button>
132
+ </div>
133
+ </nav>
134
+ {/if}
@@ -0,0 +1,17 @@
1
+ declare const Pagination: import("svelte").Component<{
2
+ page?: number;
3
+ total: number;
4
+ perPage?: number;
5
+ onchange?: (page: number) => void;
6
+ href?: (page: number) => string;
7
+ class?: string;
8
+ messages?: Partial<{
9
+ label: string;
10
+ previous: string;
11
+ next: string;
12
+ pageOf: (page: number, pageCount: number) => string;
13
+ range: (first: number, last: number, total: number) => string;
14
+ }>;
15
+ }, {}, "page">;
16
+ type Pagination = ReturnType<typeof Pagination>;
17
+ export default Pagination;
@@ -0,0 +1,131 @@
1
+ <script lang="ts">
2
+ import CheckIcon from "@lucide/svelte/icons/check";
3
+ import { RadioGroup as RadioGroupPrimitive } from "bits-ui";
4
+
5
+ import * as RadioGroup from "../ui/radio-group/index.js";
6
+ import { cn } from "../../utils.js";
7
+
8
+ import { errorList, type FieldProps } from "./field-frame.svelte";
9
+ import FieldsetFrame from "./fieldset-frame.svelte";
10
+
11
+ interface Option {
12
+ value: string;
13
+ label: string;
14
+ description?: string;
15
+ disabled?: boolean;
16
+ }
17
+
18
+ type Props = FieldProps & {
19
+ options: readonly Option[];
20
+ value?: string;
21
+ variant?: "list" | "cards";
22
+ };
23
+
24
+ let {
25
+ options,
26
+ value = $bindable(""),
27
+ variant = "list",
28
+ name,
29
+ label,
30
+ description,
31
+ error,
32
+ required = false,
33
+ disabled = false,
34
+ id,
35
+ class: className,
36
+ }: Props = $props();
37
+
38
+ const generatedId = $props.id();
39
+ const listId = $derived(id ?? generatedId);
40
+ const invalid = $derived(errorList(error).length > 0);
41
+ </script>
42
+
43
+ <FieldsetFrame
44
+ id={listId}
45
+ {label}
46
+ {description}
47
+ {error}
48
+ {required}
49
+ {disabled}
50
+ class={className}
51
+ >
52
+ <RadioGroup.Root
53
+ {name}
54
+ {required}
55
+ {disabled}
56
+ bind:value
57
+ aria-labelledby={label ? `${listId}-label` : undefined}
58
+ aria-invalid={invalid || undefined}
59
+ class={variant === "cards"
60
+ ? "gap-2 md:grid-cols-2 xl:grid-cols-3"
61
+ : "flex flex-col gap-0"}
62
+ >
63
+ {#each options as option, index (option.value)}
64
+ {@const optionId = `${listId}-${index}`}
65
+ {@const describedBy = option.description
66
+ ? `${optionId}-description`
67
+ : undefined}
68
+ {#if variant === "cards"}
69
+ <RadioGroupPrimitive.Item
70
+ id={optionId}
71
+ value={option.value}
72
+ disabled={option.disabled}
73
+ aria-labelledby="{optionId}-label"
74
+ aria-describedby={describedBy}
75
+ class={cn(
76
+ "border-border bg-card data-checked:border-primary data-checked:ring-primary/30 focus-visible:ring-ring/50 flex min-h-14 w-full cursor-pointer items-center gap-3 rounded-2xl border p-3 text-left transition-all outline-none data-checked:ring-2 focus-visible:ring-3 disabled:cursor-not-allowed disabled:opacity-50",
77
+ invalid && "border-destructive",
78
+ )}
79
+ >
80
+ {#snippet children({ checked })}
81
+ <span class="min-w-0 flex-1">
82
+ <span id="{optionId}-label" class="block text-sm font-medium"
83
+ >{option.label}</span
84
+ >
85
+ {#if option.description}
86
+ <span
87
+ id="{optionId}-description"
88
+ class="text-muted-foreground block text-sm"
89
+ >{option.description}</span
90
+ >
91
+ {/if}
92
+ </span>
93
+ <CheckIcon
94
+ class={cn(
95
+ "text-primary size-5 shrink-0",
96
+ !checked && "invisible",
97
+ )}
98
+ aria-hidden="true"
99
+ />
100
+ {/snippet}
101
+ </RadioGroupPrimitive.Item>
102
+ {:else}
103
+ <div
104
+ class="group/row relative flex min-h-11 items-start gap-3 py-2.5 has-disabled:opacity-50"
105
+ >
106
+ <RadioGroup.Item
107
+ id={optionId}
108
+ value={option.value}
109
+ disabled={option.disabled}
110
+ aria-describedby={describedBy}
111
+ class={cn("z-10 size-5", invalid && "border-destructive")}
112
+ />
113
+ <span class="min-w-0 flex-1">
114
+ <label
115
+ for={optionId}
116
+ class="block cursor-pointer text-sm leading-snug font-medium after:absolute after:inset-0 group-has-disabled/row:cursor-not-allowed"
117
+ >{option.label}</label
118
+ >
119
+ {#if option.description}
120
+ <span
121
+ id="{optionId}-description"
122
+ class="text-muted-foreground mt-0.5 block text-sm"
123
+ >{option.description}</span
124
+ >
125
+ {/if}
126
+ </span>
127
+ </div>
128
+ {/if}
129
+ {/each}
130
+ </RadioGroup.Root>
131
+ </FieldsetFrame>
@@ -0,0 +1,15 @@
1
+ import { type FieldProps } from "./field-frame.svelte";
2
+ interface Option {
3
+ value: string;
4
+ label: string;
5
+ description?: string;
6
+ disabled?: boolean;
7
+ }
8
+ type Props = FieldProps & {
9
+ options: readonly Option[];
10
+ value?: string;
11
+ variant?: "list" | "cards";
12
+ };
13
+ declare const RadioList: import("svelte").Component<Props, {}, "value">;
14
+ type RadioList = ReturnType<typeof RadioList>;
15
+ export default RadioList;
@@ -0,0 +1,47 @@
1
+ <script lang="ts">
2
+ import type { Component } from "svelte";
3
+
4
+ import * as ToggleGroup from "../../ui/toggle-group/index.js";
5
+ import * as Tooltip from "../../ui/tooltip/index.js";
6
+
7
+ interface Props {
8
+ value: string;
9
+ label: string;
10
+ icon?: Component<{ class?: string }>;
11
+ }
12
+
13
+ const { value, label, icon: Icon }: Props = $props();
14
+
15
+ let available = $state(0);
16
+ let needed = $state(0);
17
+ const fits = $derived(!Icon || needed <= available);
18
+ </script>
19
+
20
+ <Tooltip.Root disabled={fits}>
21
+ <Tooltip.Trigger>
22
+ {#snippet child({ props })}
23
+ <ToggleGroup.Item
24
+ {...props}
25
+ {value}
26
+ aria-label={label}
27
+ class="bg-secondary data-[state=on]:bg-primary data-[state=on]:text-primary-foreground relative h-11 min-w-0 flex-1 overflow-hidden rounded-xl px-3"
28
+ >
29
+ <span
30
+ class="flex min-w-0 flex-1 items-center justify-center gap-2"
31
+ bind:clientWidth={available}
32
+ >
33
+ {#if Icon}<Icon class="shrink-0" />{/if}
34
+ {#if fits}<span class="truncate">{label}</span>{/if}
35
+ </span>
36
+ <span
37
+ class="invisible absolute flex items-center gap-2 whitespace-nowrap"
38
+ aria-hidden="true"
39
+ bind:clientWidth={needed}
40
+ >
41
+ {#if Icon}<Icon class="shrink-0" />{/if}{label}
42
+ </span>
43
+ </ToggleGroup.Item>
44
+ {/snippet}
45
+ </Tooltip.Trigger>
46
+ <Tooltip.Content>{label}</Tooltip.Content>
47
+ </Tooltip.Root>
@@ -0,0 +1,11 @@
1
+ import type { Component } from "svelte";
2
+ interface Props {
3
+ value: string;
4
+ label: string;
5
+ icon?: Component<{
6
+ class?: string;
7
+ }>;
8
+ }
9
+ declare const Segment: Component<Props, {}, "">;
10
+ type Segment = ReturnType<typeof Segment>;
11
+ export default Segment;
@@ -0,0 +1,70 @@
1
+ <script lang="ts">
2
+ import type { Component } from "svelte";
3
+
4
+ import * as ToggleGroup from "../ui/toggle-group/index.js";
5
+ import * as Tooltip from "../ui/tooltip/index.js";
6
+
7
+ import { errorList, type FieldProps } from "./field-frame.svelte";
8
+ import FieldsetFrame from "./fieldset-frame.svelte";
9
+ import Segment from "./segmented-control/segment.svelte";
10
+
11
+ interface Option {
12
+ value: string;
13
+ label: string;
14
+ icon?: Component<{ class?: string }>;
15
+ }
16
+
17
+ type Props = FieldProps & {
18
+ options: readonly Option[];
19
+ value?: string;
20
+ };
21
+
22
+ let {
23
+ options,
24
+ value = $bindable(""),
25
+ name,
26
+ label,
27
+ description,
28
+ error,
29
+ required = false,
30
+ disabled = false,
31
+ id,
32
+ class: className,
33
+ }: Props = $props();
34
+
35
+ const generatedId = $props.id();
36
+ const controlId = $derived(id ?? generatedId);
37
+ const invalid = $derived(errorList(error).length > 0);
38
+ </script>
39
+
40
+ <FieldsetFrame
41
+ id={controlId}
42
+ {label}
43
+ {description}
44
+ {error}
45
+ {required}
46
+ {disabled}
47
+ class={className}
48
+ >
49
+ <Tooltip.Provider>
50
+ <ToggleGroup.Root
51
+ type="single"
52
+ bind:value={
53
+ () => value,
54
+ (next) => {
55
+ if (typeof next === "string" && next) value = next;
56
+ }
57
+ }
58
+ spacing={2}
59
+ {disabled}
60
+ aria-labelledby={label ? `${controlId}-label` : undefined}
61
+ aria-invalid={invalid || undefined}
62
+ class="w-full"
63
+ >
64
+ {#each options as option (option.value)}
65
+ <Segment value={option.value} label={option.label} icon={option.icon} />
66
+ {/each}
67
+ </ToggleGroup.Root>
68
+ </Tooltip.Provider>
69
+ <input type="hidden" {name} {value} {disabled} />
70
+ </FieldsetFrame>