@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
@@ -12,10 +12,21 @@
12
12
  chapters: Chapter[];
13
13
  label: string;
14
14
  onseek: (time: number) => void;
15
+ /** `overlay` over a video, white on dark; `surface` on the theme's own surfaces. */
16
+ variant?: "overlay" | "surface";
15
17
  }
16
18
 
17
- const { currentTime, duration, bufferedEnd, chapters, label, onseek }: Props =
18
- $props();
19
+ const {
20
+ currentTime,
21
+ duration,
22
+ bufferedEnd,
23
+ chapters,
24
+ label,
25
+ onseek,
26
+ variant = "overlay",
27
+ }: Props = $props();
28
+
29
+ const overlay = $derived(variant === "overlay");
19
30
 
20
31
  let track = $state<HTMLDivElement | null>(null);
21
32
  let hoverRatio = $state<number | null>(null);
@@ -41,43 +52,59 @@
41
52
  <div
42
53
  bind:this={track}
43
54
  role="presentation"
44
- class="group/scrub relative flex h-6 items-center"
55
+ class="group/scrub relative flex h-8 items-center"
45
56
  {onpointermove}
46
57
  onpointerleave={() => (hoverRatio = null)}
47
58
  >
48
59
  <div
49
- class="bg-foreground/15 pointer-events-none absolute inset-x-0 h-1.5 rounded-full transition-[height] group-hover/scrub:h-2"
60
+ class="pointer-events-none absolute inset-x-0 h-1.5 rounded-full transition-[height] group-hover/scrub:h-2 {overlay
61
+ ? 'bg-white/15'
62
+ : 'bg-foreground/15'}"
50
63
  ></div>
51
64
  <div
52
- class="bg-foreground/20 pointer-events-none absolute h-1.5 rounded-full transition-[height] group-hover/scrub:h-2"
65
+ class="pointer-events-none absolute h-1.5 rounded-full transition-[height] group-hover/scrub:h-2 {overlay
66
+ ? 'bg-white/25'
67
+ : 'bg-foreground/20'}"
53
68
  style:width="{ratio(bufferedEnd) * 100}%"
54
69
  ></div>
55
70
  <div
56
- class="bg-primary glow pointer-events-none absolute h-1.5 rounded-full transition-[height] group-hover/scrub:h-2"
71
+ class="pointer-events-none absolute h-1.5 rounded-full transition-[height] group-hover/scrub:h-2 {overlay
72
+ ? 'from-primary/60 to-primary bg-linear-to-r shadow-[0_0_12px_var(--color-primary)]'
73
+ : 'bg-primary glow'}"
57
74
  style:width="{ratio(currentTime) * 100}%"
58
75
  ></div>
59
76
  {#each chapters as chapter (chapter.start)}
60
77
  {#if chapter.start > 0 && chapter.start < duration}
61
78
  <div
62
- class="bg-background pointer-events-none absolute h-2.5 w-0.5 -translate-x-1/2 rounded-full"
79
+ class="pointer-events-none absolute h-3 w-0.5 -translate-x-1/2 rounded-full {overlay
80
+ ? 'bg-black/70'
81
+ : 'bg-background'}"
63
82
  style:left="{ratio(chapter.start) * 100}%"
64
83
  ></div>
65
84
  {/if}
66
85
  {/each}
67
86
  <div
68
- class="bg-primary ring-background pointer-events-none absolute size-3.5 -translate-x-1/2 rounded-full shadow-md ring-2 transition-transform group-focus-within/scrub:scale-125 group-hover/scrub:scale-125"
87
+ class="pointer-events-none absolute size-3.5 -translate-x-1/2 rounded-full shadow-md transition-transform group-focus-within/scrub:scale-125 group-hover/scrub:scale-125 {overlay
88
+ ? 'ring-primary/40 bg-white ring-4'
89
+ : 'bg-primary ring-background ring-2'}"
69
90
  style:left="{ratio(currentTime) * 100}%"
70
91
  ></div>
71
92
  {#if hoverRatio !== null && duration > 0}
72
93
  <div
73
- class="bg-popover text-popover-foreground pointer-events-none absolute bottom-full mb-2 flex -translate-x-1/2 flex-col items-center rounded-lg px-2 py-1 text-xs font-medium tabular shadow-md"
94
+ class="pointer-events-none absolute bottom-full mb-2 flex -translate-x-1/2 flex-col items-center px-2 py-1 text-xs font-medium tabular {overlay
95
+ ? 'rounded-md bg-black/80 text-white ring-1 ring-white/10 backdrop-blur-md'
96
+ : 'bg-popover text-popover-foreground rounded-lg shadow-md'}"
74
97
  style:left="{hoverRatio * 100}%"
75
98
  >
76
99
  {#if hovered?.title}
77
100
  <span class="max-w-48 truncate font-semibold">{hovered.title}</span>
78
101
  {/if}
79
- <span class={hovered?.title ? "text-muted-foreground" : undefined}
80
- >{formatTime(hoverRatio * duration)}</span
102
+ <span
103
+ class={hovered?.title
104
+ ? overlay
105
+ ? "text-white/60"
106
+ : "text-muted-foreground"
107
+ : undefined}>{formatTime(hoverRatio * duration)}</span
81
108
  >
82
109
  </div>
83
110
  {/if}
@@ -90,6 +117,6 @@
90
117
  aria-label={label}
91
118
  aria-valuetext="{formatTime(currentTime)} / {formatTime(duration)}"
92
119
  oninput={(event) => onseek(Number(event.currentTarget.value))}
93
- class="relative h-6 w-full cursor-pointer appearance-none bg-transparent focus-visible:outline-none [&::-moz-range-thumb]:size-5 [&::-moz-range-thumb]:appearance-none [&::-moz-range-thumb]:border-0 [&::-moz-range-thumb]:bg-transparent [&::-webkit-slider-thumb]:size-5 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:bg-transparent"
120
+ class="relative h-8 w-full cursor-pointer appearance-none bg-transparent focus-visible:outline-none [&::-moz-range-thumb]:size-5 [&::-moz-range-thumb]:appearance-none [&::-moz-range-thumb]:border-0 [&::-moz-range-thumb]:bg-transparent [&::-webkit-slider-thumb]:size-5 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:bg-transparent"
94
121
  />
95
122
  </div>
@@ -6,6 +6,8 @@ interface Props {
6
6
  chapters: Chapter[];
7
7
  label: string;
8
8
  onseek: (time: number) => void;
9
+ /** `overlay` over a video, white on dark; `surface` on the theme's own surfaces. */
10
+ variant?: "overlay" | "surface";
9
11
  }
10
12
  declare const ScrubBar: import("svelte").Component<Props, {}, "">;
11
13
  type ScrubBar = ReturnType<typeof ScrubBar>;
@@ -255,12 +255,12 @@
255
255
 
256
256
  <div
257
257
  class={cn(
258
- "pointer-events-none absolute inset-0 flex flex-col pt-safe-top pr-safe-right pb-safe-bottom pl-safe-left transition-[opacity,visibility] duration-200",
258
+ "pointer-events-none absolute inset-0 flex flex-col bg-linear-to-t from-black/60 via-transparent to-black/60 pt-safe-top pr-safe-right pb-safe-bottom pl-safe-left transition-[opacity,visibility] duration-200",
259
259
  controlsShown && !player.failed ? "opacity-100" : "invisible opacity-0",
260
260
  )}
261
261
  >
262
262
  {#if title}
263
- <div class="bg-linear-to-b from-black/60 to-transparent px-4 pt-3 pb-8">
263
+ <div class="px-5 pt-4">
264
264
  <p class="truncate font-semibold text-white drop-shadow">
265
265
  {title}
266
266
  </p>
@@ -0,0 +1,70 @@
1
+ <script lang="ts" module>
2
+ export interface AccordionEntry {
3
+ title: string;
4
+ content?: string;
5
+ value?: string;
6
+ }
7
+ </script>
8
+
9
+ <script lang="ts" generics="Item extends AccordionEntry">
10
+ import type { Snippet } from "svelte";
11
+
12
+ import * as Accordion from "../ui/accordion/index.js";
13
+ import { cn } from "../../utils.js";
14
+
15
+ interface Props {
16
+ items: Item[];
17
+ type?: "single" | "multiple";
18
+ value?: string | string[];
19
+ children?: Snippet<[Item]>;
20
+ class?: string;
21
+ }
22
+
23
+ let {
24
+ items,
25
+ type = "single",
26
+ value = $bindable(),
27
+ children,
28
+ class: className,
29
+ }: Props = $props();
30
+ </script>
31
+
32
+ {#snippet entries()}
33
+ {#each items as item, index (index)}
34
+ <Accordion.Item value={item.value ?? item.title}>
35
+ <Accordion.Trigger>{item.title}</Accordion.Trigger>
36
+ <Accordion.Content>
37
+ {#if children}
38
+ {@render children(item)}
39
+ {:else}
40
+ <p class="text-muted-foreground whitespace-pre-line">
41
+ {item.content}
42
+ </p>
43
+ {/if}
44
+ </Accordion.Content>
45
+ </Accordion.Item>
46
+ {/each}
47
+ {/snippet}
48
+
49
+ {#if type === "multiple"}
50
+ <Accordion.Root
51
+ type="multiple"
52
+ class={cn("w-full", className)}
53
+ bind:value={
54
+ () => (Array.isArray(value) ? value : value ? [value] : []),
55
+ (next) => (value = next)
56
+ }
57
+ >
58
+ {@render entries()}
59
+ </Accordion.Root>
60
+ {:else}
61
+ <Accordion.Root
62
+ type="single"
63
+ class={cn("w-full", className)}
64
+ bind:value={
65
+ () => (typeof value === "string" ? value : ""), (next) => (value = next)
66
+ }
67
+ >
68
+ {@render entries()}
69
+ </Accordion.Root>
70
+ {/if}
@@ -0,0 +1,37 @@
1
+ export interface AccordionEntry {
2
+ title: string;
3
+ content?: string;
4
+ value?: string;
5
+ }
6
+ import type { Snippet } from "svelte";
7
+ import * as Accordion from "../ui/accordion/index.js";
8
+ declare function $$render<Item extends AccordionEntry>(): {
9
+ props: {
10
+ items: Item[];
11
+ type?: "single" | "multiple";
12
+ value?: string | string[];
13
+ children?: Snippet<[Item]>;
14
+ class?: string;
15
+ };
16
+ exports: {};
17
+ bindings: "value";
18
+ slots: {};
19
+ events: {};
20
+ };
21
+ declare class __sveltets_Render<Item extends AccordionEntry> {
22
+ props(): ReturnType<typeof $$render<Item>>['props'];
23
+ events(): ReturnType<typeof $$render<Item>>['events'];
24
+ slots(): ReturnType<typeof $$render<Item>>['slots'];
25
+ bindings(): "value";
26
+ exports(): {};
27
+ }
28
+ interface $$IsomorphicComponent {
29
+ new <Item extends AccordionEntry>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<Item>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<Item>['props']>, ReturnType<__sveltets_Render<Item>['events']>, ReturnType<__sveltets_Render<Item>['slots']>> & {
30
+ $$bindings?: ReturnType<__sveltets_Render<Item>['bindings']>;
31
+ } & ReturnType<__sveltets_Render<Item>['exports']>;
32
+ <Item extends AccordionEntry>(internal: unknown, props: ReturnType<__sveltets_Render<Item>['props']> & {}): ReturnType<__sveltets_Render<Item>['exports']>;
33
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
34
+ }
35
+ declare const Accordion: $$IsomorphicComponent;
36
+ type Accordion<Item extends AccordionEntry> = InstanceType<typeof Accordion<Item>>;
37
+ export default Accordion;
@@ -0,0 +1,84 @@
1
+ <script lang="ts" module>
2
+ export interface AvatarPerson {
3
+ name: string;
4
+ src?: string;
5
+ }
6
+
7
+ export function initials(name: string): string {
8
+ return name
9
+ .trim()
10
+ .split(/\s+/)
11
+ .slice(0, 2)
12
+ .map((word) => word.charAt(0))
13
+ .join("")
14
+ .toUpperCase();
15
+ }
16
+ </script>
17
+
18
+ <script lang="ts">
19
+ import * as Avatar from "../ui/avatar/index.js";
20
+ import * as Tooltip from "../ui/tooltip/index.js";
21
+ import { cn } from "../../utils.js";
22
+
23
+ interface Props {
24
+ people: AvatarPerson[];
25
+ max?: number;
26
+ size?: "sm" | "default" | "lg";
27
+ class?: string;
28
+ }
29
+
30
+ const {
31
+ people,
32
+ max = 4,
33
+ size = "default",
34
+ class: className,
35
+ }: Props = $props();
36
+
37
+ const shown = $derived(people.slice(0, Math.max(0, max)));
38
+ const hidden = $derived(people.slice(shown.length));
39
+ const hiddenNames = $derived(hidden.map((person) => person.name).join(", "));
40
+
41
+ const boxSizes = { sm: "size-6", default: "size-8", lg: "size-10" };
42
+ const textSizes = {
43
+ sm: "text-[0.625rem]",
44
+ default: "text-xs",
45
+ lg: "text-sm",
46
+ };
47
+
48
+ const triggerClass =
49
+ "bg-background ring-background focus-visible:ring-ring/50 relative shrink-0 rounded-full ring-2 outline-none focus-visible:ring-3";
50
+ </script>
51
+
52
+ <div class={cn("flex w-fit items-center -space-x-2", className)}>
53
+ {#each shown as person, index (index)}
54
+ <Tooltip.Root>
55
+ <Tooltip.Trigger class={triggerClass} aria-label={person.name}>
56
+ <Avatar.Root {size}>
57
+ {#if person.src}
58
+ <Avatar.Image src={person.src} alt="" />
59
+ {/if}
60
+ <Avatar.Fallback class={cn("font-medium", textSizes[size])}>
61
+ {initials(person.name)}
62
+ </Avatar.Fallback>
63
+ </Avatar.Root>
64
+ </Tooltip.Trigger>
65
+ <Tooltip.Content>{person.name}</Tooltip.Content>
66
+ </Tooltip.Root>
67
+ {/each}
68
+ {#if hidden.length > 0}
69
+ <Tooltip.Root>
70
+ <Tooltip.Trigger class={triggerClass} aria-label={hiddenNames}>
71
+ <span
72
+ class={cn(
73
+ "bg-muted text-muted-foreground tabular flex items-center justify-center rounded-full font-medium",
74
+ boxSizes[size],
75
+ textSizes[size],
76
+ )}
77
+ >
78
+ +{hidden.length}
79
+ </span>
80
+ </Tooltip.Trigger>
81
+ <Tooltip.Content>{hiddenNames}</Tooltip.Content>
82
+ </Tooltip.Root>
83
+ {/if}
84
+ </div>
@@ -0,0 +1,14 @@
1
+ export interface AvatarPerson {
2
+ name: string;
3
+ src?: string;
4
+ }
5
+ export declare function initials(name: string): string;
6
+ interface Props {
7
+ people: AvatarPerson[];
8
+ max?: number;
9
+ size?: "sm" | "default" | "lg";
10
+ class?: string;
11
+ }
12
+ declare const AvatarGroup: import("svelte").Component<Props, {}, "">;
13
+ type AvatarGroup = ReturnType<typeof AvatarGroup>;
14
+ export default AvatarGroup;
@@ -0,0 +1,104 @@
1
+ <script lang="ts" module>
2
+ import type { Component } from "svelte";
3
+
4
+ export interface BarListItem {
5
+ label: string;
6
+ value: number;
7
+ href?: string;
8
+ icon?: Component<{ class?: string }>;
9
+ }
10
+ </script>
11
+
12
+ <script lang="ts">
13
+ import { cn } from "../../utils.js";
14
+
15
+ const defaultMessages = {
16
+ empty: "Nothing to show yet.",
17
+ showAll: "Show all",
18
+ showLess: "Show less",
19
+ };
20
+
21
+ interface Props {
22
+ items: BarListItem[];
23
+ format?: (value: number) => string;
24
+ max?: number;
25
+ messages?: Partial<typeof defaultMessages>;
26
+ class?: string;
27
+ }
28
+
29
+ const {
30
+ items,
31
+ format = (value: number) => value.toLocaleString(),
32
+ max,
33
+ messages = {},
34
+ class: className,
35
+ }: Props = $props();
36
+
37
+ const text = $derived({ ...defaultMessages, ...messages });
38
+ const ranked = $derived(
39
+ items.toSorted((left, right) => right.value - left.value),
40
+ );
41
+ const largest = $derived(Math.max(0, ...ranked.map((item) => item.value)));
42
+ const collapsible = $derived(max !== undefined && ranked.length > max);
43
+
44
+ let expanded = $state(false);
45
+
46
+ const shown = $derived(
47
+ collapsible && !expanded ? ranked.slice(0, max) : ranked,
48
+ );
49
+
50
+ function width(value: number): string {
51
+ return largest > 0 ? `${(Math.max(0, value) / largest) * 100}%` : "0%";
52
+ }
53
+ </script>
54
+
55
+ {#snippet row(item: BarListItem)}
56
+ <span class="flex items-center gap-2 text-sm">
57
+ {#if item.icon}
58
+ <item.icon class="text-muted-foreground size-4 shrink-0" />
59
+ {/if}
60
+ <span class="min-w-0 flex-1 truncate">{item.label}</span>
61
+ <span class="tabular shrink-0 font-medium">{format(item.value)}</span>
62
+ </span>
63
+ <span class="bg-muted block h-2 rounded-r-full" aria-hidden="true">
64
+ <span
65
+ class="bg-primary block h-full rounded-r-full"
66
+ style:width={width(item.value)}
67
+ ></span>
68
+ </span>
69
+ {/snippet}
70
+
71
+ <div class={cn("flex w-full flex-col gap-2", className)}>
72
+ {#if ranked.length === 0}
73
+ <p class="text-muted-foreground text-sm">{text.empty}</p>
74
+ {:else}
75
+ <ol class="flex flex-col gap-1">
76
+ {#each shown as item, index (index)}
77
+ <li>
78
+ {#if item.href}
79
+ <a
80
+ href={item.href}
81
+ class="hover:bg-muted focus-visible:ring-ring/50 flex flex-col gap-1.5 rounded-lg px-2 py-1.5 outline-none focus-visible:ring-3"
82
+ >
83
+ {@render row(item)}
84
+ </a>
85
+ {:else}
86
+ <div class="flex flex-col gap-1.5 px-2 py-1.5">
87
+ {@render row(item)}
88
+ </div>
89
+ {/if}
90
+ </li>
91
+ {/each}
92
+ </ol>
93
+ {#if collapsible}
94
+ <button
95
+ type="button"
96
+ aria-expanded={expanded}
97
+ onclick={() => (expanded = !expanded)}
98
+ class="text-primary focus-visible:ring-ring/50 h-11 self-start rounded-md px-2 text-sm font-medium outline-none hover:underline focus-visible:ring-3 md:h-9"
99
+ >
100
+ {expanded ? text.showLess : text.showAll}
101
+ </button>
102
+ {/if}
103
+ {/if}
104
+ </div>
@@ -0,0 +1,22 @@
1
+ import type { Component } from "svelte";
2
+ export interface BarListItem {
3
+ label: string;
4
+ value: number;
5
+ href?: string;
6
+ icon?: Component<{
7
+ class?: string;
8
+ }>;
9
+ }
10
+ declare const BarList: Component<{
11
+ items: BarListItem[];
12
+ format?: (value: number) => string;
13
+ max?: number;
14
+ messages?: Partial<{
15
+ empty: string;
16
+ showAll: string;
17
+ showLess: string;
18
+ }>;
19
+ class?: string;
20
+ }, {}, "">;
21
+ type BarList = ReturnType<typeof BarList>;
22
+ export default BarList;
@@ -0,0 +1,102 @@
1
+ <script lang="ts" module>
2
+ export interface Crumb {
3
+ label: string;
4
+ href?: string;
5
+ }
6
+ </script>
7
+
8
+ <script lang="ts">
9
+ import ChevronRightIcon from "@lucide/svelte/icons/chevron-right";
10
+ import EllipsisIcon from "@lucide/svelte/icons/ellipsis";
11
+ import { MediaQuery } from "svelte/reactivity";
12
+
13
+ import { cn } from "../../utils.js";
14
+
15
+ import Menu from "./menu.svelte";
16
+
17
+ const defaultMessages = {
18
+ label: "Breadcrumb",
19
+ more: "Show hidden pages",
20
+ };
21
+
22
+ interface Props {
23
+ items: Crumb[];
24
+ class?: string;
25
+ messages?: Partial<typeof defaultMessages>;
26
+ }
27
+
28
+ const { items, class: className, messages = {} }: Props = $props();
29
+
30
+ const desktop = new MediaQuery("(min-width: 768px)");
31
+ const text = $derived({ ...defaultMessages, ...messages });
32
+
33
+ const visibleTail = $derived(desktop.current ? 2 : 1);
34
+ const collapsed = $derived(
35
+ items.length > (desktop.current ? 4 : 2)
36
+ ? items.slice(1, items.length - visibleTail)
37
+ : [],
38
+ );
39
+ const shown = $derived(
40
+ collapsed.length > 0
41
+ ? [...items.slice(0, 1), ...items.slice(items.length - visibleTail)]
42
+ : items,
43
+ );
44
+ </script>
45
+
46
+ {#snippet separator()}
47
+ <li role="presentation" aria-hidden="true" class="text-muted-foreground/70">
48
+ <ChevronRightIcon class="size-3.5" />
49
+ </li>
50
+ {/snippet}
51
+
52
+ <nav aria-label={text.label} class={cn("w-full min-w-0", className)}>
53
+ <ol class="text-muted-foreground flex min-w-0 items-center gap-1 text-sm">
54
+ {#each shown as crumb, index (index)}
55
+ {@const current = index === shown.length - 1}
56
+ {#if index > 0}
57
+ {@render separator()}
58
+ {/if}
59
+ <li class={cn("flex min-w-0 items-center", !current && "shrink-2")}>
60
+ {#if current}
61
+ <span aria-current="page" class="text-foreground truncate font-medium"
62
+ >{crumb.label}</span
63
+ >
64
+ {:else if crumb.href}
65
+ <a
66
+ href={crumb.href}
67
+ class="hover:text-foreground focus-visible:ring-ring/50 flex h-11 min-w-0 items-center rounded-md px-1 transition-colors outline-none focus-visible:ring-3 md:h-8"
68
+ >
69
+ <span class="truncate">{crumb.label}</span>
70
+ </a>
71
+ {:else}
72
+ <span class="truncate px-1">{crumb.label}</span>
73
+ {/if}
74
+ </li>
75
+ {#if index === 0 && collapsed.length > 0}
76
+ {@render separator()}
77
+ <li class="flex shrink-0 items-center">
78
+ <Menu
79
+ align="start"
80
+ label={text.more}
81
+ items={collapsed.map((hidden) => ({
82
+ label: hidden.label,
83
+ href: hidden.href,
84
+ disabled: !hidden.href,
85
+ }))}
86
+ >
87
+ {#snippet trigger({ props })}
88
+ <button
89
+ type="button"
90
+ {...props}
91
+ aria-label={text.more}
92
+ class="hover:text-foreground hover:bg-muted focus-visible:ring-ring/50 flex size-11 items-center justify-center rounded-md transition-colors outline-none focus-visible:ring-3 md:size-8"
93
+ >
94
+ <EllipsisIcon class="size-4" />
95
+ </button>
96
+ {/snippet}
97
+ </Menu>
98
+ </li>
99
+ {/if}
100
+ {/each}
101
+ </ol>
102
+ </nav>
@@ -0,0 +1,14 @@
1
+ export interface Crumb {
2
+ label: string;
3
+ href?: string;
4
+ }
5
+ declare const Breadcrumbs: import("svelte").Component<{
6
+ items: Crumb[];
7
+ class?: string;
8
+ messages?: Partial<{
9
+ label: string;
10
+ more: string;
11
+ }>;
12
+ }, {}, "">;
13
+ type Breadcrumbs = ReturnType<typeof Breadcrumbs>;
14
+ export default Breadcrumbs;
@@ -0,0 +1,98 @@
1
+ <script lang="ts">
2
+ import { Checkbox } from "../ui/checkbox/index.js";
3
+ import { cn } from "../../utils.js";
4
+
5
+ import { errorList, type FieldProps } from "./field-frame.svelte";
6
+ import FieldsetFrame from "./fieldset-frame.svelte";
7
+
8
+ interface Option {
9
+ value: string;
10
+ label: string;
11
+ description?: string;
12
+ disabled?: boolean;
13
+ }
14
+
15
+ type Props = FieldProps & {
16
+ options: readonly Option[];
17
+ value?: string[];
18
+ orientation?: "vertical" | "horizontal";
19
+ };
20
+
21
+ let {
22
+ options,
23
+ value = $bindable([]),
24
+ orientation = "vertical",
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 listId = $derived(id ?? generatedId);
37
+ const invalid = $derived(errorList(error).length > 0);
38
+
39
+ function setChecked(optionValue: string, checked: boolean) {
40
+ value = checked
41
+ ? [...value, optionValue]
42
+ : value.filter((entry) => entry !== optionValue);
43
+ }
44
+ </script>
45
+
46
+ <FieldsetFrame
47
+ id={listId}
48
+ {label}
49
+ {description}
50
+ {error}
51
+ {required}
52
+ {disabled}
53
+ class={className}
54
+ >
55
+ <div
56
+ class={cn(
57
+ "flex",
58
+ orientation === "horizontal" ? "flex-row flex-wrap gap-x-6" : "flex-col",
59
+ )}
60
+ >
61
+ {#each options as option, index (option.value)}
62
+ {@const optionId = `${listId}-${index}`}
63
+ <div
64
+ class="group/row relative flex min-h-11 items-start gap-3 py-2.5 has-disabled:opacity-50"
65
+ >
66
+ <Checkbox
67
+ id={optionId}
68
+ {name}
69
+ value={option.value}
70
+ disabled={disabled || option.disabled}
71
+ bind:checked={
72
+ () => value.includes(option.value),
73
+ (checked) => setChecked(option.value, checked)
74
+ }
75
+ aria-invalid={invalid || undefined}
76
+ aria-describedby={option.description
77
+ ? `${optionId}-description`
78
+ : undefined}
79
+ class="z-10 size-5"
80
+ />
81
+ <span class="min-w-0 flex-1">
82
+ <label
83
+ for={optionId}
84
+ class="block cursor-pointer text-sm leading-snug font-medium after:absolute after:inset-0 group-has-disabled/row:cursor-not-allowed"
85
+ >{option.label}</label
86
+ >
87
+ {#if option.description}
88
+ <span
89
+ id="{optionId}-description"
90
+ class="text-muted-foreground mt-0.5 block text-sm"
91
+ >{option.description}</span
92
+ >
93
+ {/if}
94
+ </span>
95
+ </div>
96
+ {/each}
97
+ </div>
98
+ </FieldsetFrame>