@krizaka/ui 2.3.0 → 2.4.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.
- package/README.md +16 -1
- package/dist/audio-player.d.ts +113 -0
- package/dist/audio-player.js +207 -0
- package/dist/audio-player.js.map +1 -0
- package/dist/chunk-2USC2YT6.js +17 -0
- package/dist/chunk-2USC2YT6.js.map +1 -0
- package/dist/chunk-EUPTXKH7.js +95 -0
- package/dist/chunk-EUPTXKH7.js.map +1 -0
- package/dist/chunk-RUPRYJ45.js +82 -0
- package/dist/chunk-RUPRYJ45.js.map +1 -0
- package/dist/chunk-XUMJA5ZI.js +283 -0
- package/dist/chunk-XUMJA5ZI.js.map +1 -0
- package/dist/chunk-YH4LMWRU.js +92 -0
- package/dist/chunk-YH4LMWRU.js.map +1 -0
- package/dist/chunk-YX6CNVJA.js +94 -0
- package/dist/chunk-YX6CNVJA.js.map +1 -0
- package/dist/chunk-ZIXB33X7.js +95 -0
- package/dist/chunk-ZIXB33X7.js.map +1 -0
- package/dist/combobox.d.ts +188 -0
- package/dist/combobox.js +16 -0
- package/dist/combobox.js.map +1 -0
- package/dist/data-table.d.ts +246 -0
- package/dist/data-table.js +290 -0
- package/dist/data-table.js.map +1 -0
- package/dist/date-picker.d.ts +288 -0
- package/dist/date-picker.js +364 -0
- package/dist/date-picker.js.map +1 -0
- package/dist/file-upload.d.ts +238 -0
- package/dist/file-upload.js +437 -0
- package/dist/file-upload.js.map +1 -0
- package/dist/filter-bar.d.ts +139 -0
- package/dist/filter-bar.js +155 -0
- package/dist/filter-bar.js.map +1 -0
- package/dist/image-cropper.d.ts +115 -0
- package/dist/image-cropper.js +166 -0
- package/dist/image-cropper.js.map +1 -0
- package/dist/index.js.map +1 -1
- package/dist/media-capture.d.ts +155 -0
- package/dist/media-capture.js +242 -0
- package/dist/media-capture.js.map +1 -0
- package/dist/native/media-capture.d.ts +49 -0
- package/dist/native/media-capture.js +113 -0
- package/dist/native/media-capture.js.map +1 -0
- package/dist/native/video-player.d.ts +50 -0
- package/dist/native/video-player.js +69 -0
- package/dist/native/video-player.js.map +1 -0
- package/dist/native.d.ts +66 -2
- package/dist/native.js +166 -68
- package/dist/native.js.map +1 -1
- package/dist/pagination.d.ts +141 -0
- package/dist/pagination.js +165 -0
- package/dist/pagination.js.map +1 -0
- package/dist/search-field.d.ts +100 -0
- package/dist/search-field.js +167 -0
- package/dist/search-field.js.map +1 -0
- package/dist/tag-input.d.ts +44 -0
- package/dist/tag-input.js +67 -0
- package/dist/tag-input.js.map +1 -0
- package/dist/video-player.d.ts +204 -0
- package/dist/video-player.js +371 -0
- package/dist/video-player.js.map +1 -0
- package/dist/video-trimmer.d.ts +142 -0
- package/dist/video-trimmer.js +193 -0
- package/dist/video-trimmer.js.map +1 -0
- package/package.json +232 -4
- package/registry/alert.json +1 -1
- package/registry/audio-player.json +282 -0
- package/registry/avatar.json +1 -1
- package/registry/badge.json +1 -1
- package/registry/button.json +1 -1
- package/registry/card.json +1 -1
- package/registry/checkbox.json +1 -1
- package/registry/chip-link.json +1 -1
- package/registry/chip.json +1 -1
- package/registry/combobox.json +475 -0
- package/registry/command.json +1 -1
- package/registry/confirm-button.json +1 -1
- package/registry/countdown.json +1 -1
- package/registry/data-table.json +500 -0
- package/registry/date-picker.json +978 -0
- package/registry/dialog.json +1 -1
- package/registry/dropdown-menu.json +1 -1
- package/registry/empty-state.json +1 -1
- package/registry/examples/audio-player/podcast.d.ts +4 -0
- package/registry/examples/audio-player/podcast.js +22 -0
- package/registry/examples/audio-player/podcast.tsx +19 -0
- package/registry/examples/audio-player/voice-note.d.ts +4 -0
- package/registry/examples/audio-player/voice-note.js +12 -0
- package/registry/examples/audio-player/voice-note.tsx +12 -0
- package/registry/examples/combobox/async.d.ts +4 -0
- package/registry/examples/combobox/async.js +23 -0
- package/registry/examples/combobox/async.tsx +22 -0
- package/registry/examples/combobox/creatable.d.ts +4 -0
- package/registry/examples/combobox/creatable.js +32 -0
- package/registry/examples/combobox/creatable.tsx +28 -0
- package/registry/examples/combobox/invalid.d.ts +4 -0
- package/registry/examples/combobox/invalid.js +23 -0
- package/registry/examples/combobox/invalid.tsx +17 -0
- package/registry/examples/combobox/multiple.d.ts +4 -0
- package/registry/examples/combobox/multiple.js +29 -0
- package/registry/examples/combobox/multiple.tsx +26 -0
- package/registry/examples/data-table/cards.d.ts +4 -0
- package/registry/examples/data-table/cards.js +21 -0
- package/registry/examples/data-table/cards.tsx +20 -0
- package/registry/examples/data-table/empty.d.ts +4 -0
- package/registry/examples/data-table/empty.js +26 -0
- package/registry/examples/data-table/empty.tsx +21 -0
- package/registry/examples/data-table/error.d.ts +4 -0
- package/registry/examples/data-table/error.js +28 -0
- package/registry/examples/data-table/error.tsx +29 -0
- package/registry/examples/data-table/jobs.d.ts +4 -0
- package/registry/examples/data-table/jobs.js +37 -0
- package/registry/examples/data-table/jobs.tsx +36 -0
- package/registry/examples/data-table/loading.d.ts +4 -0
- package/registry/examples/data-table/loading.js +16 -0
- package/registry/examples/data-table/loading.tsx +12 -0
- package/registry/examples/data-table/virtualized.d.ts +4 -0
- package/registry/examples/data-table/virtualized.js +21 -0
- package/registry/examples/data-table/virtualized.tsx +20 -0
- package/registry/examples/date-picker/calendar.d.ts +4 -0
- package/registry/examples/date-picker/calendar.js +14 -0
- package/registry/examples/date-picker/calendar.tsx +16 -0
- package/registry/examples/date-picker/french.d.ts +4 -0
- package/registry/examples/date-picker/french.js +20 -0
- package/registry/examples/date-picker/french.tsx +14 -0
- package/registry/examples/date-picker/open.d.ts +4 -0
- package/registry/examples/date-picker/open.js +11 -0
- package/registry/examples/date-picker/open.tsx +8 -0
- package/registry/examples/date-picker/range.d.ts +4 -0
- package/registry/examples/date-picker/range.js +29 -0
- package/registry/examples/date-picker/range.tsx +25 -0
- package/registry/examples/date-picker/schedule.d.ts +4 -0
- package/registry/examples/date-picker/schedule.js +26 -0
- package/registry/examples/date-picker/schedule.tsx +22 -0
- package/registry/examples/file-upload/images.d.ts +4 -0
- package/registry/examples/file-upload/images.js +32 -0
- package/registry/examples/file-upload/images.tsx +25 -0
- package/registry/examples/file-upload/states.d.ts +4 -0
- package/registry/examples/file-upload/states.js +28 -0
- package/registry/examples/file-upload/states.tsx +23 -0
- package/registry/examples/file-upload/tus.d.ts +4 -0
- package/registry/examples/file-upload/tus.js +23 -0
- package/registry/examples/file-upload/tus.tsx +26 -0
- package/registry/examples/file-upload/video.d.ts +4 -0
- package/registry/examples/file-upload/video.js +39 -0
- package/registry/examples/file-upload/video.tsx +36 -0
- package/registry/examples/filter-bar/address.d.ts +4 -0
- package/registry/examples/filter-bar/address.js +29 -0
- package/registry/examples/filter-bar/address.tsx +27 -0
- package/registry/examples/filter-bar/jobs.d.ts +4 -0
- package/registry/examples/filter-bar/jobs.js +37 -0
- package/registry/examples/filter-bar/jobs.tsx +37 -0
- package/registry/examples/filter-bar/menu.d.ts +4 -0
- package/registry/examples/filter-bar/menu.js +27 -0
- package/registry/examples/filter-bar/menu.tsx +23 -0
- package/registry/examples/image-cropper/avatar.d.ts +4 -0
- package/registry/examples/image-cropper/avatar.js +13 -0
- package/registry/examples/image-cropper/avatar.tsx +14 -0
- package/registry/examples/image-cropper/banner.d.ts +4 -0
- package/registry/examples/image-cropper/banner.js +11 -0
- package/registry/examples/image-cropper/banner.tsx +10 -0
- package/registry/examples/image-cropper/vertical-video.d.ts +4 -0
- package/registry/examples/image-cropper/vertical-video.js +12 -0
- package/registry/examples/image-cropper/vertical-video.tsx +16 -0
- package/registry/examples/media-capture/clip.d.ts +4 -0
- package/registry/examples/media-capture/clip.js +20 -0
- package/registry/examples/media-capture/clip.tsx +18 -0
- package/registry/examples/media-capture/native/photo.d.ts +4 -0
- package/registry/examples/media-capture/native/photo.dark.png +0 -0
- package/registry/examples/media-capture/native/photo.js +27 -0
- package/registry/examples/media-capture/native/photo.light.png +0 -0
- package/registry/examples/media-capture/native/photo.tsx +20 -0
- package/registry/examples/media-capture/profile-photo.d.ts +4 -0
- package/registry/examples/media-capture/profile-photo.js +24 -0
- package/registry/examples/media-capture/profile-photo.tsx +20 -0
- package/registry/examples/pagination/cursor.d.ts +4 -0
- package/registry/examples/pagination/cursor.js +24 -0
- package/registry/examples/pagination/cursor.tsx +20 -0
- package/registry/examples/pagination/links.d.ts +4 -0
- package/registry/examples/pagination/links.js +11 -0
- package/registry/examples/pagination/links.tsx +6 -0
- package/registry/examples/pagination/native/cursor.d.ts +4 -0
- package/registry/examples/pagination/native/cursor.dark.png +0 -0
- package/registry/examples/pagination/native/cursor.js +19 -0
- package/registry/examples/pagination/native/cursor.light.png +0 -0
- package/registry/examples/pagination/native/cursor.tsx +13 -0
- package/registry/examples/pagination/native/pages.d.ts +4 -0
- package/registry/examples/pagination/native/pages.dark.png +0 -0
- package/registry/examples/pagination/native/pages.js +21 -0
- package/registry/examples/pagination/native/pages.light.png +0 -0
- package/registry/examples/pagination/native/pages.tsx +14 -0
- package/registry/examples/pagination/results.d.ts +4 -0
- package/registry/examples/pagination/results.js +23 -0
- package/registry/examples/pagination/results.tsx +19 -0
- package/registry/examples/pagination/table-footer.d.ts +4 -0
- package/registry/examples/pagination/table-footer.js +31 -0
- package/registry/examples/pagination/table-footer.tsx +27 -0
- package/registry/examples/search-field/global.d.ts +4 -0
- package/registry/examples/search-field/global.js +19 -0
- package/registry/examples/search-field/global.tsx +15 -0
- package/registry/examples/search-field/native/default.d.ts +4 -0
- package/registry/examples/search-field/native/default.dark.png +0 -0
- package/registry/examples/search-field/native/default.js +13 -0
- package/registry/examples/search-field/native/default.light.png +0 -0
- package/registry/examples/search-field/native/default.tsx +7 -0
- package/registry/examples/search-field/suggestions.d.ts +4 -0
- package/registry/examples/search-field/suggestions.js +30 -0
- package/registry/examples/search-field/suggestions.tsx +27 -0
- package/registry/examples/search-field/toolbar.d.ts +4 -0
- package/registry/examples/search-field/toolbar.js +17 -0
- package/registry/examples/search-field/toolbar.tsx +21 -0
- package/registry/examples/tag-input/recipients.d.ts +4 -0
- package/registry/examples/tag-input/recipients.js +23 -0
- package/registry/examples/tag-input/recipients.tsx +20 -0
- package/registry/examples/tag-input/video-tags.d.ts +4 -0
- package/registry/examples/tag-input/video-tags.js +26 -0
- package/registry/examples/tag-input/video-tags.tsx +22 -0
- package/registry/examples/video-player/default.d.ts +4 -0
- package/registry/examples/video-player/default.js +12 -0
- package/registry/examples/video-player/default.tsx +8 -0
- package/registry/examples/video-player/gated.d.ts +4 -0
- package/registry/examples/video-player/gated.js +26 -0
- package/registry/examples/video-player/gated.tsx +23 -0
- package/registry/examples/video-player/native/default.d.ts +4 -0
- package/registry/examples/video-player/native/default.dark.png +0 -0
- package/registry/examples/video-player/native/default.js +18 -0
- package/registry/examples/video-player/native/default.light.png +0 -0
- package/registry/examples/video-player/native/default.tsx +12 -0
- package/registry/examples/video-player/signed-source.d.ts +4 -0
- package/registry/examples/video-player/signed-source.js +16 -0
- package/registry/examples/video-player/signed-source.tsx +15 -0
- package/registry/examples/video-player/vertical.d.ts +4 -0
- package/registry/examples/video-player/vertical.js +12 -0
- package/registry/examples/video-player/vertical.tsx +12 -0
- package/registry/examples/video-trimmer/custom.d.ts +4 -0
- package/registry/examples/video-trimmer/custom.js +24 -0
- package/registry/examples/video-trimmer/custom.tsx +26 -0
- package/registry/examples/video-trimmer/filmstrip.d.ts +4 -0
- package/registry/examples/video-trimmer/filmstrip.js +15 -0
- package/registry/examples/video-trimmer/filmstrip.tsx +13 -0
- package/registry/examples/video-trimmer/playhead.d.ts +4 -0
- package/registry/examples/video-trimmer/playhead.js +15 -0
- package/registry/examples/video-trimmer/playhead.tsx +13 -0
- package/registry/field.json +1 -1
- package/registry/file-upload.json +471 -0
- package/registry/filter-bar.json +347 -0
- package/registry/glow-card.json +1 -1
- package/registry/image-cropper.json +313 -0
- package/registry/index.json +255 -1
- package/registry/iso.json +1 -1
- package/registry/kbd.json +1 -1
- package/registry/loader.json +1 -1
- package/registry/marquee.json +1 -1
- package/registry/mascots.json +1 -1
- package/registry/media-capture.json +347 -0
- package/registry/meter.json +3 -3
- package/registry/nav-bar.json +1 -1
- package/registry/number-ticker.json +1 -1
- package/registry/page-header.json +1 -1
- package/registry/pagination.json +475 -0
- package/registry/popover.json +1 -1
- package/registry/progress.json +5 -5
- package/registry/radio-group.json +1 -1
- package/registry/search-field.json +436 -0
- package/registry/section-backdrop.json +1 -1
- package/registry/separator.json +1 -1
- package/registry/skeleton.json +1 -1
- package/registry/slider.json +1 -1
- package/registry/slot.json +1 -1
- package/registry/spinner.json +1 -1
- package/registry/stat.json +1 -1
- package/registry/stepper.json +1 -1
- package/registry/switch.json +1 -1
- package/registry/tabs.json +1 -1
- package/registry/tag-input.json +363 -0
- package/registry/theme.json +1 -1
- package/registry/toast.json +1 -1
- package/registry/tooltip.json +1 -1
- package/registry/video-player.json +598 -0
- package/registry/video-trimmer.json +385 -0
package/README.md
CHANGED
|
@@ -165,8 +165,23 @@ light or a product's — is a set of tokens.
|
|
|
165
165
|
| `@krizaka/ui/mascots` | `Mascot` (`bird` falcon·owl·duck·flamingo·pigeon, `mood` welcome·thinking·success·empty·error, `tone` brand·own, `size`, `animated`, `title`), `MASCOT_BIRDS`, `MASCOT_MOODS`: the Krizaka flock, in the host brand's accent — one bird at a moment that matters (onboarding, a long wait, a success, an empty list, an error); native `Mascot` in `/native` | client |
|
|
166
166
|
| `@krizaka/ui/iso` | the isometric kit: `IsoHub` (a hero driven by data: `layers`, `dome`, `satellites`, `highlight`), `IsoScene`, `IsoPrism`, `IsoStack`, `IsoDome`, `IsoTile`, `IsoLink`, `IsoNode`, `IsoLabel`, the projection helpers — static SVG animated by motion.css | server |
|
|
167
167
|
| `@krizaka/ui/motion` | `MotionObserver` (reveal, off-screen pause, settle, spotlight, magnetism) and `RotatingWord` (no layout shift) | client |
|
|
168
|
+
| `@krizaka/ui/pagination` | `Pagination` (pages: `page`, `pageCount`, `onPageChange` or `getHref` for links; cursor: `hasPrevious`/`hasNext`, `onPrevious`/`onNext`; `status`, `pageSize` + `pageSizeOptions`, `siblings`, `boundaries`, `labels`; folds into "Page 3 of 12" in a narrow container), `pageRange` | server |
|
|
169
|
+
| `@krizaka/ui/data-table` | `DataTable` (`rows`, `columns` with `header`, `cell`, `value`, `sortable`, `align`, `card` title·label·hidden; `caption`, `sort`/`defaultSort` + `onSortChange`, `manualSort`, `selectable` + `selected`, `loading`, `empty`, `error`, `layout` auto·table·cards, `density`, `virtualize`, `footer`) | client |
|
|
170
|
+
| `@krizaka/ui/filter-bar` | `FilterBar` (`label`, the controls as children, `active` chips, `onClearAll`, `results` announced), `FilterMenu` (checkboxes or radios with counts), `readFilterParams` / `writeFilterParams` (the filters in the address) | client |
|
|
171
|
+
| `@krizaka/ui/combobox` | `Combobox` (ARIA 1.2: `options` or `onSearch(query, signal)` debounced, `multiple` chips + `maxSelected`, `creatable` + `onCreate`, `separators`, `hint`, `error`, `labels`) | client |
|
|
172
|
+
| `@krizaka/ui/tag-input` | `TagInput` (free words as chips: `suggestions`, `normalize`, `validate`, `max`, `separators`, paste split) | client |
|
|
173
|
+
| `@krizaka/ui/date-picker` | `DatePicker`, `DateRangePicker` (`presets`), `Calendar` — ISO dates, the locale's names through `@krizaka/intl`, `min`/`max`/`isDateDisabled`, the APG grid keyboard | client |
|
|
174
|
+
| `@krizaka/ui/search-field` | `SearchField` (`onSearch` debounced, `onSubmit`, `shortcut` ⌘K, `suggestions`, `clearLabel`, `landmark`) | client |
|
|
175
|
+
| `@krizaka/ui/video-player` | `VideoPlayer` (`src` or a signed-source function, `loadHls={() => import("hls.js")}` (optional peer, loaded only without native HLS), `tracks`, `playbackRates`, `preload`, `pauseWhenHidden`, `overlay` for a gate, `labels`; keyboard Space/K, J/L, arrows, M, F, C) | client |
|
|
176
|
+
| `@krizaka/ui/file-upload` | `Dropzone` (`accept`, `maxSize`, `maxFiles`, refusals with reasons), `UploadList` (preview, progress, speed, pause/resume/retry/cancel), `useUploadQueue` (`upload(file, { signal, onProgress, resumed })`: Tus-ready without depending on it), `acceptsFile`, `formatBytes` | client |
|
|
177
|
+
| `@krizaka/ui/media-capture` | `MediaCapture` (`mode` photo·video·both, `facing`, `maxDuration`, permission asked on a gesture, the file picker as the way out, `onCapture({ blob, kind, url })`) | client |
|
|
178
|
+
| `@krizaka/ui/video-trimmer` | `VideoTrimmer` (`duration`, `value` [start, end], `minLength`, `step`/`largeStep`, `frames` or children, `time` + `onSeek`, `classNames` per part, `renderHandle`) | client |
|
|
179
|
+
| `@krizaka/ui/audio-player` | `AudioPlayer` (a seekable waveform: `peaks` or `computePeaks`, `bars`, `playbackRates`, `duration`), `resamplePeaks`, `computeAudioPeaks` | client |
|
|
180
|
+
| `@krizaka/ui/image-cropper` | `ImageCropper` (`aspect`, `round`, an image or a `<video>`, a focus point + zoom out, keyboard and zoom slider), `cropRect` | client |
|
|
181
|
+
| `@krizaka/ui/native/video-player` | React Native `VideoPlayer` on expo-video (optional peer) | native |
|
|
182
|
+
| `@krizaka/ui/native/media-capture` | React Native `MediaCapture` on expo-camera (optional peer) | native |
|
|
168
183
|
| `@krizaka/ui` | the marks, `MotionObserver`, `RotatingWord`, `cx` (unchanged) | client |
|
|
169
|
-
| `@krizaka/ui/native` | React Native: `ThemeProvider`/`useTheme`, `Txt`, `Button`, `IconButton`, `Card.*`, `Badge`, `Chip` (+ `Group`), `Avatar` (+ `Group`), `Skeleton`, `EmptyState`, `Spinner`, `Countdown`, `Segmented`, `Progress`, `Toaster`/`toast`, the marks — see [React Native](#react-native) | native |
|
|
184
|
+
| `@krizaka/ui/native` | React Native: `ThemeProvider`/`useTheme`, `Txt`, `Button`, `IconButton`, `Card.*`, `Badge`, `Chip` (+ `Group`), `Avatar` (+ `Group`), `Skeleton`, `EmptyState`, `Spinner`, `Countdown`, `Segmented`, `Progress`, `Toaster`/`toast`, `Pagination`, `SearchField`, the marks — see [React Native](#react-native) | native |
|
|
170
185
|
| `@krizaka/ui/motion.css`, `@krizaka/ui/tailwind.css` | the motion signature; the `@source` of the primitives' classes | CSS |
|
|
171
186
|
| `@krizaka/ui/registry/<name>`, `@krizaka/ui/registry/examples/<name>/<example>` | the registry (JSON) and the examples — see [Registry](#registry--the-code-drives-the-documentation) | data · client |
|
|
172
187
|
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import * as tailwind_variants from 'tailwind-variants';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
|
|
4
|
+
declare const audioPlayer: tailwind_variants.TVReturnType<{
|
|
5
|
+
[key: string]: {
|
|
6
|
+
[key: string]: tailwind_variants.ClassValue | {
|
|
7
|
+
root?: tailwind_variants.ClassValue;
|
|
8
|
+
body?: tailwind_variants.ClassValue;
|
|
9
|
+
title?: tailwind_variants.ClassValue;
|
|
10
|
+
bar?: tailwind_variants.ClassValue;
|
|
11
|
+
meta?: tailwind_variants.ClassValue;
|
|
12
|
+
play?: tailwind_variants.ClassValue;
|
|
13
|
+
wave?: tailwind_variants.ClassValue;
|
|
14
|
+
speed?: tailwind_variants.ClassValue;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
} | {
|
|
18
|
+
[x: string]: {
|
|
19
|
+
[x: string]: tailwind_variants.ClassValue | {
|
|
20
|
+
root?: tailwind_variants.ClassValue;
|
|
21
|
+
body?: tailwind_variants.ClassValue;
|
|
22
|
+
title?: tailwind_variants.ClassValue;
|
|
23
|
+
bar?: tailwind_variants.ClassValue;
|
|
24
|
+
meta?: tailwind_variants.ClassValue;
|
|
25
|
+
play?: tailwind_variants.ClassValue;
|
|
26
|
+
wave?: tailwind_variants.ClassValue;
|
|
27
|
+
speed?: tailwind_variants.ClassValue;
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
} | {}, {
|
|
31
|
+
root: string;
|
|
32
|
+
play: string;
|
|
33
|
+
body: string;
|
|
34
|
+
title: string;
|
|
35
|
+
wave: string;
|
|
36
|
+
bar: string;
|
|
37
|
+
meta: string;
|
|
38
|
+
speed: string;
|
|
39
|
+
}, undefined, {
|
|
40
|
+
[key: string]: {
|
|
41
|
+
[key: string]: tailwind_variants.ClassValue | {
|
|
42
|
+
root?: tailwind_variants.ClassValue;
|
|
43
|
+
body?: tailwind_variants.ClassValue;
|
|
44
|
+
title?: tailwind_variants.ClassValue;
|
|
45
|
+
bar?: tailwind_variants.ClassValue;
|
|
46
|
+
meta?: tailwind_variants.ClassValue;
|
|
47
|
+
play?: tailwind_variants.ClassValue;
|
|
48
|
+
wave?: tailwind_variants.ClassValue;
|
|
49
|
+
speed?: tailwind_variants.ClassValue;
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
} | {}, {
|
|
53
|
+
root: string;
|
|
54
|
+
play: string;
|
|
55
|
+
body: string;
|
|
56
|
+
title: string;
|
|
57
|
+
wave: string;
|
|
58
|
+
bar: string;
|
|
59
|
+
meta: string;
|
|
60
|
+
speed: string;
|
|
61
|
+
}, tailwind_variants.TVReturnTypeLike<unknown, {
|
|
62
|
+
root: string;
|
|
63
|
+
play: string;
|
|
64
|
+
body: string;
|
|
65
|
+
title: string;
|
|
66
|
+
wave: string;
|
|
67
|
+
bar: string;
|
|
68
|
+
meta: string;
|
|
69
|
+
speed: string;
|
|
70
|
+
}>>;
|
|
71
|
+
type AudioPlayerLabels = {
|
|
72
|
+
play: string;
|
|
73
|
+
pause: string;
|
|
74
|
+
/** The waveform slider ("Seek"). */
|
|
75
|
+
seek: string;
|
|
76
|
+
/** The speed button: `(rate) => "Speed " + rate + "×"`. */
|
|
77
|
+
speed: (rate: number) => string;
|
|
78
|
+
/** The slider's value: `(time, duration) => "0:42 of 3:10"`. */
|
|
79
|
+
time: (time: string, duration: string) => string;
|
|
80
|
+
};
|
|
81
|
+
declare const audioPlayerLabels: AudioPlayerLabels;
|
|
82
|
+
type AudioPlayerProps = {
|
|
83
|
+
/** The address of the sound. */
|
|
84
|
+
src: string;
|
|
85
|
+
/** What it is ("Voice note — Eric, 0:42"): shown, and the accessible name of the player. */
|
|
86
|
+
title: string;
|
|
87
|
+
/** The peaks, between 0 and 1, as many as you like (they are resampled to `bars`). */
|
|
88
|
+
peaks?: readonly number[];
|
|
89
|
+
/** Without `peaks`: fetches the file and computes them with Web Audio at the first play (same-origin or CORS). */
|
|
90
|
+
computePeaks?: boolean;
|
|
91
|
+
/** How many bars. Default 48. */
|
|
92
|
+
bars?: number;
|
|
93
|
+
/** The speeds of the speed button. Default `[1, 1.5, 2]`; `[]` hides it. */
|
|
94
|
+
playbackRates?: readonly number[];
|
|
95
|
+
/** Pauses when it leaves the screen. Default `true`. */
|
|
96
|
+
pauseWhenHidden?: boolean;
|
|
97
|
+
/** The length, in seconds, before the metadata is loaded (from the server). */
|
|
98
|
+
duration?: number;
|
|
99
|
+
/** Called when playback starts, pauses, ends. */
|
|
100
|
+
onPlayingChange?: (playing: boolean) => void;
|
|
101
|
+
/** The words, passed translated; English by default. */
|
|
102
|
+
labels?: Partial<AudioPlayerLabels>;
|
|
103
|
+
/** Classes merged on the root. */
|
|
104
|
+
className?: string;
|
|
105
|
+
};
|
|
106
|
+
/** Resamples peaks to `count` bars (the maximum of each slice), normalised to the loudest. */
|
|
107
|
+
declare function resamplePeaks(peaks: readonly number[], count: number): number[];
|
|
108
|
+
/** The peaks of a sound file, computed with Web Audio (`count` of them). */
|
|
109
|
+
declare function computeAudioPeaks(src: string, count?: number, signal?: AbortSignal): Promise<number[]>;
|
|
110
|
+
/** A sound with a seekable waveform: play / pause, time, speed — a voice note, a podcast clip, a track. */
|
|
111
|
+
declare function AudioPlayer({ src, title, peaks: given, computePeaks, bars, playbackRates, pauseWhenHidden, duration: knownDuration, onPlayingChange, labels: words, className, }: AudioPlayerProps): React.JSX.Element;
|
|
112
|
+
|
|
113
|
+
export { AudioPlayer, type AudioPlayerLabels, type AudioPlayerProps, audioPlayer, audioPlayerLabels, computeAudioPeaks, resamplePeaks };
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import {
|
|
3
|
+
clamp,
|
|
4
|
+
formatClock,
|
|
5
|
+
playSafely,
|
|
6
|
+
useInView
|
|
7
|
+
} from "./chunk-EUPTXKH7.js";
|
|
8
|
+
import {
|
|
9
|
+
PauseGlyph,
|
|
10
|
+
PlayGlyph
|
|
11
|
+
} from "./chunk-YX6CNVJA.js";
|
|
12
|
+
|
|
13
|
+
// src/audio-player/audio-player.tsx
|
|
14
|
+
import * as React from "react";
|
|
15
|
+
import { tv } from "tailwind-variants";
|
|
16
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
17
|
+
var audioPlayer = tv({
|
|
18
|
+
slots: {
|
|
19
|
+
root: "flex w-full items-center gap-3 rounded-xl border border-border-default bg-surface-1 p-3",
|
|
20
|
+
play: "inline-flex h-11 w-11 shrink-0 items-center justify-center rounded-full bg-accent text-on-accent transition-colors hover:bg-accent-hover focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-surface-1",
|
|
21
|
+
body: "flex min-w-0 flex-1 flex-col gap-1",
|
|
22
|
+
title: "truncate text-sm font-semibold text-fg",
|
|
23
|
+
wave: "relative flex h-10 w-full cursor-pointer touch-none items-center gap-[2px] rounded-md focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring",
|
|
24
|
+
bar: "min-w-[2px] flex-1 rounded-full bg-border-strong data-[played]:bg-accent",
|
|
25
|
+
meta: "flex items-center justify-between font-mono text-[11px] tabular-nums text-fg-secondary",
|
|
26
|
+
speed: "shrink-0 rounded-md px-2 py-1 text-xs font-semibold tabular-nums text-fg-secondary hover:bg-surface-3 hover:text-fg focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
var audioPlayerLabels = {
|
|
30
|
+
play: "Play",
|
|
31
|
+
pause: "Pause",
|
|
32
|
+
seek: "Seek",
|
|
33
|
+
speed: (rate) => `Speed ${rate}\xD7`,
|
|
34
|
+
time: (time, duration) => `${time} of ${duration}`
|
|
35
|
+
};
|
|
36
|
+
function resamplePeaks(peaks, count) {
|
|
37
|
+
if (peaks.length === 0) return Array.from({ length: count }, () => 0.15);
|
|
38
|
+
const out = Array.from({ length: count }, (_, i) => {
|
|
39
|
+
const from = Math.floor(i / count * peaks.length);
|
|
40
|
+
const to = Math.max(from + 1, Math.floor((i + 1) / count * peaks.length));
|
|
41
|
+
return Math.max(...peaks.slice(from, to).map(Math.abs));
|
|
42
|
+
});
|
|
43
|
+
const loudest = Math.max(...out, 1e-4);
|
|
44
|
+
return out.map((v) => Math.max(0.08, v / loudest));
|
|
45
|
+
}
|
|
46
|
+
async function computeAudioPeaks(src, count = 200, signal) {
|
|
47
|
+
const response = await fetch(src, { signal });
|
|
48
|
+
const data = await response.arrayBuffer();
|
|
49
|
+
const Context = window.AudioContext ?? window.webkitAudioContext;
|
|
50
|
+
const context = new Context();
|
|
51
|
+
try {
|
|
52
|
+
const buffer = await context.decodeAudioData(data);
|
|
53
|
+
const channel = buffer.getChannelData(0);
|
|
54
|
+
const size = Math.max(1, Math.floor(channel.length / count));
|
|
55
|
+
return Array.from({ length: count }, (_, i) => {
|
|
56
|
+
let peak = 0;
|
|
57
|
+
for (let j = i * size; j < Math.min(channel.length, (i + 1) * size); j += 1) peak = Math.max(peak, Math.abs(channel[j]));
|
|
58
|
+
return peak;
|
|
59
|
+
});
|
|
60
|
+
} finally {
|
|
61
|
+
void context.close();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function AudioPlayer({
|
|
65
|
+
src,
|
|
66
|
+
title,
|
|
67
|
+
peaks: given,
|
|
68
|
+
computePeaks,
|
|
69
|
+
bars = 48,
|
|
70
|
+
playbackRates = [1, 1.5, 2],
|
|
71
|
+
pauseWhenHidden = true,
|
|
72
|
+
duration: knownDuration = 0,
|
|
73
|
+
onPlayingChange,
|
|
74
|
+
labels: words,
|
|
75
|
+
className
|
|
76
|
+
}) {
|
|
77
|
+
const labels = { ...audioPlayerLabels, ...words };
|
|
78
|
+
const s = audioPlayer();
|
|
79
|
+
const audio = React.useRef(null);
|
|
80
|
+
const root = React.useRef(null);
|
|
81
|
+
const wave = React.useRef(null);
|
|
82
|
+
const dragging = React.useRef(false);
|
|
83
|
+
const [playing, setPlaying] = React.useState(false);
|
|
84
|
+
const [time, setTime] = React.useState(0);
|
|
85
|
+
const [duration, setDuration] = React.useState(knownDuration);
|
|
86
|
+
const [rate, setRate] = React.useState(playbackRates[0] ?? 1);
|
|
87
|
+
const [computed, setComputed] = React.useState(null);
|
|
88
|
+
const [wantPeaks, setWantPeaks] = React.useState(false);
|
|
89
|
+
React.useEffect(() => {
|
|
90
|
+
if (given || !computePeaks || !wantPeaks) return;
|
|
91
|
+
const controller = new AbortController();
|
|
92
|
+
computeAudioPeaks(src, 200, controller.signal).then(setComputed, () => void 0);
|
|
93
|
+
return () => controller.abort();
|
|
94
|
+
}, [given, computePeaks, wantPeaks, src]);
|
|
95
|
+
useInView(root, (visible) => !visible && audio.current && !audio.current.paused && audio.current.pause(), pauseWhenHidden);
|
|
96
|
+
const shape = React.useMemo(() => resamplePeaks(given ?? computed ?? [], bars), [given, computed, bars]);
|
|
97
|
+
const progress = duration > 0 ? time / duration : 0;
|
|
98
|
+
const seek = (to) => {
|
|
99
|
+
if (!audio.current) return;
|
|
100
|
+
const next = clamp(to, 0, duration || audio.current.duration || 0);
|
|
101
|
+
audio.current.currentTime = next;
|
|
102
|
+
setTime(next);
|
|
103
|
+
};
|
|
104
|
+
const at = (clientX) => {
|
|
105
|
+
const box = wave.current?.getBoundingClientRect();
|
|
106
|
+
return box && box.width ? (clientX - box.left) / box.width * duration : 0;
|
|
107
|
+
};
|
|
108
|
+
const toggle = () => {
|
|
109
|
+
setWantPeaks(true);
|
|
110
|
+
if (!audio.current) return;
|
|
111
|
+
if (audio.current.paused) playSafely(audio.current);
|
|
112
|
+
else audio.current.pause();
|
|
113
|
+
};
|
|
114
|
+
return /* @__PURE__ */ jsxs("div", { ref: root, role: "group", "aria-label": title, className: s.root({ className }), children: [
|
|
115
|
+
/* @__PURE__ */ jsx(
|
|
116
|
+
"audio",
|
|
117
|
+
{
|
|
118
|
+
ref: audio,
|
|
119
|
+
src,
|
|
120
|
+
preload: "metadata",
|
|
121
|
+
onPlay: () => {
|
|
122
|
+
setPlaying(true);
|
|
123
|
+
onPlayingChange?.(true);
|
|
124
|
+
},
|
|
125
|
+
onPause: () => {
|
|
126
|
+
setPlaying(false);
|
|
127
|
+
onPlayingChange?.(false);
|
|
128
|
+
},
|
|
129
|
+
onTimeUpdate: (event) => setTime(event.currentTarget.currentTime),
|
|
130
|
+
onDurationChange: (event) => Number.isFinite(event.currentTarget.duration) && setDuration(event.currentTarget.duration)
|
|
131
|
+
}
|
|
132
|
+
),
|
|
133
|
+
/* @__PURE__ */ jsx("button", { type: "button", className: s.play(), "aria-label": playing ? labels.pause : labels.play, onClick: toggle, children: playing ? /* @__PURE__ */ jsx(PauseGlyph, { className: "h-5 w-5" }) : /* @__PURE__ */ jsx(PlayGlyph, { className: "ml-0.5 h-5 w-5" }) }),
|
|
134
|
+
/* @__PURE__ */ jsxs("div", { className: s.body(), children: [
|
|
135
|
+
/* @__PURE__ */ jsx("p", { className: s.title(), "aria-hidden": true, children: title }),
|
|
136
|
+
/* @__PURE__ */ jsx(
|
|
137
|
+
"div",
|
|
138
|
+
{
|
|
139
|
+
ref: wave,
|
|
140
|
+
role: "slider",
|
|
141
|
+
tabIndex: 0,
|
|
142
|
+
"aria-label": labels.seek,
|
|
143
|
+
"aria-valuemin": 0,
|
|
144
|
+
"aria-valuemax": Math.round(duration),
|
|
145
|
+
"aria-valuenow": Math.round(time),
|
|
146
|
+
"aria-valuetext": labels.time(formatClock(time), formatClock(duration)),
|
|
147
|
+
className: s.wave(),
|
|
148
|
+
onPointerDown: (event) => {
|
|
149
|
+
dragging.current = true;
|
|
150
|
+
wave.current?.setPointerCapture?.(event.pointerId);
|
|
151
|
+
seek(at(event.clientX));
|
|
152
|
+
},
|
|
153
|
+
onPointerMove: (event) => dragging.current && seek(at(event.clientX)),
|
|
154
|
+
onPointerUp: () => {
|
|
155
|
+
dragging.current = false;
|
|
156
|
+
},
|
|
157
|
+
onKeyDown: (event) => {
|
|
158
|
+
const moves = {
|
|
159
|
+
ArrowLeft: time - 5,
|
|
160
|
+
ArrowDown: time - 5,
|
|
161
|
+
ArrowRight: time + 5,
|
|
162
|
+
ArrowUp: time + 5,
|
|
163
|
+
PageDown: time - 30,
|
|
164
|
+
PageUp: time + 30,
|
|
165
|
+
Home: 0,
|
|
166
|
+
End: duration
|
|
167
|
+
};
|
|
168
|
+
const to = moves[event.key];
|
|
169
|
+
if (to === void 0) return;
|
|
170
|
+
event.preventDefault();
|
|
171
|
+
seek(to);
|
|
172
|
+
},
|
|
173
|
+
children: shape.map((peak, i) => /* @__PURE__ */ jsx("span", { "aria-hidden": true, "data-played": (i + 0.5) / bars <= progress ? "" : void 0, className: s.bar(), style: { height: `${Math.round(peak * 100)}%` } }, i))
|
|
174
|
+
}
|
|
175
|
+
),
|
|
176
|
+
/* @__PURE__ */ jsxs("div", { className: s.meta(), "aria-hidden": true, children: [
|
|
177
|
+
/* @__PURE__ */ jsx("span", { children: formatClock(time) }),
|
|
178
|
+
/* @__PURE__ */ jsx("span", { children: formatClock(duration) })
|
|
179
|
+
] })
|
|
180
|
+
] }),
|
|
181
|
+
playbackRates.length > 1 && /* @__PURE__ */ jsxs(
|
|
182
|
+
"button",
|
|
183
|
+
{
|
|
184
|
+
type: "button",
|
|
185
|
+
className: s.speed(),
|
|
186
|
+
"aria-label": labels.speed(rate),
|
|
187
|
+
onClick: () => {
|
|
188
|
+
const next = playbackRates[(playbackRates.indexOf(rate) + 1) % playbackRates.length] ?? 1;
|
|
189
|
+
if (audio.current) audio.current.playbackRate = next;
|
|
190
|
+
setRate(next);
|
|
191
|
+
},
|
|
192
|
+
children: [
|
|
193
|
+
rate,
|
|
194
|
+
"\xD7"
|
|
195
|
+
]
|
|
196
|
+
}
|
|
197
|
+
)
|
|
198
|
+
] });
|
|
199
|
+
}
|
|
200
|
+
export {
|
|
201
|
+
AudioPlayer,
|
|
202
|
+
audioPlayer,
|
|
203
|
+
audioPlayerLabels,
|
|
204
|
+
computeAudioPeaks,
|
|
205
|
+
resamplePeaks
|
|
206
|
+
};
|
|
207
|
+
//# sourceMappingURL=audio-player.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/audio-player/audio-player.tsx"],"sourcesContent":["// Client. An `<audio>` with a waveform you can seek on: the bars are the sound's peaks (given by the server, or\n// computed here from the file with Web Audio when `computePeaks` — once, on demand), the played part in the accent.\n// The waveform is a `slider` (arrows ± 5 s, Page keys ± 30 s, Home / End) whose value is read as a time; the play\n// button and the speed are named by props. No animation but the played part's colour, and it pauses off screen.\nimport * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nimport { PauseGlyph, PlayGlyph } from \"../glyphs\";\nimport { clamp, formatClock, playSafely, useInView } from \"../media\";\n\nexport const audioPlayer = tv({\n slots: {\n root: \"flex w-full items-center gap-3 rounded-xl border border-border-default bg-surface-1 p-3\",\n play:\n \"inline-flex h-11 w-11 shrink-0 items-center justify-center rounded-full bg-accent text-on-accent transition-colors hover:bg-accent-hover \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-surface-1\",\n body: \"flex min-w-0 flex-1 flex-col gap-1\",\n title: \"truncate text-sm font-semibold text-fg\",\n wave:\n \"relative flex h-10 w-full cursor-pointer touch-none items-center gap-[2px] rounded-md \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\",\n bar: \"min-w-[2px] flex-1 rounded-full bg-border-strong data-[played]:bg-accent\",\n meta: \"flex items-center justify-between font-mono text-[11px] tabular-nums text-fg-secondary\",\n speed:\n \"shrink-0 rounded-md px-2 py-1 text-xs font-semibold tabular-nums text-fg-secondary hover:bg-surface-3 hover:text-fg \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\",\n },\n});\n\nexport type AudioPlayerLabels = {\n play: string;\n pause: string;\n /** The waveform slider (\"Seek\"). */\n seek: string;\n /** The speed button: `(rate) => \"Speed \" + rate + \"×\"`. */\n speed: (rate: number) => string;\n /** The slider's value: `(time, duration) => \"0:42 of 3:10\"`. */\n time: (time: string, duration: string) => string;\n};\n\nexport const audioPlayerLabels: AudioPlayerLabels = {\n play: \"Play\",\n pause: \"Pause\",\n seek: \"Seek\",\n speed: (rate) => `Speed ${rate}×`,\n time: (time, duration) => `${time} of ${duration}`,\n};\n\nexport type AudioPlayerProps = {\n /** The address of the sound. */\n src: string;\n /** What it is (\"Voice note — Eric, 0:42\"): shown, and the accessible name of the player. */\n title: string;\n /** The peaks, between 0 and 1, as many as you like (they are resampled to `bars`). */\n peaks?: readonly number[];\n /** Without `peaks`: fetches the file and computes them with Web Audio at the first play (same-origin or CORS). */\n computePeaks?: boolean;\n /** How many bars. Default 48. */\n bars?: number;\n /** The speeds of the speed button. Default `[1, 1.5, 2]`; `[]` hides it. */\n playbackRates?: readonly number[];\n /** Pauses when it leaves the screen. Default `true`. */\n pauseWhenHidden?: boolean;\n /** The length, in seconds, before the metadata is loaded (from the server). */\n duration?: number;\n /** Called when playback starts, pauses, ends. */\n onPlayingChange?: (playing: boolean) => void;\n /** The words, passed translated; English by default. */\n labels?: Partial<AudioPlayerLabels>;\n /** Classes merged on the root. */\n className?: string;\n};\n\n/** Resamples peaks to `count` bars (the maximum of each slice), normalised to the loudest. */\nexport function resamplePeaks(peaks: readonly number[], count: number): number[] {\n if (peaks.length === 0) return Array.from({ length: count }, () => 0.15);\n const out = Array.from({ length: count }, (_, i) => {\n const from = Math.floor((i / count) * peaks.length);\n const to = Math.max(from + 1, Math.floor(((i + 1) / count) * peaks.length));\n return Math.max(...peaks.slice(from, to).map(Math.abs));\n });\n const loudest = Math.max(...out, 0.0001);\n return out.map((v) => Math.max(0.08, v / loudest));\n}\n\n/** The peaks of a sound file, computed with Web Audio (`count` of them). */\nexport async function computeAudioPeaks(src: string, count = 200, signal?: AbortSignal): Promise<number[]> {\n const response = await fetch(src, { signal });\n const data = await response.arrayBuffer();\n const Context = window.AudioContext ?? (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext;\n const context = new Context();\n try {\n const buffer = await context.decodeAudioData(data);\n const channel = buffer.getChannelData(0);\n const size = Math.max(1, Math.floor(channel.length / count));\n return Array.from({ length: count }, (_, i) => {\n let peak = 0;\n for (let j = i * size; j < Math.min(channel.length, (i + 1) * size); j += 1) peak = Math.max(peak, Math.abs(channel[j]!));\n return peak;\n });\n } finally {\n void context.close();\n }\n}\n\n/** A sound with a seekable waveform: play / pause, time, speed — a voice note, a podcast clip, a track. */\nexport function AudioPlayer({\n src,\n title,\n peaks: given,\n computePeaks,\n bars = 48,\n playbackRates = [1, 1.5, 2],\n pauseWhenHidden = true,\n duration: knownDuration = 0,\n onPlayingChange,\n labels: words,\n className,\n}: AudioPlayerProps) {\n const labels = { ...audioPlayerLabels, ...words };\n const s = audioPlayer();\n const audio = React.useRef<HTMLAudioElement>(null);\n const root = React.useRef<HTMLDivElement>(null);\n const wave = React.useRef<HTMLDivElement>(null);\n const dragging = React.useRef(false);\n const [playing, setPlaying] = React.useState(false);\n const [time, setTime] = React.useState(0);\n const [duration, setDuration] = React.useState(knownDuration);\n const [rate, setRate] = React.useState(playbackRates[0] ?? 1);\n const [computed, setComputed] = React.useState<number[] | null>(null);\n const [wantPeaks, setWantPeaks] = React.useState(false);\n\n React.useEffect(() => {\n if (given || !computePeaks || !wantPeaks) return;\n const controller = new AbortController();\n computeAudioPeaks(src, 200, controller.signal).then(setComputed, () => undefined);\n return () => controller.abort();\n }, [given, computePeaks, wantPeaks, src]);\n\n useInView(root, (visible) => !visible && audio.current && !audio.current.paused && audio.current.pause(), pauseWhenHidden);\n\n const shape = React.useMemo(() => resamplePeaks(given ?? computed ?? [], bars), [given, computed, bars]);\n const progress = duration > 0 ? time / duration : 0;\n\n const seek = (to: number) => {\n if (!audio.current) return;\n const next = clamp(to, 0, duration || audio.current.duration || 0);\n audio.current.currentTime = next;\n setTime(next);\n };\n const at = (clientX: number) => {\n const box = wave.current?.getBoundingClientRect();\n return box && box.width ? ((clientX - box.left) / box.width) * duration : 0;\n };\n const toggle = () => {\n setWantPeaks(true);\n if (!audio.current) return;\n if (audio.current.paused) playSafely(audio.current);\n else audio.current.pause();\n };\n\n return (\n <div ref={root} role=\"group\" aria-label={title} className={s.root({ className })}>\n <audio\n ref={audio}\n src={src}\n preload=\"metadata\"\n onPlay={() => {\n setPlaying(true);\n onPlayingChange?.(true);\n }}\n onPause={() => {\n setPlaying(false);\n onPlayingChange?.(false);\n }}\n onTimeUpdate={(event) => setTime(event.currentTarget.currentTime)}\n onDurationChange={(event) => Number.isFinite(event.currentTarget.duration) && setDuration(event.currentTarget.duration)}\n />\n <button type=\"button\" className={s.play()} aria-label={playing ? labels.pause : labels.play} onClick={toggle}>\n {playing ? <PauseGlyph className=\"h-5 w-5\" /> : <PlayGlyph className=\"ml-0.5 h-5 w-5\" />}\n </button>\n <div className={s.body()}>\n <p className={s.title()} aria-hidden>\n {title}\n </p>\n <div\n ref={wave}\n role=\"slider\"\n tabIndex={0}\n aria-label={labels.seek}\n aria-valuemin={0}\n aria-valuemax={Math.round(duration)}\n aria-valuenow={Math.round(time)}\n aria-valuetext={labels.time(formatClock(time), formatClock(duration))}\n className={s.wave()}\n onPointerDown={(event) => {\n dragging.current = true;\n wave.current?.setPointerCapture?.(event.pointerId);\n seek(at(event.clientX));\n }}\n onPointerMove={(event) => dragging.current && seek(at(event.clientX))}\n onPointerUp={() => {\n dragging.current = false;\n }}\n onKeyDown={(event) => {\n const moves: Record<string, number> = {\n ArrowLeft: time - 5,\n ArrowDown: time - 5,\n ArrowRight: time + 5,\n ArrowUp: time + 5,\n PageDown: time - 30,\n PageUp: time + 30,\n Home: 0,\n End: duration,\n };\n const to = moves[event.key];\n if (to === undefined) return;\n event.preventDefault();\n seek(to);\n }}\n >\n {shape.map((peak, i) => (\n <span key={i} aria-hidden data-played={(i + 0.5) / bars <= progress ? \"\" : undefined} className={s.bar()} style={{ height: `${Math.round(peak * 100)}%` }} />\n ))}\n </div>\n <div className={s.meta()} aria-hidden>\n <span>{formatClock(time)}</span>\n <span>{formatClock(duration)}</span>\n </div>\n </div>\n {playbackRates.length > 1 && (\n <button\n type=\"button\"\n className={s.speed()}\n aria-label={labels.speed(rate)}\n onClick={() => {\n const next = playbackRates[(playbackRates.indexOf(rate) + 1) % playbackRates.length] ?? 1;\n if (audio.current) audio.current.playbackRate = next;\n setRate(next);\n }}\n >\n {rate}×\n </button>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/cn.ts
|
|
4
|
+
import { clsx } from "clsx";
|
|
5
|
+
import { cn as merge } from "tailwind-variants";
|
|
6
|
+
function cn(...inputs) {
|
|
7
|
+
return merge(clsx(inputs)) ?? "";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// src/floating.ts
|
|
11
|
+
var floating = "kz-pop z-50 rounded-lg border border-border-default bg-surface-2 text-fg shadow-lg outline-hidden";
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
cn,
|
|
15
|
+
floating
|
|
16
|
+
};
|
|
17
|
+
//# sourceMappingURL=chunk-2USC2YT6.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cn.ts","../src/floating.ts"],"sourcesContent":["import { type ClassValue, clsx } from \"clsx\";\nimport { cn as merge } from \"tailwind-variants\";\n\nexport type { ClassValue };\n\n/**\n * Merges Tailwind classes: the last one wins — a product's `className` overrides the primitive's. The merge engine is\n * tailwind-merge as tailwind-variants ships it (the one `tv` uses), so the platform carries a single copy of it.\n * tailwind-variants bundles that engine since 3.3 (it never imports the `tailwind-merge` package, an optional peer):\n * hence `tailwind-variants ^3.3.1` and no `tailwind-merge` dependency — `scripts/consumer.mjs` checks it on a clean\n * install of the packed package.\n */\nexport function cn(...inputs: ClassValue[]): string {\n return merge(clsx(inputs)) ?? \"\";\n}\n","// The one surface of everything that floats next to its trigger (popover, menu, tooltip): a raised surface, a border,\n// the large shadow and the `kz-pop` entrance from the side Radix measured (`--radix-*-transform-origin`).\nexport const floating =\n \"kz-pop z-50 rounded-lg border border-border-default bg-surface-2 text-fg shadow-lg outline-hidden\";\n"],"mappings":";;;AAAA,SAA0B,YAAY;AACtC,SAAS,MAAM,aAAa;AAWrB,SAAS,MAAM,QAA8B;AAClD,SAAO,MAAM,KAAK,MAAM,CAAC,KAAK;AAChC;;;ACZO,IAAM,WACX;","names":[]}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/media.ts
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
function formatClock(seconds) {
|
|
6
|
+
const s = Number.isFinite(seconds) && seconds > 0 ? Math.floor(seconds) : 0;
|
|
7
|
+
const h = Math.floor(s / 3600);
|
|
8
|
+
const m = Math.floor(s % 3600 / 60);
|
|
9
|
+
const rest = String(s % 60).padStart(2, "0");
|
|
10
|
+
return h > 0 ? `${h}:${String(m).padStart(2, "0")}:${rest}` : `${m}:${rest}`;
|
|
11
|
+
}
|
|
12
|
+
var clamp = (value, min, max) => Math.min(max, Math.max(min, value));
|
|
13
|
+
var REDUCE = "(prefers-reduced-motion: reduce)";
|
|
14
|
+
var subscribeReduce = (notify) => {
|
|
15
|
+
if (typeof window === "undefined" || !window.matchMedia) return () => void 0;
|
|
16
|
+
const query = window.matchMedia(REDUCE);
|
|
17
|
+
query.addEventListener?.("change", notify);
|
|
18
|
+
return () => query.removeEventListener?.("change", notify);
|
|
19
|
+
};
|
|
20
|
+
function usePrefersReducedMotion() {
|
|
21
|
+
return React.useSyncExternalStore(
|
|
22
|
+
subscribeReduce,
|
|
23
|
+
() => typeof window !== "undefined" && window.matchMedia ? window.matchMedia(REDUCE).matches : false,
|
|
24
|
+
() => false
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
function useLatest(value) {
|
|
28
|
+
const ref = React.useRef(value);
|
|
29
|
+
React.useLayoutEffect(() => {
|
|
30
|
+
ref.current = value;
|
|
31
|
+
});
|
|
32
|
+
return ref;
|
|
33
|
+
}
|
|
34
|
+
function useInView(ref, onChange, enabled = true, threshold = 0.25) {
|
|
35
|
+
const latest = useLatest(onChange);
|
|
36
|
+
React.useEffect(() => {
|
|
37
|
+
const element = ref.current;
|
|
38
|
+
if (!enabled || !element || typeof IntersectionObserver === "undefined") return;
|
|
39
|
+
const observer = new IntersectionObserver(([entry]) => entry && latest.current(entry.isIntersecting), { threshold });
|
|
40
|
+
observer.observe(element);
|
|
41
|
+
return () => observer.disconnect();
|
|
42
|
+
}, [ref, enabled, threshold, latest]);
|
|
43
|
+
}
|
|
44
|
+
function useDebouncedCallback(callback, delay) {
|
|
45
|
+
const latest = useLatest(callback);
|
|
46
|
+
const [debounced] = React.useState(() => {
|
|
47
|
+
let timer;
|
|
48
|
+
let wait = delay;
|
|
49
|
+
const run = (...args) => {
|
|
50
|
+
clearTimeout(timer);
|
|
51
|
+
if (wait <= 0) latest.current(...args);
|
|
52
|
+
else timer = setTimeout(() => latest.current(...args), wait);
|
|
53
|
+
};
|
|
54
|
+
return Object.assign(run, {
|
|
55
|
+
cancel: () => clearTimeout(timer),
|
|
56
|
+
setDelay: (next) => {
|
|
57
|
+
wait = next;
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
React.useEffect(() => debounced.setDelay(delay), [debounced, delay]);
|
|
62
|
+
React.useEffect(() => () => debounced.cancel(), [debounced]);
|
|
63
|
+
return debounced;
|
|
64
|
+
}
|
|
65
|
+
function useControllable(value, defaultValue, onChange) {
|
|
66
|
+
const [inner, setInner] = React.useState(defaultValue);
|
|
67
|
+
const controlled = value !== void 0;
|
|
68
|
+
const current = controlled ? value : inner;
|
|
69
|
+
const set = React.useCallback(
|
|
70
|
+
(next) => {
|
|
71
|
+
if (!controlled) setInner(next);
|
|
72
|
+
onChange?.(next);
|
|
73
|
+
},
|
|
74
|
+
[controlled, onChange]
|
|
75
|
+
);
|
|
76
|
+
return [current, set];
|
|
77
|
+
}
|
|
78
|
+
function playSafely(media) {
|
|
79
|
+
try {
|
|
80
|
+
const started = media?.play();
|
|
81
|
+
started?.catch?.(() => void 0);
|
|
82
|
+
} catch {
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export {
|
|
87
|
+
formatClock,
|
|
88
|
+
clamp,
|
|
89
|
+
usePrefersReducedMotion,
|
|
90
|
+
useInView,
|
|
91
|
+
useDebouncedCallback,
|
|
92
|
+
useControllable,
|
|
93
|
+
playSafely
|
|
94
|
+
};
|
|
95
|
+
//# sourceMappingURL=chunk-EUPTXKH7.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/media.ts"],"sourcesContent":["// Shared by the data and media primitives (client): the clock format, the reduced-motion preference, the\n// in-view observer, a debounced callback. Never an entry of its own.\nimport * as React from \"react\";\n\n/** Seconds as a clock: `75` → `1:15`, `3725` → `1:02:05`. Negative or unknown values read `0:00`. */\nexport function formatClock(seconds: number): string {\n const s = Number.isFinite(seconds) && seconds > 0 ? Math.floor(seconds) : 0;\n const h = Math.floor(s / 3600);\n const m = Math.floor((s % 3600) / 60);\n const rest = String(s % 60).padStart(2, \"0\");\n return h > 0 ? `${h}:${String(m).padStart(2, \"0\")}:${rest}` : `${m}:${rest}`;\n}\n\n/** Clamps `value` between `min` and `max`. */\nexport const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));\n\nconst REDUCE = \"(prefers-reduced-motion: reduce)\";\nconst subscribeReduce = (notify: () => void) => {\n if (typeof window === \"undefined\" || !window.matchMedia) return () => undefined;\n const query = window.matchMedia(REDUCE);\n query.addEventListener?.(\"change\", notify);\n return () => query.removeEventListener?.(\"change\", notify);\n};\n\n/** `true` when the person asked for less motion (`prefers-reduced-motion: reduce`); follows a change of the setting. */\nexport function usePrefersReducedMotion(): boolean {\n return React.useSyncExternalStore(\n subscribeReduce,\n () => (typeof window !== \"undefined\" && window.matchMedia ? window.matchMedia(REDUCE).matches : false),\n () => false,\n );\n}\n\n/** The latest value of a callback, for an effect or an event that must not re-subscribe when it changes. */\nexport function useLatest<T>(value: T) {\n const ref = React.useRef(value);\n React.useLayoutEffect(() => {\n ref.current = value;\n });\n return ref;\n}\n\n/** Calls `onChange(visible)` when at least `threshold` of the element enters or leaves the viewport. */\nexport function useInView(ref: React.RefObject<Element | null>, onChange: (visible: boolean) => void, enabled = true, threshold = 0.25) {\n const latest = useLatest(onChange);\n React.useEffect(() => {\n const element = ref.current;\n if (!enabled || !element || typeof IntersectionObserver === \"undefined\") return;\n const observer = new IntersectionObserver(([entry]) => entry && latest.current(entry.isIntersecting), { threshold });\n observer.observe(element);\n return () => observer.disconnect();\n }, [ref, enabled, threshold, latest]);\n}\n\n/** A stable function that runs `callback` once `delay` ms have passed without a new call; `cancel()` drops the pending one. */\nexport function useDebouncedCallback<A extends unknown[]>(callback: (...args: A) => void, delay: number) {\n const latest = useLatest(callback);\n const [debounced] = React.useState(() => {\n let timer: ReturnType<typeof setTimeout> | undefined;\n let wait = delay;\n const run = (...args: A) => {\n clearTimeout(timer);\n if (wait <= 0) latest.current(...args);\n else timer = setTimeout(() => latest.current(...args), wait);\n };\n return Object.assign(run, {\n cancel: () => clearTimeout(timer),\n setDelay: (next: number) => {\n wait = next;\n },\n });\n });\n React.useEffect(() => debounced.setDelay(delay), [debounced, delay]);\n React.useEffect(() => () => debounced.cancel(), [debounced]);\n return debounced;\n}\n\n/** A value controlled by a prop when it is given, held inside otherwise; `set` calls `onChange` either way. */\nexport function useControllable<T>(value: T | undefined, defaultValue: T, onChange?: (next: T) => void) {\n const [inner, setInner] = React.useState(defaultValue);\n const controlled = value !== undefined;\n const current = controlled ? value : inner;\n const set = React.useCallback(\n (next: T) => {\n if (!controlled) setInner(next);\n onChange?.(next);\n },\n [controlled, onChange],\n );\n return [current, set] as const;\n}\n\n/** Starts a media element without an unhandled rejection (autoplay refused, the source replaced meanwhile). */\nexport function playSafely(media: HTMLMediaElement | null | undefined) {\n try {\n const started = media?.play() as Promise<void> | undefined;\n started?.catch?.(() => undefined);\n } catch {\n // Not playable here (no media support): the controls stay as they are.\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/slider/slider.tsx
|
|
4
|
+
import { Slider as SliderPrimitive } from "radix-ui";
|
|
5
|
+
import * as React from "react";
|
|
6
|
+
import { tv } from "tailwind-variants";
|
|
7
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
var slider = tv({
|
|
9
|
+
slots: {
|
|
10
|
+
root: "flex flex-col gap-2",
|
|
11
|
+
header: "flex items-baseline justify-between gap-3 text-xs font-semibold text-fg-secondary",
|
|
12
|
+
value: "font-mono text-[11px] tabular-nums text-fg-secondary",
|
|
13
|
+
control: "relative flex h-6 w-full touch-none select-none items-center data-disabled:cursor-not-allowed data-disabled:opacity-40",
|
|
14
|
+
track: "relative h-1.5 w-full grow overflow-hidden rounded-full bg-surface-3",
|
|
15
|
+
range: "absolute h-full rounded-full bg-linear-to-r from-accent to-accent-2",
|
|
16
|
+
thumb: "block h-5 w-5 cursor-grab rounded-full border border-border-strong bg-fg-on-media shadow-md transition-[box-shadow] focus-visible:outline-hidden focus-visible:ring-4 focus-visible:ring-ring/50 active:cursor-grabbing"
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
var toArray = (v) => v === void 0 ? void 0 : typeof v === "number" ? [v] : v;
|
|
20
|
+
function Slider({
|
|
21
|
+
value,
|
|
22
|
+
defaultValue,
|
|
23
|
+
onValueChange,
|
|
24
|
+
onValueCommit,
|
|
25
|
+
label,
|
|
26
|
+
thumbLabels,
|
|
27
|
+
formatValue = String,
|
|
28
|
+
showLabel,
|
|
29
|
+
origin,
|
|
30
|
+
min = 0,
|
|
31
|
+
max = 100,
|
|
32
|
+
className,
|
|
33
|
+
...props
|
|
34
|
+
}) {
|
|
35
|
+
const s = slider();
|
|
36
|
+
const range = Array.isArray(value ?? defaultValue);
|
|
37
|
+
const [inner, setInner] = React.useState(() => toArray(defaultValue) ?? [min]);
|
|
38
|
+
const current = toArray(value) ?? inner;
|
|
39
|
+
const out = (next) => range ? next : next[0];
|
|
40
|
+
const pct = (v) => (v - min) / (max - min || 1) * 100;
|
|
41
|
+
const fill = origin !== void 0 && !range ? { left: `${Math.min(pct(origin), pct(current[0]))}%`, right: `${100 - Math.max(pct(origin), pct(current[0]))}%` } : void 0;
|
|
42
|
+
const id = React.useId();
|
|
43
|
+
return /* @__PURE__ */ jsxs("div", { className: s.root({ className }), "data-range": range ? "" : void 0, children: [
|
|
44
|
+
showLabel && /* @__PURE__ */ jsxs("div", { className: s.header(), children: [
|
|
45
|
+
/* @__PURE__ */ jsx("span", { id: `${id}-label`, children: label }),
|
|
46
|
+
/* @__PURE__ */ jsx("span", { className: s.value(), "aria-hidden": true, children: current.map(formatValue).join(" \u2013 ") })
|
|
47
|
+
] }),
|
|
48
|
+
/* @__PURE__ */ jsxs(
|
|
49
|
+
SliderPrimitive.Root,
|
|
50
|
+
{
|
|
51
|
+
min,
|
|
52
|
+
max,
|
|
53
|
+
value: current,
|
|
54
|
+
onValueChange: (next) => {
|
|
55
|
+
if (value === void 0) setInner(next);
|
|
56
|
+
onValueChange?.(out(next));
|
|
57
|
+
},
|
|
58
|
+
onValueCommit: onValueCommit && ((next) => onValueCommit(out(next))),
|
|
59
|
+
className: s.control(),
|
|
60
|
+
...props,
|
|
61
|
+
children: [
|
|
62
|
+
/* @__PURE__ */ jsx(SliderPrimitive.Track, { className: s.track(), children: fill ? /* @__PURE__ */ jsx("span", { "data-origin": "", className: s.range(), style: fill }) : /* @__PURE__ */ jsx(SliderPrimitive.Range, { className: s.range() }) }),
|
|
63
|
+
current.map((v, i) => /* @__PURE__ */ jsx(
|
|
64
|
+
SliderPrimitive.Thumb,
|
|
65
|
+
{
|
|
66
|
+
"aria-label": range ? thumbLabels?.[i] ?? label : showLabel ? void 0 : label,
|
|
67
|
+
"aria-labelledby": !range && showLabel ? `${id}-label` : void 0,
|
|
68
|
+
"aria-valuetext": formatValue(v),
|
|
69
|
+
className: s.thumb()
|
|
70
|
+
},
|
|
71
|
+
i
|
|
72
|
+
))
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
)
|
|
76
|
+
] });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export {
|
|
80
|
+
Slider
|
|
81
|
+
};
|
|
82
|
+
//# sourceMappingURL=chunk-RUPRYJ45.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/slider/slider.tsx"],"sourcesContent":["// Client. Radix Slider: role=\"slider\" on each thumb, arrows / Page Up / Page Down / Home / End, a range with two\n// thumbs. The words arrive as props: `label` names the thumb(s), `formatValue` gives the aria-valuetext and the value\n// shown next to a visible label.\nimport { Slider as SliderPrimitive } from \"radix-ui\";\nimport * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nexport const slider = tv({\n slots: {\n root: \"flex flex-col gap-2\",\n header: \"flex items-baseline justify-between gap-3 text-xs font-semibold text-fg-secondary\",\n value: \"font-mono text-[11px] tabular-nums text-fg-secondary\",\n control:\n \"relative flex h-6 w-full touch-none select-none items-center data-disabled:cursor-not-allowed data-disabled:opacity-40\",\n track: \"relative h-1.5 w-full grow overflow-hidden rounded-full bg-surface-3\",\n range: \"absolute h-full rounded-full bg-linear-to-r from-accent to-accent-2\",\n thumb:\n \"block h-5 w-5 cursor-grab rounded-full border border-border-strong bg-fg-on-media shadow-md transition-[box-shadow] \" +\n \"focus-visible:outline-hidden focus-visible:ring-4 focus-visible:ring-ring/50 active:cursor-grabbing\",\n },\n});\n\nexport type SliderValue = number | [number, number];\n\nexport type SliderProps<V extends SliderValue = number> = Omit<\n React.ComponentProps<typeof SliderPrimitive.Root>,\n \"value\" | \"defaultValue\" | \"onValueChange\" | \"onValueCommit\" | \"children\"\n> & {\n /** One number (a thumb) or two (a range). Controlled with `onValueChange`. */\n value?: V;\n /** The value at first, uncontrolled: one number, or two for a range. */\n defaultValue?: V;\n /** Called on every move, with one number or two. */\n onValueChange?: (value: V) => void;\n /** Called once the drag or the key press ends. */\n onValueCommit?: (value: V) => void;\n /** The accessible name — of the thumb, or of both thumbs of a range — passed translated. */\n label: string;\n /** For a range: each thumb's own name ([\"Minimum\", \"Maximum\"]), passed translated. */\n thumbLabels?: [string, string];\n /** The value as words (aria-valuetext) and as shown: \"1.5×\", \"−12 %\", \"3 min\". */\n formatValue?: (value: number) => string;\n /** Shows the label and the value above the track. Without it, `label` only names the thumb. */\n showLabel?: boolean;\n /** Fills from this value instead of the minimum (a ±range from 0: brightness, balance). One thumb only. */\n origin?: number;\n};\n\nconst toArray = (v: SliderValue | undefined) => (v === undefined ? undefined : typeof v === \"number\" ? [v] : v);\n\n/** A slider on one value or a range: `value`/`defaultValue` + `onValueChange`, `min`, `max`, `step`. */\nexport function Slider<V extends SliderValue = number>({\n value,\n defaultValue,\n onValueChange,\n onValueCommit,\n label,\n thumbLabels,\n formatValue = String,\n showLabel,\n origin,\n min = 0,\n max = 100,\n className,\n ...props\n}: SliderProps<V>) {\n const s = slider();\n const range = Array.isArray(value ?? defaultValue);\n const [inner, setInner] = React.useState(() => toArray(defaultValue) ?? [min]);\n const current = toArray(value) ?? inner;\n const out = (next: number[]) => (range ? next : next[0]) as V;\n const pct = (v: number) => ((v - min) / (max - min || 1)) * 100;\n const fill =\n origin !== undefined && !range\n ? { left: `${Math.min(pct(origin), pct(current[0]))}%`, right: `${100 - Math.max(pct(origin), pct(current[0]))}%` }\n : undefined;\n const id = React.useId();\n return (\n <div className={s.root({ className })} data-range={range ? \"\" : undefined}>\n {showLabel && (\n <div className={s.header()}>\n <span id={`${id}-label`}>{label}</span>\n <span className={s.value()} aria-hidden>\n {current.map(formatValue).join(\" – \")}\n </span>\n </div>\n )}\n <SliderPrimitive.Root\n min={min}\n max={max}\n value={current}\n onValueChange={(next) => {\n if (value === undefined) setInner(next);\n onValueChange?.(out(next));\n }}\n onValueCommit={onValueCommit && ((next) => onValueCommit(out(next)))}\n className={s.control()}\n {...props}\n >\n <SliderPrimitive.Track className={s.track()}>\n {fill ? <span data-origin=\"\" className={s.range()} style={fill} /> : <SliderPrimitive.Range className={s.range()} />}\n </SliderPrimitive.Track>\n {current.map((v, i) => (\n <SliderPrimitive.Thumb\n key={i}\n aria-label={range ? (thumbLabels?.[i] ?? label) : showLabel ? undefined : label}\n aria-labelledby={!range && showLabel ? `${id}-label` : undefined}\n aria-valuetext={formatValue(v)}\n className={s.thumb()}\n />\n ))}\n </SliderPrimitive.Root>\n </div>\n );\n}\n"],"mappings":";;;AAGA,SAAS,UAAU,uBAAuB;AAC1C,YAAY,WAAW;AACvB,SAAS,UAAU;AA2EX,SACE,KADF;AAzED,IAAM,SAAS,GAAG;AAAA,EACvB,OAAO;AAAA,IACL,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,SACE;AAAA,IACF,OAAO;AAAA,IACP,OAAO;AAAA,IACP,OACE;AAAA,EAEJ;AACF,CAAC;AA4BD,IAAM,UAAU,CAAC,MAAgC,MAAM,SAAY,SAAY,OAAO,MAAM,WAAW,CAAC,CAAC,IAAI;AAGtG,SAAS,OAAuC;AAAA,EACrD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd;AAAA,EACA;AAAA,EACA,MAAM;AAAA,EACN,MAAM;AAAA,EACN;AAAA,EACA,GAAG;AACL,GAAmB;AACjB,QAAM,IAAI,OAAO;AACjB,QAAM,QAAQ,MAAM,QAAQ,SAAS,YAAY;AACjD,QAAM,CAAC,OAAO,QAAQ,IAAU,eAAS,MAAM,QAAQ,YAAY,KAAK,CAAC,GAAG,CAAC;AAC7E,QAAM,UAAU,QAAQ,KAAK,KAAK;AAClC,QAAM,MAAM,CAAC,SAAoB,QAAQ,OAAO,KAAK,CAAC;AACtD,QAAM,MAAM,CAAC,OAAgB,IAAI,QAAQ,MAAM,OAAO,KAAM;AAC5D,QAAM,OACJ,WAAW,UAAa,CAAC,QACrB,EAAE,MAAM,GAAG,KAAK,IAAI,IAAI,MAAM,GAAG,IAAI,QAAQ,CAAC,CAAC,CAAC,CAAC,KAAK,OAAO,GAAG,MAAM,KAAK,IAAI,IAAI,MAAM,GAAG,IAAI,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,IAChH;AACN,QAAM,KAAW,YAAM;AACvB,SACE,qBAAC,SAAI,WAAW,EAAE,KAAK,EAAE,UAAU,CAAC,GAAG,cAAY,QAAQ,KAAK,QAC7D;AAAA,iBACC,qBAAC,SAAI,WAAW,EAAE,OAAO,GACvB;AAAA,0BAAC,UAAK,IAAI,GAAG,EAAE,UAAW,iBAAM;AAAA,MAChC,oBAAC,UAAK,WAAW,EAAE,MAAM,GAAG,eAAW,MACpC,kBAAQ,IAAI,WAAW,EAAE,KAAK,UAAK,GACtC;AAAA,OACF;AAAA,IAEF;AAAA,MAAC,gBAAgB;AAAA,MAAhB;AAAA,QACC;AAAA,QACA;AAAA,QACA,OAAO;AAAA,QACP,eAAe,CAAC,SAAS;AACvB,cAAI,UAAU,OAAW,UAAS,IAAI;AACtC,0BAAgB,IAAI,IAAI,CAAC;AAAA,QAC3B;AAAA,QACA,eAAe,kBAAkB,CAAC,SAAS,cAAc,IAAI,IAAI,CAAC;AAAA,QAClE,WAAW,EAAE,QAAQ;AAAA,QACpB,GAAG;AAAA,QAEJ;AAAA,8BAAC,gBAAgB,OAAhB,EAAsB,WAAW,EAAE,MAAM,GACvC,iBAAO,oBAAC,UAAK,eAAY,IAAG,WAAW,EAAE,MAAM,GAAG,OAAO,MAAM,IAAK,oBAAC,gBAAgB,OAAhB,EAAsB,WAAW,EAAE,MAAM,GAAG,GACpH;AAAA,UACC,QAAQ,IAAI,CAAC,GAAG,MACf;AAAA,YAAC,gBAAgB;AAAA,YAAhB;AAAA,cAEC,cAAY,QAAS,cAAc,CAAC,KAAK,QAAS,YAAY,SAAY;AAAA,cAC1E,mBAAiB,CAAC,SAAS,YAAY,GAAG,EAAE,WAAW;AAAA,cACvD,kBAAgB,YAAY,CAAC;AAAA,cAC7B,WAAW,EAAE,MAAM;AAAA;AAAA,YAJd;AAAA,UAKP,CACD;AAAA;AAAA;AAAA,IACH;AAAA,KACF;AAEJ;","names":[]}
|