@orochibraru/ui 0.0.0 → 1.0.0-canary.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 (90) hide show
  1. package/README.md +97 -21
  2. package/dist/components/blocks/accent-toggle.svelte +85 -0
  3. package/dist/components/blocks/accent-toggle.svelte.d.ts +18 -0
  4. package/dist/components/blocks/bottom-nav.svelte +67 -0
  5. package/dist/components/blocks/bottom-nav.svelte.d.ts +18 -0
  6. package/dist/components/blocks/music-player/format-time.d.ts +2 -0
  7. package/dist/components/blocks/music-player/format-time.js +13 -0
  8. package/dist/components/blocks/music-player/labelled-slider.svelte +36 -0
  9. package/dist/components/blocks/music-player/labelled-slider.svelte.d.ts +15 -0
  10. package/dist/components/blocks/music-player/volume-control.svelte +48 -0
  11. package/dist/components/blocks/music-player/volume-control.svelte.d.ts +7 -0
  12. package/dist/components/blocks/music-player.svelte +263 -0
  13. package/dist/components/blocks/music-player.svelte.d.ts +26 -0
  14. package/dist/components/blocks/nav-cards.svelte +42 -0
  15. package/dist/components/blocks/nav-cards.svelte.d.ts +15 -0
  16. package/dist/components/blocks/new-password-fields.svelte +127 -0
  17. package/dist/components/blocks/new-password-fields.svelte.d.ts +19 -0
  18. package/dist/components/blocks/option-group.svelte +71 -0
  19. package/dist/components/blocks/option-group.svelte.d.ts +20 -0
  20. package/dist/components/blocks/otp-field.svelte +59 -0
  21. package/dist/components/blocks/otp-field.svelte.d.ts +12 -0
  22. package/dist/components/blocks/password-input.svelte +51 -0
  23. package/dist/components/blocks/password-input.svelte.d.ts +11 -0
  24. package/dist/components/blocks/stat-card.svelte +1 -1
  25. package/dist/components/blocks/tab-nav.svelte +38 -0
  26. package/dist/components/blocks/tab-nav.svelte.d.ts +14 -0
  27. package/dist/components/blocks/tab-section.svelte +48 -0
  28. package/dist/components/blocks/tab-section.svelte.d.ts +12 -0
  29. package/dist/components/blocks/theme-picker.svelte +69 -0
  30. package/dist/components/blocks/theme-picker.svelte.d.ts +16 -0
  31. package/dist/components/blocks/theme-toggle.svelte +68 -0
  32. package/dist/components/blocks/theme-toggle.svelte.d.ts +12 -0
  33. package/dist/components/blocks/video-player/center-controls.svelte +54 -0
  34. package/dist/components/blocks/video-player/center-controls.svelte.d.ts +10 -0
  35. package/dist/components/blocks/video-player/control-bar.svelte +184 -0
  36. package/dist/components/blocks/video-player/control-bar.svelte.d.ts +18 -0
  37. package/dist/components/blocks/video-player/control-button.svelte +54 -0
  38. package/dist/components/blocks/video-player/control-button.svelte.d.ts +13 -0
  39. package/dist/components/blocks/video-player/format.d.ts +9 -0
  40. package/dist/components/blocks/video-player/format.js +24 -0
  41. package/dist/components/blocks/video-player/gesture-feedback.svelte +32 -0
  42. package/dist/components/blocks/video-player/gesture-feedback.svelte.d.ts +7 -0
  43. package/dist/components/blocks/video-player/gestures.d.ts +13 -0
  44. package/dist/components/blocks/video-player/gestures.js +25 -0
  45. package/dist/components/blocks/video-player/keymap.d.ts +14 -0
  46. package/dist/components/blocks/video-player/keymap.js +55 -0
  47. package/dist/components/blocks/video-player/messages.d.ts +22 -0
  48. package/dist/components/blocks/video-player/messages.js +21 -0
  49. package/dist/components/blocks/video-player/option-menu.svelte +85 -0
  50. package/dist/components/blocks/video-player/option-menu.svelte.d.ts +18 -0
  51. package/dist/components/blocks/video-player/player.svelte.d.ts +37 -0
  52. package/dist/components/blocks/video-player/player.svelte.js +115 -0
  53. package/dist/components/blocks/video-player/scrub-bar.svelte +95 -0
  54. package/dist/components/blocks/video-player/scrub-bar.svelte.d.ts +12 -0
  55. package/dist/components/blocks/video-player/tap-gestures.svelte.d.ts +19 -0
  56. package/dist/components/blocks/video-player/tap-gestures.svelte.js +56 -0
  57. package/dist/components/blocks/video-player.svelte +288 -0
  58. package/dist/components/blocks/video-player.svelte.d.ts +29 -0
  59. package/dist/components/ui/accordion/accordion.svelte.d.ts +1 -1
  60. package/dist/components/ui/calendar/calendar.svelte.d.ts +1 -1
  61. package/dist/components/ui/command/command-dialog.svelte.d.ts +1 -1
  62. package/dist/components/ui/command/command-input.svelte.d.ts +1 -1
  63. package/dist/components/ui/command/command.svelte.d.ts +1 -1
  64. package/dist/components/ui/context-menu/context-menu-radio-group.svelte.d.ts +1 -1
  65. package/dist/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte.d.ts +1 -1
  66. package/dist/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte.d.ts +1 -1
  67. package/dist/components/ui/input/input.svelte +2 -2
  68. package/dist/components/ui/input/input.svelte.d.ts +1 -1
  69. package/dist/components/ui/input-group/input-group-input.svelte.d.ts +2 -2
  70. package/dist/components/ui/input-group/input-group-textarea.svelte.d.ts +1 -1
  71. package/dist/components/ui/input-group/input-group.svelte +1 -1
  72. package/dist/components/ui/input-otp/input-otp-slot.svelte +1 -1
  73. package/dist/components/ui/input-otp/input-otp.svelte.d.ts +1 -1
  74. package/dist/components/ui/menubar/menubar-radio-group.svelte.d.ts +1 -1
  75. package/dist/components/ui/radio-group/radio-group.svelte.d.ts +1 -1
  76. package/dist/components/ui/range-calendar/range-calendar.svelte.d.ts +1 -1
  77. package/dist/components/ui/select/select-trigger.svelte +1 -1
  78. package/dist/components/ui/sidebar/sidebar-input.svelte.d.ts +2 -2
  79. package/dist/components/ui/slider/slider.svelte.d.ts +1 -1
  80. package/dist/components/ui/tabs/tabs.svelte.d.ts +1 -1
  81. package/dist/components/ui/textarea/textarea.svelte +1 -1
  82. package/dist/components/ui/textarea/textarea.svelte.d.ts +1 -1
  83. package/dist/components/ui/toggle/toggle.svelte.d.ts +1 -1
  84. package/dist/components/ui/toggle-group/toggle-group-item.svelte.d.ts +1 -1
  85. package/dist/components/ui/toggle-group/toggle-group.svelte.d.ts +1 -1
  86. package/dist/mcp-content.json +1 -0
  87. package/dist/mcp.js +83 -0
  88. package/dist/password.d.ts +11 -0
  89. package/dist/password.js +38 -0
  90. package/package.json +15 -3
package/README.md CHANGED
@@ -59,26 +59,58 @@ a search box never gets a login suggestion.
59
59
 
60
60
  Under `@orochibraru/ui/blocks/<name>`:
61
61
 
62
- | Block | What it does |
63
- | ------------------ | --------------------------------------------------------------------------- |
64
- | `auth-shell` | Centered sign-in layout: heading, optional `brand` and `footer` snippets. |
65
- | `copy-field` | Monospace value with a copy button and a promise toast. |
66
- | `document-title` | `<title>` from the page title, suffixed with `appName`. |
67
- | `empty-state` | Icon, title, subtitle and an action slot for an empty list. |
68
- | `error-page` | Status, message and error id from `page`, with a link to `homeHref`. |
69
- | `fab` | Floating action button above the phone tab bar, with a haptic tap. |
70
- | `icon-button` | Round icon button with a tooltip that doubles as its accessible name. |
71
- | `install-prompt` | One-time PWA install sheet: native prompt, iOS steps or browser menu steps. |
72
- | `responsive-sheet` | A dialog on desktop, a bottom drawer under 768px. |
73
- | `setting-switch` | Labelled switch row for settings forms, posting `name=true` when on. |
74
- | `stat-card` | Glass card with an icon, a value and a hint, optionally a link. |
75
- | `top-loading-bar` | Progress bar during SvelteKit navigations, safe-area aware. |
76
-
77
- Blocks with built-in copy (`copy-field`, `error-page`, `install-prompt`) take a
78
- `messages` prop. Its keys override the English defaults one by one, which is how
79
- a French or translated app uses them. Blocks never set the page title: the app
62
+ | Block | What it does |
63
+ | --------------------- | ----------------------------------------------------------------------------- |
64
+ | `accent-toggle` | The accent presets: swatch strip on desktop, labelled rows on phones. |
65
+ | `auth-shell` | Centered sign-in layout: heading, optional `brand` and `footer` snippets. |
66
+ | `bottom-nav` | Phone tab bar of top-level destinations, the current one lit. |
67
+ | `copy-field` | Monospace value with a copy button and a promise toast. |
68
+ | `document-title` | `<title>` from the page title, suffixed with `appName`. |
69
+ | `empty-state` | Icon, title, subtitle and an action slot for an empty list. |
70
+ | `error-page` | Status, message and error id from `page`, with a link to `homeHref`. |
71
+ | `fab` | Floating action button above the phone tab bar, with a haptic tap. |
72
+ | `icon-button` | Round icon button with a tooltip that doubles as its accessible name. |
73
+ | `install-prompt` | One-time PWA install sheet: native prompt, iOS steps or browser menu steps. |
74
+ | `music-player` | Audio bar with a queue: play, skip, seek, volume; positioning is the app's. |
75
+ | `nav-cards` | Phone-only list of a section's tabs as cards: icon, label, description. |
76
+ | `new-password-fields` | New password and its confirmation: reveal, strength meter, rules. |
77
+ | `option-group` | Radio cards for a settings choice, with an icon or a `swatch` snippet each. |
78
+ | `otp-field` | One-time code: digits only, SMS autofill, posted as one `code` field. |
79
+ | `password-input` | Password field with a button to reveal it. |
80
+ | `responsive-sheet` | A dialog on desktop, a bottom drawer under 768px. |
81
+ | `setting-switch` | Labelled switch row for settings forms, posting `name=true` when on. |
82
+ | `stat-card` | Glass card with an icon, a value and a hint, optionally a link. |
83
+ | `tab-nav` | Pill tab bar of links, the current page's tab highlighted. |
84
+ | `tab-section` | Layout for a tabbed section: tabs on desktop, nav cards on phones. |
85
+ | `theme-picker` | System, light and dark as radio cards, for an appearance page. |
86
+ | `theme-toggle` | System, light and dark: icon strip on desktop, three rows on phones. |
87
+ | `top-loading-bar` | Progress bar during SvelteKit navigations, safe-area aware. |
88
+ | `video-player` | Video with scrubbing, chapters, subtitles, speed, fullscreen, gestures, keys. |
89
+
90
+ Blocks with built-in copy (`accent-toggle`, `copy-field`, `error-page`,
91
+ `install-prompt`, `new-password-fields`, `password-input`, `theme-picker`,
92
+ `theme-toggle`) take a `messages` prop. Its keys override the English defaults
93
+ one by one, which is how a French or translated app uses them. Blocks never set
94
+ the page title, except `tab-section`, which owns its section's title: the app
80
95
  calls `setPageTitle` itself.
81
96
 
97
+ ### Tabbed sections
98
+
99
+ A tabbed page is one real route per tab, each tab with a description, under a
100
+ `tab-section` layout. On phones the index route renders `nav-cards` and each tab
101
+ is a page titled by its label, its back button returning to the cards. On
102
+ desktop the index replaces itself with the first tab and `tab-nav` shows them
103
+ all. The tabs live in one list (`SectionTab[]`, exported by `nav-cards`) that
104
+ the layout and the index page share.
105
+
106
+ ### Theme
107
+
108
+ `theme-picker` and `theme-toggle` call `mode-watcher`'s `setMode`, so the app
109
+ mounts `<ModeWatcher />` in its root layout. Both take the saved theme as
110
+ `value`, so the server-rendered page checks the right one, and call `onchange`
111
+ with the new theme for the app to persist. `theme-picker` also posts as a
112
+ `theme` radio field (`name` changes it) inside a form.
113
+
82
114
  `icon-button`, and `copy-field` through it, need a `Tooltip.Provider` above
83
115
  them, which apps put in their root layout.
84
116
 
@@ -94,6 +126,9 @@ them, which apps put in their root layout.
94
126
  - `@orochibraru/ui/appearance`: `applyAppearance` and `appearanceBootScript`
95
127
  apply a saved accent, theme, dark variant, font and corner style to `<html>`,
96
128
  the boot script before first paint.
129
+ - `@orochibraru/ui/password`: `passwordRules` and `passwordScore`, the rules a
130
+ new password must pass and a 0 to 4 strength, the same on the form and the
131
+ server. `new-password-fields` renders them; `ruleLabels` translates them.
97
132
  - `@orochibraru/ui/haptics`: `haptic("tap" | "success" | "error")`, a no-op
98
133
  where vibration isn't supported.
99
134
 
@@ -133,9 +168,23 @@ bun install
133
168
  bun run dev
134
169
  ```
135
170
 
136
- The dev server shows every primitive and block on one page, with an accent and
137
- dark mode switch. `bun run check`, `bun run test` and `bun run build` are the
138
- gates; `build` packages `dist/` and runs publint.
171
+ The dev server is the docs site: a page per block, primitive and module, with
172
+ the component live and the code that renders it. That code is the example file
173
+ itself, its `#lib` imports rewritten to the package subpaths, so the two can't
174
+ drift apart. A file named `<slug>.<name>` next to an example is a companion,
175
+ shown after it: the layout, action or page an app needs for what the preview
176
+ can't hold. An example without a script is a note for the preview only.
177
+
178
+ Every component page can be copied as Markdown, viewed as Markdown, exported as
179
+ a PDF (through the browser's print dialog, in the light theme), or opened in
180
+ Claude or ChatGPT. The list pages filter by category.
181
+
182
+ The `Dockerfile` deploys the docs site: `bun run build:docs` compiles it to one
183
+ Bun binary that serves on port 3000 and reads `x-forwarded-proto` and
184
+ `x-forwarded-host`, so llms.txt links point at the public URL behind a proxy.
185
+
186
+ `bun run check`, `bun run test` and `bun run build` are the gates; `build`
187
+ packages `dist/` and runs publint.
139
188
 
140
189
  New upstream components come from the shadcn-svelte CLI (`components.json` is
141
190
  set up), followed by `node_modules/.bin/tailwint --fix` on the new files. The
@@ -145,3 +194,30 @@ put them back after.
145
194
 
146
195
  Dependencies use caret ranges, unlike the apps' exact pins, so a consuming app
147
196
  dedupes `bits-ui` and friends with its own copy instead of bundling two.
197
+
198
+ ## MCP server
199
+
200
+ The package ships an MCP server with the docs and every example's code bundled
201
+ in, so an editor's assistant can list, search and read the components without
202
+ the docs site. It runs over stdio from the package's `bin`:
203
+
204
+ ```bash
205
+ claude mcp add orochibraru-ui -- npx -y @orochibraru/ui
206
+ ```
207
+
208
+ Cursor, VS Code and other clients take the same command:
209
+
210
+ ```json
211
+ {
212
+ "mcpServers": {
213
+ "orochibraru-ui": { "command": "npx", "args": ["-y", "@orochibraru/ui"] }
214
+ }
215
+ }
216
+ ```
217
+
218
+ Its tools are `get_setup` (this README), `list_components` (by group or
219
+ category), `search_components` and `get_component` (the description, import path
220
+ and usage code). `bun run build` bundles it with its content, so it needs no
221
+ dependency at runtime. The docs site serves the same server over streamable HTTP
222
+ at `/mcp`, plus `/llms.txt`, `/llms-full.txt` and a Markdown page per component
223
+ at `/<group>/<slug>.md`.
@@ -0,0 +1,85 @@
1
+ <script lang="ts">
2
+ import { MediaQuery } from "svelte/reactivity";
3
+
4
+ import * as ToggleGroup from "../ui/toggle-group/index.js";
5
+
6
+ type Accent =
7
+ | "blue"
8
+ | "bordeaux"
9
+ | "green"
10
+ | "violet"
11
+ | "rose"
12
+ | "orange"
13
+ | "amber"
14
+ | "cyan"
15
+ | "neutral";
16
+
17
+ const defaultMessages = {
18
+ label: "Accent",
19
+ blue: "Blue",
20
+ bordeaux: "Bordeaux",
21
+ green: "Green",
22
+ violet: "Violet",
23
+ rose: "Rose",
24
+ orange: "Orange",
25
+ amber: "Amber",
26
+ cyan: "Cyan",
27
+ neutral: "Neutral",
28
+ };
29
+
30
+ interface Props {
31
+ value?: Accent;
32
+ onchange?: (accent: Accent) => void;
33
+ messages?: Partial<typeof defaultMessages>;
34
+ }
35
+
36
+ let { value = $bindable("blue"), onchange, messages = {} }: Props = $props();
37
+
38
+ const accents: Accent[] = [
39
+ "blue",
40
+ "bordeaux",
41
+ "green",
42
+ "violet",
43
+ "rose",
44
+ "orange",
45
+ "amber",
46
+ "cyan",
47
+ "neutral",
48
+ ];
49
+
50
+ const desktop = new MediaQuery("(min-width: 768px)");
51
+ const text = $derived({ ...defaultMessages, ...messages });
52
+
53
+ function pick(next: string) {
54
+ if (!next) {
55
+ return;
56
+ }
57
+ value = next as Accent;
58
+ document.documentElement.dataset.accent = value;
59
+ onchange?.(value);
60
+ }
61
+ </script>
62
+
63
+ <ToggleGroup.Root
64
+ type="single"
65
+ variant="outline"
66
+ orientation={desktop.current ? "horizontal" : "vertical"}
67
+ aria-label={text.label}
68
+ class="max-md:w-full"
69
+ bind:value={() => value, pick}
70
+ >
71
+ {#each accents as accent (accent)}
72
+ <ToggleGroup.Item
73
+ value={accent}
74
+ aria-label={text[accent]}
75
+ title={text[accent]}
76
+ data-accent={accent}
77
+ class="max-md:h-12 max-md:w-full max-md:justify-start max-md:gap-3 max-md:px-4 md:size-9"
78
+ >
79
+ <span
80
+ class="size-3 rounded-full bg-[oklch(0.6_var(--primary-chroma)_var(--hue))]"
81
+ ></span>
82
+ <span class="md:hidden">{text[accent]}</span>
83
+ </ToggleGroup.Item>
84
+ {/each}
85
+ </ToggleGroup.Root>
@@ -0,0 +1,18 @@
1
+ declare const AccentToggle: import("svelte").Component<{
2
+ value?: "blue" | "bordeaux" | "green" | "violet" | "rose" | "orange" | "amber" | "cyan" | "neutral";
3
+ onchange?: (accent: "blue" | "bordeaux" | "green" | "violet" | "rose" | "orange" | "amber" | "cyan" | "neutral") => void;
4
+ messages?: Partial<{
5
+ label: string;
6
+ blue: string;
7
+ bordeaux: string;
8
+ green: string;
9
+ violet: string;
10
+ rose: string;
11
+ orange: string;
12
+ amber: string;
13
+ cyan: string;
14
+ neutral: string;
15
+ }>;
16
+ }, {}, "value">;
17
+ type AccentToggle = ReturnType<typeof AccentToggle>;
18
+ export default AccentToggle;
@@ -0,0 +1,67 @@
1
+ <script lang="ts" module>
2
+ import type { Component } from "svelte";
3
+
4
+ export interface NavItem {
5
+ href: string;
6
+ label: string;
7
+ icon: Component<{ class?: string }>;
8
+ }
9
+
10
+ /** Whether `pathname` sits under the destination at `href`; the root only matches itself. */
11
+ export function isUnder(pathname: string, href: string): boolean {
12
+ return href === "/"
13
+ ? pathname === "/"
14
+ : pathname === href || pathname.startsWith(`${href}/`);
15
+ }
16
+ </script>
17
+
18
+ <script lang="ts">
19
+ import { page } from "$app/state";
20
+
21
+ interface Props {
22
+ items: NavItem[];
23
+ isActive?: (pathname: string, href: string) => boolean;
24
+ label?: string;
25
+ }
26
+
27
+ const { items, isActive = isUnder, label = "Main" }: Props = $props();
28
+
29
+ let compact = $state(false);
30
+ let lastY = 0;
31
+
32
+ function onscroll() {
33
+ const y = window.scrollY;
34
+ if (Math.abs(y - lastY) < 8) {
35
+ return;
36
+ }
37
+ compact = y > lastY && y > 64;
38
+ lastY = y;
39
+ }
40
+ </script>
41
+
42
+ <svelte:window {onscroll} />
43
+
44
+ <nav
45
+ class="group/nav fixed inset-x-0 bottom-0 z-40 px-4 pb-[max(0.75rem,env(safe-area-inset-bottom))] md:hidden print:hidden"
46
+ style="view-transition-name: tab-bar"
47
+ aria-label={label}
48
+ data-compact={compact || undefined}
49
+ >
50
+ <ul
51
+ class="bg-popover border-border mx-auto flex max-w-sm items-center gap-1 rounded-full border p-1.5 shadow-lg"
52
+ >
53
+ {#each items as item (item.href)}
54
+ {@const active = isActive(page.url.pathname, item.href)}
55
+ <li class="flex-1">
56
+ <a
57
+ href={item.href}
58
+ aria-current={active ? "page" : undefined}
59
+ class="text-muted-foreground aria-[current=page]:bg-primary aria-[current=page]:text-primary-foreground aria-[current=page]:glow flex h-12 flex-col items-center justify-center gap-0.5 rounded-full px-0.5 text-[0.6875rem] font-medium tracking-tight transition-all group-data-compact/nav:h-10 active:scale-95"
60
+ >
61
+ <item.icon class="size-5" />
62
+ <span class="group-data-compact/nav:sr-only">{item.label}</span>
63
+ </a>
64
+ </li>
65
+ {/each}
66
+ </ul>
67
+ </nav>
@@ -0,0 +1,18 @@
1
+ import type { Component } from "svelte";
2
+ export interface NavItem {
3
+ href: string;
4
+ label: string;
5
+ icon: Component<{
6
+ class?: string;
7
+ }>;
8
+ }
9
+ /** Whether `pathname` sits under the destination at `href`; the root only matches itself. */
10
+ export declare function isUnder(pathname: string, href: string): boolean;
11
+ interface Props {
12
+ items: NavItem[];
13
+ isActive?: (pathname: string, href: string) => boolean;
14
+ label?: string;
15
+ }
16
+ declare const BottomNav: Component<Props, {}, "">;
17
+ type BottomNav = ReturnType<typeof BottomNav>;
18
+ export default BottomNav;
@@ -0,0 +1,2 @@
1
+ /** `m:ss`, or `h:mm:ss` past an hour; an unknown or endless duration reads `0:00`. */
2
+ export declare function formatTime(seconds: number): string;
@@ -0,0 +1,13 @@
1
+ /** `m:ss`, or `h:mm:ss` past an hour; an unknown or endless duration reads `0:00`. */
2
+ export function formatTime(seconds) {
3
+ if (!Number.isFinite(seconds) || seconds < 0) {
4
+ return "0:00";
5
+ }
6
+ const whole = Math.floor(seconds);
7
+ const hours = Math.floor(whole / 3600);
8
+ const minutes = Math.floor((whole % 3600) / 60);
9
+ const rest = String(whole % 60).padStart(2, "0");
10
+ return hours > 0
11
+ ? `${hours}:${String(minutes).padStart(2, "0")}:${rest}`
12
+ : `${minutes}:${rest}`;
13
+ }
@@ -0,0 +1,36 @@
1
+ <script lang="ts">
2
+ import { Slider } from "../../ui/slider/index.js";
3
+
4
+ interface Props {
5
+ value: number;
6
+ max: number;
7
+ step: number;
8
+ label: string;
9
+ valueText: string;
10
+ onchange?: (value: number) => void;
11
+ oncommit?: (value: number) => void;
12
+ orientation?: "horizontal" | "vertical";
13
+ disabled?: boolean;
14
+ class?: string;
15
+ }
16
+
17
+ const { value, label, valueText, onchange, oncommit, ...rest }: Props =
18
+ $props();
19
+
20
+ let ref: HTMLElement | null = $state(null);
21
+
22
+ $effect(() => {
23
+ const thumb = ref?.querySelector("[role=slider]");
24
+ thumb?.setAttribute("aria-label", label);
25
+ thumb?.setAttribute("aria-valuetext", valueText);
26
+ });
27
+ </script>
28
+
29
+ <Slider
30
+ bind:ref
31
+ type="single"
32
+ {value}
33
+ onValueChange={onchange}
34
+ onValueCommit={oncommit}
35
+ {...rest}
36
+ />
@@ -0,0 +1,15 @@
1
+ interface Props {
2
+ value: number;
3
+ max: number;
4
+ step: number;
5
+ label: string;
6
+ valueText: string;
7
+ onchange?: (value: number) => void;
8
+ oncommit?: (value: number) => void;
9
+ orientation?: "horizontal" | "vertical";
10
+ disabled?: boolean;
11
+ class?: string;
12
+ }
13
+ declare const LabelledSlider: import("svelte").Component<Props, {}, "">;
14
+ type LabelledSlider = ReturnType<typeof LabelledSlider>;
15
+ export default LabelledSlider;
@@ -0,0 +1,48 @@
1
+ <script lang="ts">
2
+ import Volume1Icon from "@lucide/svelte/icons/volume-1";
3
+ import Volume2Icon from "@lucide/svelte/icons/volume-2";
4
+ import VolumeXIcon from "@lucide/svelte/icons/volume-x";
5
+
6
+ import { Button } from "../../ui/button/index.js";
7
+ import * as Popover from "../../ui/popover/index.js";
8
+
9
+ import LabelledSlider from "./labelled-slider.svelte";
10
+
11
+ interface Props {
12
+ volume: number;
13
+ label: string;
14
+ }
15
+
16
+ let { volume = $bindable(1), label }: Props = $props();
17
+
18
+ const Icon = $derived(
19
+ volume === 0 ? VolumeXIcon : volume < 0.5 ? Volume1Icon : Volume2Icon,
20
+ );
21
+ </script>
22
+
23
+ <Popover.Root>
24
+ <Popover.Trigger>
25
+ {#snippet child({ props })}
26
+ <Button
27
+ {...props}
28
+ variant="ghost"
29
+ size="icon"
30
+ aria-label={label}
31
+ class="size-11 rounded-full md:size-9"
32
+ >
33
+ <Icon class="size-5" />
34
+ </Button>
35
+ {/snippet}
36
+ </Popover.Trigger>
37
+ <Popover.Content class="glass w-auto items-center rounded-2xl p-4">
38
+ <LabelledSlider
39
+ orientation="vertical"
40
+ value={volume}
41
+ max={1}
42
+ step={0.01}
43
+ {label}
44
+ valueText={`${Math.round(volume * 100)}%`}
45
+ onchange={(value) => (volume = value)}
46
+ />
47
+ </Popover.Content>
48
+ </Popover.Root>
@@ -0,0 +1,7 @@
1
+ interface Props {
2
+ volume: number;
3
+ label: string;
4
+ }
5
+ declare const VolumeControl: import("svelte").Component<Props, {}, "volume">;
6
+ type VolumeControl = ReturnType<typeof VolumeControl>;
7
+ export default VolumeControl;