@cueplusplus/ui 0.1.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/dist/charts/_token.js +39 -0
- package/dist/charts/container.d.ts +58 -0
- package/dist/charts/container.js +77 -0
- package/dist/charts/index.d.ts +5 -0
- package/dist/charts/index.js +5 -0
- package/dist/charts/ramps.d.ts +140 -0
- package/dist/charts/ramps.js +615 -0
- package/dist/charts/theme.d.ts +112 -0
- package/dist/charts/theme.js +165 -0
- package/dist/charts/tones.d.ts +111 -0
- package/dist/charts/tones.js +127 -0
- package/dist/chat/_glyphs.js +55 -0
- package/dist/chat/agent-color.d.ts +84 -0
- package/dist/chat/agent-color.js +101 -0
- package/dist/chat/agent-pile.d.ts +55 -0
- package/dist/chat/agent-pile.js +121 -0
- package/dist/chat/ask-box.d.ts +88 -0
- package/dist/chat/ask-box.js +341 -0
- package/dist/chat/composer.d.ts +66 -0
- package/dist/chat/composer.js +144 -0
- package/dist/chat/delegation-card.d.ts +60 -0
- package/dist/chat/delegation-card.js +129 -0
- package/dist/chat/index.d.ts +8 -0
- package/dist/chat/index.js +8 -0
- package/dist/chat/message-list.d.ts +39 -0
- package/dist/chat/message-list.js +105 -0
- package/dist/chat/message.d.ts +47 -0
- package/dist/chat/message.js +108 -0
- package/dist/chrome/_glyphs.js +101 -0
- package/dist/chrome/_tabs-scroll.d.ts +62 -0
- package/dist/chrome/_tabs-scroll.js +68 -0
- package/dist/chrome/_tree.d.ts +75 -0
- package/dist/chrome/_tree.js +49 -0
- package/dist/chrome/empty-state.d.ts +37 -0
- package/dist/chrome/empty-state.js +57 -0
- package/dist/chrome/eyebrow.d.ts +29 -0
- package/dist/chrome/eyebrow.js +36 -0
- package/dist/chrome/index.d.ts +17 -0
- package/dist/chrome/index.js +17 -0
- package/dist/chrome/navigation-menu.d.ts +80 -0
- package/dist/chrome/navigation-menu.js +121 -0
- package/dist/chrome/page-shell.d.ts +38 -0
- package/dist/chrome/page-shell.js +66 -0
- package/dist/chrome/panel.d.ts +55 -0
- package/dist/chrome/panel.js +67 -0
- package/dist/chrome/row.d.ts +82 -0
- package/dist/chrome/row.js +106 -0
- package/dist/chrome/scroll-area.d.ts +39 -0
- package/dist/chrome/scroll-area.js +70 -0
- package/dist/chrome/section-header.d.ts +42 -0
- package/dist/chrome/section-header.js +79 -0
- package/dist/chrome/segmented-control.d.ts +58 -0
- package/dist/chrome/segmented-control.js +76 -0
- package/dist/chrome/status-bar.d.ts +46 -0
- package/dist/chrome/status-bar.js +56 -0
- package/dist/chrome/tabs.d.ts +80 -0
- package/dist/chrome/tabs.js +176 -0
- package/dist/chrome/title-bar.d.ts +37 -0
- package/dist/chrome/title-bar.js +45 -0
- package/dist/chrome/toolbar.d.ts +46 -0
- package/dist/chrome/toolbar.js +62 -0
- package/dist/chrome/tree.d.ts +98 -0
- package/dist/chrome/tree.js +248 -0
- package/dist/color/_checker.js +14 -0
- package/dist/color/_convert.d.ts +52 -0
- package/dist/color/_convert.js +66 -0
- package/dist/color/_glyphs.js +28 -0
- package/dist/color/_thumb.js +15 -0
- package/dist/color/_value.js +28 -0
- package/dist/color/color-area.d.ts +27 -0
- package/dist/color/color-area.js +40 -0
- package/dist/color/color-field.d.ts +32 -0
- package/dist/color/color-field.js +43 -0
- package/dist/color/color-picker.d.ts +71 -0
- package/dist/color/color-picker.js +178 -0
- package/dist/color/color-slider.d.ts +31 -0
- package/dist/color/color-slider.js +51 -0
- package/dist/color/color-swatch.d.ts +32 -0
- package/dist/color/color-swatch.js +44 -0
- package/dist/color/index.d.ts +7 -0
- package/dist/color/index.js +7 -0
- package/dist/configurator/_export.d.ts +78 -0
- package/dist/configurator/_export.js +261 -0
- package/dist/configurator/_glyphs.js +92 -0
- package/dist/configurator/_overrides.d.ts +247 -0
- package/dist/configurator/_overrides.js +514 -0
- package/dist/configurator/configurator.d.ts +71 -0
- package/dist/configurator/configurator.js +279 -0
- package/dist/configurator/export-dialog.d.ts +30 -0
- package/dist/configurator/export-dialog.js +121 -0
- package/dist/configurator/index.d.ts +8 -0
- package/dist/configurator/index.js +8 -0
- package/dist/configurator/panel-sections.d.ts +86 -0
- package/dist/configurator/panel-sections.js +316 -0
- package/dist/configurator/token-editor.d.ts +74 -0
- package/dist/configurator/token-editor.js +149 -0
- package/dist/configurator/use-zoom.d.ts +87 -0
- package/dist/configurator/use-zoom.js +113 -0
- package/dist/date/_glyphs.js +34 -0
- package/dist/date/_segments.js +55 -0
- package/dist/date/_trigger.js +31 -0
- package/dist/date/calendar.d.ts +25 -0
- package/dist/date/calendar.js +125 -0
- package/dist/date/date-field.d.ts +34 -0
- package/dist/date/date-field.js +41 -0
- package/dist/date/date-picker.d.ts +69 -0
- package/dist/date/date-picker.js +89 -0
- package/dist/date/date-range-picker.d.ts +65 -0
- package/dist/date/date-range-picker.js +92 -0
- package/dist/date/index.d.ts +6 -0
- package/dist/date/index.js +6 -0
- package/dist/date/time-field.d.ts +29 -0
- package/dist/date/time-field.js +36 -0
- package/dist/dmx/_ramp.d.ts +77 -0
- package/dist/dmx/_ramp.js +169 -0
- package/dist/dmx/_readout.js +53 -0
- package/dist/dmx/_universe.d.ts +105 -0
- package/dist/dmx/_universe.js +112 -0
- package/dist/dmx/channel-matrix.d.ts +81 -0
- package/dist/dmx/channel-matrix.js +285 -0
- package/dist/dmx/dmx-bar.d.ts +50 -0
- package/dist/dmx/dmx-bar.js +107 -0
- package/dist/dmx/dmx-strip.d.ts +41 -0
- package/dist/dmx/dmx-strip.js +196 -0
- package/dist/dmx/index.d.ts +7 -0
- package/dist/dmx/index.js +7 -0
- package/dist/dmx/universe-grid.d.ts +61 -0
- package/dist/dmx/universe-grid.js +182 -0
- package/dist/flow/_frame.d.ts +52 -0
- package/dist/flow/_frame.js +38 -0
- package/dist/flow/controls.d.ts +41 -0
- package/dist/flow/controls.js +53 -0
- package/dist/flow/flow.css +121 -0
- package/dist/flow/handles.d.ts +40 -0
- package/dist/flow/handles.js +61 -0
- package/dist/flow/index.d.ts +7 -0
- package/dist/flow/index.js +7 -0
- package/dist/flow/node-card.d.ts +71 -0
- package/dist/flow/node-card.js +102 -0
- package/dist/flow/signal-edge.d.ts +60 -0
- package/dist/flow/signal-edge.js +108 -0
- package/dist/flow/use-flow-theme.d.ts +69 -0
- package/dist/flow/use-flow-theme.js +109 -0
- package/dist/forms/_chassis.d.ts +64 -0
- package/dist/forms/_chassis.js +59 -0
- package/dist/forms/_glyphs.js +143 -0
- package/dist/forms/_listbox.js +55 -0
- package/dist/forms/autocomplete.d.ts +61 -0
- package/dist/forms/autocomplete.js +88 -0
- package/dist/forms/button-group.d.ts +47 -0
- package/dist/forms/button-group.js +52 -0
- package/dist/forms/checkbox.d.ts +51 -0
- package/dist/forms/checkbox.js +98 -0
- package/dist/forms/combobox.d.ts +73 -0
- package/dist/forms/combobox.js +108 -0
- package/dist/forms/env-var-input.d.ts +74 -0
- package/dist/forms/env-var-input.js +116 -0
- package/dist/forms/field.d.ts +89 -0
- package/dist/forms/field.js +102 -0
- package/dist/forms/file-upload.d.ts +113 -0
- package/dist/forms/file-upload.js +232 -0
- package/dist/forms/index.d.ts +26 -0
- package/dist/forms/index.js +26 -0
- package/dist/forms/input-group.d.ts +31 -0
- package/dist/forms/input-group.js +54 -0
- package/dist/forms/input.d.ts +33 -0
- package/dist/forms/input.js +36 -0
- package/dist/forms/multi-select.d.ts +62 -0
- package/dist/forms/multi-select.js +107 -0
- package/dist/forms/number-field.d.ts +38 -0
- package/dist/forms/number-field.js +64 -0
- package/dist/forms/otp-field.d.ts +54 -0
- package/dist/forms/otp-field.js +59 -0
- package/dist/forms/password-input.d.ts +46 -0
- package/dist/forms/password-input.js +62 -0
- package/dist/forms/radio-group.d.ts +50 -0
- package/dist/forms/radio-group.js +64 -0
- package/dist/forms/rating.d.ts +69 -0
- package/dist/forms/rating.js +103 -0
- package/dist/forms/scrub-input.d.ts +81 -0
- package/dist/forms/scrub-input.js +249 -0
- package/dist/forms/search-input.d.ts +54 -0
- package/dist/forms/search-input.js +112 -0
- package/dist/forms/select.d.ts +75 -0
- package/dist/forms/select.js +91 -0
- package/dist/forms/slider.d.ts +44 -0
- package/dist/forms/slider.js +65 -0
- package/dist/forms/switch.d.ts +36 -0
- package/dist/forms/switch.js +71 -0
- package/dist/forms/tags-input.d.ts +69 -0
- package/dist/forms/tags-input.js +148 -0
- package/dist/forms/textarea.d.ts +27 -0
- package/dist/forms/textarea.js +37 -0
- package/dist/forms/toggle-group.d.ts +41 -0
- package/dist/forms/toggle-group.js +56 -0
- package/dist/forms/toggle.d.ts +41 -0
- package/dist/forms/toggle.js +100 -0
- package/dist/index.d.ts +112 -0
- package/dist/index.js +104 -0
- package/dist/instruments/_ansi.js +116 -0
- package/dist/instruments/_glyphs.js +90 -0
- package/dist/instruments/_motion.js +56 -0
- package/dist/instruments/_rem.js +21 -0
- package/dist/instruments/app-window-frame.d.ts +48 -0
- package/dist/instruments/app-window-frame.js +76 -0
- package/dist/instruments/copy-button.d.ts +41 -0
- package/dist/instruments/copy-button.js +112 -0
- package/dist/instruments/data-table.d.ts +168 -0
- package/dist/instruments/data-table.js +212 -0
- package/dist/instruments/group-bar.d.ts +63 -0
- package/dist/instruments/group-bar.js +122 -0
- package/dist/instruments/index.d.ts +13 -0
- package/dist/instruments/index.js +13 -0
- package/dist/instruments/log-viewer.d.ts +74 -0
- package/dist/instruments/log-viewer.js +215 -0
- package/dist/instruments/meter.d.ts +77 -0
- package/dist/instruments/meter.js +96 -0
- package/dist/instruments/progress.d.ts +45 -0
- package/dist/instruments/progress.js +61 -0
- package/dist/instruments/seam-grid.d.ts +66 -0
- package/dist/instruments/seam-grid.js +78 -0
- package/dist/instruments/sparkline.d.ts +39 -0
- package/dist/instruments/sparkline.js +85 -0
- package/dist/instruments/stat.d.ts +35 -0
- package/dist/instruments/stat.js +59 -0
- package/dist/instruments/table.d.ts +51 -0
- package/dist/instruments/table.js +92 -0
- package/dist/instruments/terminal-frame.d.ts +89 -0
- package/dist/instruments/terminal-frame.js +256 -0
- package/dist/instruments/two-step-button.d.ts +47 -0
- package/dist/instruments/two-step-button.js +147 -0
- package/dist/instruments/usage-chart.d.ts +55 -0
- package/dist/instruments/usage-chart.js +235 -0
- package/dist/layout/_collapse.js +46 -0
- package/dist/layout/_glyphs.js +45 -0
- package/dist/layout/accordion.d.ts +52 -0
- package/dist/layout/accordion.js +81 -0
- package/dist/layout/breadcrumb.d.ts +35 -0
- package/dist/layout/breadcrumb.js +67 -0
- package/dist/layout/card.d.ts +51 -0
- package/dist/layout/card.js +76 -0
- package/dist/layout/carousel.d.ts +96 -0
- package/dist/layout/carousel.js +211 -0
- package/dist/layout/collapsible.d.ts +74 -0
- package/dist/layout/collapsible.js +126 -0
- package/dist/layout/container.d.ts +27 -0
- package/dist/layout/container.js +40 -0
- package/dist/layout/description-list.d.ts +24 -0
- package/dist/layout/description-list.js +37 -0
- package/dist/layout/grid.d.ts +40 -0
- package/dist/layout/grid.js +48 -0
- package/dist/layout/index.d.ts +15 -0
- package/dist/layout/index.js +15 -0
- package/dist/layout/item.d.ts +65 -0
- package/dist/layout/item.js +100 -0
- package/dist/layout/link.d.ts +48 -0
- package/dist/layout/link.js +63 -0
- package/dist/layout/pagination.d.ts +54 -0
- package/dist/layout/pagination.js +121 -0
- package/dist/layout/resizable.d.ts +38 -0
- package/dist/layout/resizable.js +68 -0
- package/dist/layout/sidebar.d.ts +67 -0
- package/dist/layout/sidebar.js +129 -0
- package/dist/layout/stack.d.ts +46 -0
- package/dist/layout/stack.js +62 -0
- package/dist/layout/stepper.d.ts +49 -0
- package/dist/layout/stepper.js +81 -0
- package/dist/layout/timeline.d.ts +43 -0
- package/dist/layout/timeline.js +63 -0
- package/dist/lib/aria-class.js +24 -0
- package/dist/lib/cn.js +18 -0
- package/dist/lib/compose.js +38 -0
- package/dist/lib/cva.d.ts +18 -0
- package/dist/lib/cva.js +65 -0
- package/dist/midi/_notes.d.ts +101 -0
- package/dist/midi/_notes.js +183 -0
- package/dist/midi/_ruler.d.ts +100 -0
- package/dist/midi/_ruler.js +105 -0
- package/dist/midi/_time.d.ts +107 -0
- package/dist/midi/_time.js +130 -0
- package/dist/midi/index.d.ts +9 -0
- package/dist/midi/index.js +9 -0
- package/dist/midi/musical-time-input.d.ts +48 -0
- package/dist/midi/musical-time-input.js +154 -0
- package/dist/midi/piano-keyboard.d.ts +50 -0
- package/dist/midi/piano-keyboard.js +193 -0
- package/dist/midi/spectrum-visualizer.d.ts +65 -0
- package/dist/midi/spectrum-visualizer.js +213 -0
- package/dist/midi/threshold-rail.d.ts +72 -0
- package/dist/midi/threshold-rail.js +214 -0
- package/dist/midi/timeline-ruler.d.ts +83 -0
- package/dist/midi/timeline-ruler.js +152 -0
- package/dist/overlays/_debounce.js +34 -0
- package/dist/overlays/_glyphs.js +59 -0
- package/dist/overlays/_surface.d.ts +70 -0
- package/dist/overlays/_surface.js +70 -0
- package/dist/overlays/_wheel.js +42 -0
- package/dist/overlays/alert-dialog.d.ts +67 -0
- package/dist/overlays/alert-dialog.js +117 -0
- package/dist/overlays/command-palette.d.ts +93 -0
- package/dist/overlays/command-palette.js +193 -0
- package/dist/overlays/context-menu.d.ts +64 -0
- package/dist/overlays/context-menu.js +78 -0
- package/dist/overlays/dialog.d.ts +72 -0
- package/dist/overlays/dialog.js +124 -0
- package/dist/overlays/drawer.d.ts +75 -0
- package/dist/overlays/drawer.js +151 -0
- package/dist/overlays/dropdown-menu.d.ts +79 -0
- package/dist/overlays/dropdown-menu.js +156 -0
- package/dist/overlays/hover-card.d.ts +58 -0
- package/dist/overlays/hover-card.js +91 -0
- package/dist/overlays/index.d.ts +15 -0
- package/dist/overlays/index.js +15 -0
- package/dist/overlays/info-tip.d.ts +41 -0
- package/dist/overlays/info-tip.js +45 -0
- package/dist/overlays/menubar.d.ts +72 -0
- package/dist/overlays/menubar.js +100 -0
- package/dist/overlays/popover.d.ts +66 -0
- package/dist/overlays/popover.js +114 -0
- package/dist/overlays/sheet.d.ts +78 -0
- package/dist/overlays/sheet.js +131 -0
- package/dist/overlays/toast.d.ts +106 -0
- package/dist/overlays/toast.js +146 -0
- package/dist/overlays/tooltip.d.ts +57 -0
- package/dist/overlays/tooltip.js +95 -0
- package/dist/primitives/avatar.d.ts +51 -0
- package/dist/primitives/avatar.js +93 -0
- package/dist/primitives/button.d.ts +46 -0
- package/dist/primitives/button.js +94 -0
- package/dist/primitives/chip.d.ts +47 -0
- package/dist/primitives/chip.js +152 -0
- package/dist/primitives/icon-button.d.ts +41 -0
- package/dist/primitives/icon-button.js +59 -0
- package/dist/primitives/index.d.ts +10 -0
- package/dist/primitives/index.js +10 -0
- package/dist/primitives/kbd.d.ts +17 -0
- package/dist/primitives/kbd.js +24 -0
- package/dist/primitives/separator.d.ts +25 -0
- package/dist/primitives/separator.js +30 -0
- package/dist/primitives/skeleton.d.ts +20 -0
- package/dist/primitives/skeleton.js +27 -0
- package/dist/primitives/spinner.d.ts +26 -0
- package/dist/primitives/spinner.js +31 -0
- package/dist/primitives/status-dot.d.ts +44 -0
- package/dist/primitives/status-dot.js +50 -0
- package/dist/styles.css +19 -0
- package/dist/system/density.d.ts +33 -0
- package/dist/system/density.js +49 -0
- package/dist/system/index.d.ts +8 -0
- package/dist/system/index.js +7 -0
- package/dist/system/portal.d.ts +51 -0
- package/dist/system/portal.js +122 -0
- package/dist/system/prepaint.d.ts +38 -0
- package/dist/system/prepaint.js +37 -0
- package/dist/system/theme-provider.d.ts +72 -0
- package/dist/system/theme-provider.js +252 -0
- package/dist/system/use-density.d.ts +33 -0
- package/dist/system/use-density.js +84 -0
- package/dist/system/use-isomorphic-layout-effect.js +15 -0
- package/dist/system/use-theme.d.ts +19 -0
- package/dist/system/use-theme.js +25 -0
- package/dist/theming/_presets.d.ts +11 -0
- package/dist/theming/_presets.js +487 -0
- package/dist/theming/contrast.d.ts +110 -0
- package/dist/theming/contrast.js +167 -0
- package/dist/theming/create-theme.d.ts +165 -0
- package/dist/theming/create-theme.js +442 -0
- package/dist/theming/index.d.ts +5 -0
- package/dist/theming/index.js +5 -0
- package/dist/theming/serialize.d.ts +145 -0
- package/dist/theming/serialize.js +165 -0
- package/package.json +216 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
//#region src/forms/rating.d.ts
|
|
3
|
+
/** Mark size per rung — a rating is read at a glance, so it tracks the icon ladder. */
|
|
4
|
+
declare const MARK_SIZES: {
|
|
5
|
+
readonly sm: "size-icon-sm";
|
|
6
|
+
readonly md: "size-icon-md";
|
|
7
|
+
readonly lg: "size-[calc(var(--cue-icon-md)*1.35)]";
|
|
8
|
+
};
|
|
9
|
+
/** Rung of the icon ladder a {@link Rating}'s marks sit on. */
|
|
10
|
+
type RatingSize = keyof typeof MARK_SIZES;
|
|
11
|
+
interface RatingProps extends Omit<React.ComponentPropsWithoutRef<"div">, "onChange" | "defaultValue"> {
|
|
12
|
+
/** The rating, `0`–`count`. Use with `onValueChange` for a controlled widget. */
|
|
13
|
+
value?: number;
|
|
14
|
+
/** Initial rating for an uncontrolled widget. Defaults to `0`. */
|
|
15
|
+
defaultValue?: number;
|
|
16
|
+
/** Called with the new rating; `0` means "cleared". */
|
|
17
|
+
onValueChange?: (value: number) => void;
|
|
18
|
+
/** How many marks. Defaults to `5`. */
|
|
19
|
+
count?: number;
|
|
20
|
+
/** Rung of the icon ladder. Defaults to `"md"`. */
|
|
21
|
+
size?: RatingSize;
|
|
22
|
+
/**
|
|
23
|
+
* Picking the current value clears it back to `0`. Defaults to `true` — a
|
|
24
|
+
* rating you cannot take back is a trap, and there is no other affordance for
|
|
25
|
+
* "actually, no opinion".
|
|
26
|
+
*/
|
|
27
|
+
allowClear?: boolean;
|
|
28
|
+
/** Render a static readout with no controls. */
|
|
29
|
+
readOnly?: boolean;
|
|
30
|
+
/** Grey the marks out and refuse interaction. */
|
|
31
|
+
disabled?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Accessible name for the group. Also the prefix of the read-only readout —
|
|
34
|
+
* `"Signal quality: 4 of 5"`. Synonym of `aria-label`, which wins if both are
|
|
35
|
+
* given.
|
|
36
|
+
*/
|
|
37
|
+
label?: string;
|
|
38
|
+
/** Accessible name for the group, for callers who prefer the ARIA spelling. */
|
|
39
|
+
"aria-label"?: string;
|
|
40
|
+
/** The mark component. Defaults to a filled star. */
|
|
41
|
+
icon?: React.ElementType;
|
|
42
|
+
/**
|
|
43
|
+
* Accessible name for mark `index` (1-based) of `count`. Defaults to
|
|
44
|
+
* ``(index, count) => `${index} of ${count}` ``.
|
|
45
|
+
*/
|
|
46
|
+
markLabel?: (index: number, count: number) => string;
|
|
47
|
+
/** Extra classes for the group. */
|
|
48
|
+
className?: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* A 0–N rating, drawn as marks.
|
|
52
|
+
*
|
|
53
|
+
* Built as a **radio group**, not a row of buttons: exactly one value is in
|
|
54
|
+
* effect, arrow keys move between the options, and one Tab stop covers the whole
|
|
55
|
+
* widget. A row of toggle buttons would put five stops in the tab order and
|
|
56
|
+
* announce five unrelated controls.
|
|
57
|
+
*
|
|
58
|
+
* Read-only renders as a single `img` with the value in its name — an
|
|
59
|
+
* un-interactive control that still takes focus is a trap, and five disabled
|
|
60
|
+
* radios announce five things where one sentence will do.
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* <Rating label="Signal quality" value={score} onValueChange={setScore} />
|
|
64
|
+
* @example
|
|
65
|
+
* <Rating aria-label="Signal quality" value={4} readOnly size="sm" />
|
|
66
|
+
*/
|
|
67
|
+
declare const Rating: React.ForwardRefExoticComponent<RatingProps & React.RefAttributes<HTMLDivElement>>;
|
|
68
|
+
//#endregion
|
|
69
|
+
export { Rating, RatingProps, RatingSize };
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "../lib/cn.js";
|
|
3
|
+
import { StarGlyph } from "./_glyphs.js";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { jsx } from "react/jsx-runtime";
|
|
6
|
+
//#region src/forms/rating.tsx
|
|
7
|
+
/** Mark size per rung — a rating is read at a glance, so it tracks the icon ladder. */
|
|
8
|
+
const MARK_SIZES = {
|
|
9
|
+
sm: "size-icon-sm",
|
|
10
|
+
md: "size-icon-md",
|
|
11
|
+
lg: "size-[calc(var(--cue-icon-md)*1.35)]"
|
|
12
|
+
};
|
|
13
|
+
const MARK_CLASSES = "flex cursor-pointer items-center justify-center border-0 bg-transparent p-0 text-fg-subtle transition-colors duration-150 outline-none hover:text-accent focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-offset-2 focus-visible:outline-accent disabled:pointer-events-none disabled:opacity-40 aria-checked:text-accent";
|
|
14
|
+
/**
|
|
15
|
+
* A 0–N rating, drawn as marks.
|
|
16
|
+
*
|
|
17
|
+
* Built as a **radio group**, not a row of buttons: exactly one value is in
|
|
18
|
+
* effect, arrow keys move between the options, and one Tab stop covers the whole
|
|
19
|
+
* widget. A row of toggle buttons would put five stops in the tab order and
|
|
20
|
+
* announce five unrelated controls.
|
|
21
|
+
*
|
|
22
|
+
* Read-only renders as a single `img` with the value in its name — an
|
|
23
|
+
* un-interactive control that still takes focus is a trap, and five disabled
|
|
24
|
+
* radios announce five things where one sentence will do.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* <Rating label="Signal quality" value={score} onValueChange={setScore} />
|
|
28
|
+
* @example
|
|
29
|
+
* <Rating aria-label="Signal quality" value={4} readOnly size="sm" />
|
|
30
|
+
*/
|
|
31
|
+
const Rating = React.forwardRef(function Rating({ className, value: valueProp, defaultValue = 0, onValueChange, count = 5, size = "md", allowClear = true, readOnly = false, disabled = false, label, "aria-label": ariaLabel, icon: Icon = StarGlyph, markLabel = (index, total) => `${index} of ${total}`, ...elementProps }, ref) {
|
|
32
|
+
const isControlled = valueProp !== void 0;
|
|
33
|
+
const [uncontrolled, setUncontrolled] = React.useState(defaultValue);
|
|
34
|
+
const value = isControlled ? valueProp : uncontrolled;
|
|
35
|
+
const name = ariaLabel ?? label;
|
|
36
|
+
const commit = React.useCallback((next) => {
|
|
37
|
+
const clamped = Math.min(count, Math.max(0, next));
|
|
38
|
+
if (clamped === value) return;
|
|
39
|
+
if (!isControlled) setUncontrolled(clamped);
|
|
40
|
+
onValueChange?.(clamped);
|
|
41
|
+
}, [
|
|
42
|
+
count,
|
|
43
|
+
isControlled,
|
|
44
|
+
onValueChange,
|
|
45
|
+
value
|
|
46
|
+
]);
|
|
47
|
+
const marks = Array.from({ length: count }, (_, index) => index + 1);
|
|
48
|
+
if (readOnly) return /* @__PURE__ */ jsx("div", {
|
|
49
|
+
ref,
|
|
50
|
+
role: "img",
|
|
51
|
+
"data-slot": "rating",
|
|
52
|
+
"aria-label": name === void 0 ? `${value} of ${count}` : `${name}: ${value} of ${count}`,
|
|
53
|
+
className: cn("inline-flex items-center gap-(--cue-space-1)", className),
|
|
54
|
+
...elementProps,
|
|
55
|
+
children: marks.map((mark) => /* @__PURE__ */ jsx(Icon, {
|
|
56
|
+
"aria-hidden": "true",
|
|
57
|
+
"data-slot": "rating-mark",
|
|
58
|
+
"data-filled": mark <= value ? "" : void 0,
|
|
59
|
+
className: cn(MARK_SIZES[size], mark <= value ? "text-accent" : "text-fg-subtle")
|
|
60
|
+
}, mark))
|
|
61
|
+
});
|
|
62
|
+
const handleKeyDown = (event) => {
|
|
63
|
+
if (disabled) return;
|
|
64
|
+
const step = event.key === "ArrowRight" || event.key === "ArrowUp" ? 1 : event.key === "ArrowLeft" || event.key === "ArrowDown" ? -1 : 0;
|
|
65
|
+
let next = null;
|
|
66
|
+
if (step !== 0) next = Math.min(count, Math.max(1, value + step));
|
|
67
|
+
else if (event.key === "Home") next = 1;
|
|
68
|
+
else if (event.key === "End") next = count;
|
|
69
|
+
if (next === null) return;
|
|
70
|
+
event.preventDefault();
|
|
71
|
+
commit(next);
|
|
72
|
+
event.currentTarget.querySelector(`[data-slot="rating-mark"][data-index="${next}"]`)?.focus();
|
|
73
|
+
};
|
|
74
|
+
const focusable = value === 0 ? 1 : value;
|
|
75
|
+
return /* @__PURE__ */ jsx("div", {
|
|
76
|
+
ref,
|
|
77
|
+
role: "radiogroup",
|
|
78
|
+
"data-slot": "rating",
|
|
79
|
+
"aria-label": name,
|
|
80
|
+
"aria-disabled": disabled ? true : void 0,
|
|
81
|
+
onKeyDown: handleKeyDown,
|
|
82
|
+
className: cn("inline-flex items-center gap-(--cue-space-1)", className),
|
|
83
|
+
...elementProps,
|
|
84
|
+
children: marks.map((mark) => /* @__PURE__ */ jsx("button", {
|
|
85
|
+
type: "button",
|
|
86
|
+
role: "radio",
|
|
87
|
+
"data-slot": "rating-mark",
|
|
88
|
+
"data-index": mark,
|
|
89
|
+
"aria-checked": mark === value,
|
|
90
|
+
"aria-label": markLabel(mark, count),
|
|
91
|
+
tabIndex: mark === focusable ? 0 : -1,
|
|
92
|
+
disabled,
|
|
93
|
+
onClick: () => commit(mark === value && allowClear ? 0 : mark),
|
|
94
|
+
className: MARK_CLASSES,
|
|
95
|
+
children: /* @__PURE__ */ jsx(Icon, {
|
|
96
|
+
"aria-hidden": "true",
|
|
97
|
+
className: cn(MARK_SIZES[size], mark <= value ? "text-accent" : void 0)
|
|
98
|
+
})
|
|
99
|
+
}, mark))
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
//#endregion
|
|
103
|
+
export { Rating };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { FormControlSize } from "./_chassis.js";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
//#region src/forms/scrub-input.d.ts
|
|
4
|
+
interface ScrubInputProps extends Omit<React.ComponentPropsWithoutRef<"div">, "onChange" | "defaultValue" | "onDragStart" | "onDragEnd"> {
|
|
5
|
+
/** The value. Always controlled — a scrub is a stream of edits, not a state to lose. */
|
|
6
|
+
value: number;
|
|
7
|
+
/**
|
|
8
|
+
* Called with every new value: each drag frame, each arrow-key step, and the
|
|
9
|
+
* committed result of a typed edit. Not a DOM change event.
|
|
10
|
+
*/
|
|
11
|
+
onChange: (value: number) => void;
|
|
12
|
+
/** Called only for values produced by a drag, for hosts that throttle live writes. */
|
|
13
|
+
onLiveDrag?: (value: number) => void;
|
|
14
|
+
/** Called once when a drag actually begins — after the click threshold, not on press. */
|
|
15
|
+
onDragStart?: () => void;
|
|
16
|
+
/** Called once when a drag ends. */
|
|
17
|
+
onDragEnd?: () => void;
|
|
18
|
+
/**
|
|
19
|
+
* Called once per finished gesture: pointer-up after a drag, each key step, a
|
|
20
|
+
* committed typed value. Hang undo entries, network writes and persistence
|
|
21
|
+
* here — `onChange` fires dozens of times per drag.
|
|
22
|
+
*/
|
|
23
|
+
onCommit?: (value: number) => void;
|
|
24
|
+
/** Lower bound. Defaults to `-Infinity`. */
|
|
25
|
+
min?: number;
|
|
26
|
+
/** Upper bound. Defaults to `Infinity`. */
|
|
27
|
+
max?: number;
|
|
28
|
+
/** Grid the value snaps to. Defaults to `1`. */
|
|
29
|
+
step?: number;
|
|
30
|
+
/** Decimal places shown. Defaults to whatever `step` implies. */
|
|
31
|
+
precision?: number;
|
|
32
|
+
/** Suffix drawn after the number — `%`, `s`, `dB`. */
|
|
33
|
+
unit?: string;
|
|
34
|
+
/**
|
|
35
|
+
* The units this value can be expressed in. With more than one the suffix
|
|
36
|
+
* becomes a button that cycles through them and reports via `onUnitChange`.
|
|
37
|
+
*/
|
|
38
|
+
units?: readonly string[];
|
|
39
|
+
/** Called with the next unit when the suffix is clicked. */
|
|
40
|
+
onUnitChange?: (unit: string) => void;
|
|
41
|
+
/** Rung of the control ladder. Defaults to `"md"`. */
|
|
42
|
+
size?: FormControlSize;
|
|
43
|
+
/** Refuse dragging, typing and keyboard steps. */
|
|
44
|
+
disabled?: boolean;
|
|
45
|
+
/** Accessible name for the control. */
|
|
46
|
+
"aria-label"?: string;
|
|
47
|
+
/** Id of the element naming the control. */
|
|
48
|
+
"aria-labelledby"?: string;
|
|
49
|
+
/** Extra classes for the control. */
|
|
50
|
+
className?: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* A number you scrub: drag up to raise it, down to lower it, click to type it.
|
|
54
|
+
*
|
|
55
|
+
* The console's answer to `<input type="number">`. Native spinners are two 4px
|
|
56
|
+
* arrows that only appear on hover — unusable at this density and invisible on
|
|
57
|
+
* touch — while a slider costs 200px of width to say one number. Dragging on the
|
|
58
|
+
* number itself costs nothing and is the interaction every audio and lighting
|
|
59
|
+
* tool has converged on: pointer capture so the cursor may leave the box, Shift
|
|
60
|
+
* for fine adjustment, `cursor-ns-resize` to advertise the axis.
|
|
61
|
+
*
|
|
62
|
+
* Click-to-type is not a fallback, it is the other half of the control: a
|
|
63
|
+
* gesture is for *approximately*, typing is for *exactly*, and a value you can
|
|
64
|
+
* only reach by dragging is a value you cannot enter from a cue sheet.
|
|
65
|
+
*
|
|
66
|
+
* `role="spinbutton"` is what makes any of this reachable without a pointer —
|
|
67
|
+
* arrow keys step, Page keys jump ten, Home/End go to the bounds — and it is why
|
|
68
|
+
* the resting element is not an `<input>`: a spinbutton announces its value,
|
|
69
|
+
* where a text box would announce its editable string on every drag frame.
|
|
70
|
+
*
|
|
71
|
+
* The `onChange`/`onCommit` split is the same undo-coalescing contract as
|
|
72
|
+
* {@link Slider}: one drag is one undo entry, not two hundred.
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* <ScrubInput aria-label="Intensity" value={level} onChange={setLevel} onCommit={pushUndo} />
|
|
76
|
+
* @example
|
|
77
|
+
* <ScrubInput aria-label="Fade" value={fade} onChange={setFade} step={0.1} unit="s" min={0} />
|
|
78
|
+
*/
|
|
79
|
+
declare const ScrubInput: React.ForwardRefExoticComponent<ScrubInputProps & React.RefAttributes<HTMLDivElement>>;
|
|
80
|
+
//#endregion
|
|
81
|
+
export { ScrubInput, ScrubInputProps };
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "../lib/cn.js";
|
|
3
|
+
import { bareControlClasses, controlGroupFocusClasses, controlVariants } from "./_chassis.js";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
//#region src/forms/scrub-input.tsx
|
|
7
|
+
/**
|
|
8
|
+
* Pixels of vertical travel per step. Two pixels per step is the Ableton/Live
|
|
9
|
+
* feel the audit called out: fast enough to cross a 0–100 range in a flick,
|
|
10
|
+
* slow enough that a single value is reachable without the fine modifier.
|
|
11
|
+
*/
|
|
12
|
+
const PIXELS_PER_STEP = 2;
|
|
13
|
+
/**
|
|
14
|
+
* How much Shift slows a drag. Quarter speed — same value grid, four times the
|
|
15
|
+
* travel per step, which is precision of *control* rather than sub-step values
|
|
16
|
+
* the caller never asked for.
|
|
17
|
+
*/
|
|
18
|
+
const FINE_FACTOR = .25;
|
|
19
|
+
/** Travel, in px, before a press counts as a drag rather than a click. */
|
|
20
|
+
const DRAG_THRESHOLD_PX = 3;
|
|
21
|
+
/** Arrow keys move one step; Page keys move ten. */
|
|
22
|
+
const PAGE_FACTOR = 10;
|
|
23
|
+
/** Snap to the step grid, clamp to the bounds, and drop float noise. */
|
|
24
|
+
function quantise(raw, { min, max, step, precision }) {
|
|
25
|
+
const snapped = Math.round(raw / step) * step;
|
|
26
|
+
const clamped = Math.min(max, Math.max(min, snapped));
|
|
27
|
+
const factor = 10 ** precision;
|
|
28
|
+
return Math.round(clamped * factor) / factor;
|
|
29
|
+
}
|
|
30
|
+
/** Decimal places implied by a step, when the caller did not say. */
|
|
31
|
+
function precisionOf(step) {
|
|
32
|
+
const text = String(step);
|
|
33
|
+
const dot = text.indexOf(".");
|
|
34
|
+
return dot === -1 ? 0 : Math.min(6, text.length - dot - 1);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* A number you scrub: drag up to raise it, down to lower it, click to type it.
|
|
38
|
+
*
|
|
39
|
+
* The console's answer to `<input type="number">`. Native spinners are two 4px
|
|
40
|
+
* arrows that only appear on hover — unusable at this density and invisible on
|
|
41
|
+
* touch — while a slider costs 200px of width to say one number. Dragging on the
|
|
42
|
+
* number itself costs nothing and is the interaction every audio and lighting
|
|
43
|
+
* tool has converged on: pointer capture so the cursor may leave the box, Shift
|
|
44
|
+
* for fine adjustment, `cursor-ns-resize` to advertise the axis.
|
|
45
|
+
*
|
|
46
|
+
* Click-to-type is not a fallback, it is the other half of the control: a
|
|
47
|
+
* gesture is for *approximately*, typing is for *exactly*, and a value you can
|
|
48
|
+
* only reach by dragging is a value you cannot enter from a cue sheet.
|
|
49
|
+
*
|
|
50
|
+
* `role="spinbutton"` is what makes any of this reachable without a pointer —
|
|
51
|
+
* arrow keys step, Page keys jump ten, Home/End go to the bounds — and it is why
|
|
52
|
+
* the resting element is not an `<input>`: a spinbutton announces its value,
|
|
53
|
+
* where a text box would announce its editable string on every drag frame.
|
|
54
|
+
*
|
|
55
|
+
* The `onChange`/`onCommit` split is the same undo-coalescing contract as
|
|
56
|
+
* {@link Slider}: one drag is one undo entry, not two hundred.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* <ScrubInput aria-label="Intensity" value={level} onChange={setLevel} onCommit={pushUndo} />
|
|
60
|
+
* @example
|
|
61
|
+
* <ScrubInput aria-label="Fade" value={fade} onChange={setFade} step={0.1} unit="s" min={0} />
|
|
62
|
+
*/
|
|
63
|
+
const ScrubInput = React.forwardRef(function ScrubInput({ className, value, onChange, onLiveDrag, onDragStart, onDragEnd, onCommit, min = Number.NEGATIVE_INFINITY, max = Number.POSITIVE_INFINITY, step = 1, precision, unit, units, onUnitChange, size = "md", disabled = false, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, ...elementProps }, ref) {
|
|
64
|
+
const decimals = precision ?? precisionOf(step);
|
|
65
|
+
const bounds = React.useMemo(() => ({
|
|
66
|
+
min,
|
|
67
|
+
max,
|
|
68
|
+
step,
|
|
69
|
+
precision: decimals
|
|
70
|
+
}), [
|
|
71
|
+
decimals,
|
|
72
|
+
max,
|
|
73
|
+
min,
|
|
74
|
+
step
|
|
75
|
+
]);
|
|
76
|
+
const [editing, setEditing] = React.useState(false);
|
|
77
|
+
const [draft, setDraft] = React.useState("");
|
|
78
|
+
const editorRef = React.useRef(null);
|
|
79
|
+
/** Escape has already discarded this edit; a trailing blur must not undo that. */
|
|
80
|
+
const abandoned = React.useRef(false);
|
|
81
|
+
/** Live gesture state. A ref, not state: a drag must not re-render to track itself. */
|
|
82
|
+
const gesture = React.useRef(null);
|
|
83
|
+
const display = value.toFixed(decimals);
|
|
84
|
+
const valueText = unit === void 0 ? display : `${display} ${unit}`;
|
|
85
|
+
const emit = React.useCallback((next, fromDrag) => {
|
|
86
|
+
if (next === value) return false;
|
|
87
|
+
onChange(next);
|
|
88
|
+
if (fromDrag) onLiveDrag?.(next);
|
|
89
|
+
return true;
|
|
90
|
+
}, [
|
|
91
|
+
onChange,
|
|
92
|
+
onLiveDrag,
|
|
93
|
+
value
|
|
94
|
+
]);
|
|
95
|
+
const beginEditing = React.useCallback(() => {
|
|
96
|
+
abandoned.current = false;
|
|
97
|
+
setDraft(value.toFixed(decimals));
|
|
98
|
+
setEditing(true);
|
|
99
|
+
}, [decimals, value]);
|
|
100
|
+
const handlePointerDown = (event) => {
|
|
101
|
+
if (disabled || editing || event.button !== 0) return;
|
|
102
|
+
gesture.current = {
|
|
103
|
+
pointerId: event.pointerId,
|
|
104
|
+
startY: event.clientY,
|
|
105
|
+
startValue: value,
|
|
106
|
+
dragging: false,
|
|
107
|
+
last: value
|
|
108
|
+
};
|
|
109
|
+
event.currentTarget.setPointerCapture?.(event.pointerId);
|
|
110
|
+
};
|
|
111
|
+
const handlePointerMove = (event) => {
|
|
112
|
+
const active = gesture.current;
|
|
113
|
+
if (active === null || active.pointerId !== event.pointerId) return;
|
|
114
|
+
const travel = active.startY - event.clientY;
|
|
115
|
+
if (!active.dragging) {
|
|
116
|
+
if (Math.abs(travel) < DRAG_THRESHOLD_PX) return;
|
|
117
|
+
active.dragging = true;
|
|
118
|
+
onDragStart?.();
|
|
119
|
+
}
|
|
120
|
+
const scale = event.shiftKey ? FINE_FACTOR : 1;
|
|
121
|
+
const next = quantise(active.startValue + travel / PIXELS_PER_STEP * step * scale, bounds);
|
|
122
|
+
if (next === active.last) return;
|
|
123
|
+
active.last = next;
|
|
124
|
+
emit(next, true);
|
|
125
|
+
};
|
|
126
|
+
const endGesture = (event) => {
|
|
127
|
+
const active = gesture.current;
|
|
128
|
+
if (active === null || active.pointerId !== event.pointerId) return;
|
|
129
|
+
gesture.current = null;
|
|
130
|
+
event.currentTarget.releasePointerCapture?.(event.pointerId);
|
|
131
|
+
if (!active.dragging) {
|
|
132
|
+
if (!disabled) beginEditing();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
onDragEnd?.();
|
|
136
|
+
onCommit?.(active.last);
|
|
137
|
+
};
|
|
138
|
+
React.useEffect(() => {
|
|
139
|
+
if (editing) editorRef.current?.select();
|
|
140
|
+
}, [editing]);
|
|
141
|
+
const commitDraft = React.useCallback(() => {
|
|
142
|
+
setEditing(false);
|
|
143
|
+
if (abandoned.current) return;
|
|
144
|
+
const parsed = Number.parseFloat(draft);
|
|
145
|
+
if (Number.isNaN(parsed)) return;
|
|
146
|
+
const next = quantise(parsed, bounds);
|
|
147
|
+
if (emit(next, false)) onCommit?.(next);
|
|
148
|
+
}, [
|
|
149
|
+
bounds,
|
|
150
|
+
draft,
|
|
151
|
+
emit,
|
|
152
|
+
onCommit
|
|
153
|
+
]);
|
|
154
|
+
const handleKeyDown = (event) => {
|
|
155
|
+
if (disabled) return;
|
|
156
|
+
let next = null;
|
|
157
|
+
if (event.key === "ArrowUp") next = value + step;
|
|
158
|
+
else if (event.key === "ArrowDown") next = value - step;
|
|
159
|
+
else if (event.key === "PageUp") next = value + step * PAGE_FACTOR;
|
|
160
|
+
else if (event.key === "PageDown") next = value - step * PAGE_FACTOR;
|
|
161
|
+
else if (event.key === "Home" && Number.isFinite(min)) next = min;
|
|
162
|
+
else if (event.key === "End" && Number.isFinite(max)) next = max;
|
|
163
|
+
else if (event.key === "Enter") {
|
|
164
|
+
event.preventDefault();
|
|
165
|
+
beginEditing();
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (next === null) return;
|
|
169
|
+
event.preventDefault();
|
|
170
|
+
const quantised = quantise(next, bounds);
|
|
171
|
+
if (emit(quantised, false)) onCommit?.(quantised);
|
|
172
|
+
};
|
|
173
|
+
const cycleUnit = () => {
|
|
174
|
+
if (units === void 0 || units.length === 0) return;
|
|
175
|
+
const index = unit === void 0 ? -1 : units.indexOf(unit);
|
|
176
|
+
onUnitChange?.(units[(index + 1) % units.length]);
|
|
177
|
+
};
|
|
178
|
+
if (editing) return /* @__PURE__ */ jsxs("div", {
|
|
179
|
+
...elementProps,
|
|
180
|
+
ref,
|
|
181
|
+
"data-slot": "scrub-input-editor",
|
|
182
|
+
className: cn(controlVariants({ size }), "flex items-center gap-(--cue-space-2)", controlGroupFocusClasses, className),
|
|
183
|
+
children: [/* @__PURE__ */ jsx("input", {
|
|
184
|
+
ref: editorRef,
|
|
185
|
+
type: "text",
|
|
186
|
+
inputMode: "decimal",
|
|
187
|
+
"aria-label": ariaLabel,
|
|
188
|
+
"aria-labelledby": ariaLabelledBy,
|
|
189
|
+
value: draft,
|
|
190
|
+
autoFocus: true,
|
|
191
|
+
onChange: (event) => setDraft(event.currentTarget.value),
|
|
192
|
+
onBlur: commitDraft,
|
|
193
|
+
onKeyDown: (event) => {
|
|
194
|
+
if (event.key === "Enter") {
|
|
195
|
+
event.preventDefault();
|
|
196
|
+
commitDraft();
|
|
197
|
+
} else if (event.key === "Escape") {
|
|
198
|
+
event.preventDefault();
|
|
199
|
+
abandoned.current = true;
|
|
200
|
+
setEditing(false);
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
className: cn(bareControlClasses, "text-right tabular-nums", "[appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none")
|
|
204
|
+
}), unit === void 0 ? null : /* @__PURE__ */ jsx("span", {
|
|
205
|
+
className: "shrink-0 text-fg-subtle select-none",
|
|
206
|
+
children: unit
|
|
207
|
+
})]
|
|
208
|
+
});
|
|
209
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
210
|
+
...elementProps,
|
|
211
|
+
ref,
|
|
212
|
+
role: "spinbutton",
|
|
213
|
+
"data-slot": "scrub-input",
|
|
214
|
+
tabIndex: disabled ? -1 : 0,
|
|
215
|
+
"aria-label": ariaLabel,
|
|
216
|
+
"aria-labelledby": ariaLabelledBy,
|
|
217
|
+
"aria-valuenow": value,
|
|
218
|
+
"aria-valuemin": Number.isFinite(min) ? min : void 0,
|
|
219
|
+
"aria-valuemax": Number.isFinite(max) ? max : void 0,
|
|
220
|
+
"aria-valuetext": valueText,
|
|
221
|
+
"aria-disabled": disabled ? true : void 0,
|
|
222
|
+
onPointerDown: handlePointerDown,
|
|
223
|
+
onPointerMove: handlePointerMove,
|
|
224
|
+
onPointerUp: endGesture,
|
|
225
|
+
onPointerCancel: endGesture,
|
|
226
|
+
onKeyDown: handleKeyDown,
|
|
227
|
+
className: cn(controlVariants({ size }), "flex touch-none items-center justify-between gap-(--cue-space-2) select-none", disabled ? "cursor-default opacity-40" : "cursor-ns-resize", className),
|
|
228
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
229
|
+
"data-slot": "scrub-input-value",
|
|
230
|
+
className: "truncate tabular-nums",
|
|
231
|
+
children: display
|
|
232
|
+
}), unit === void 0 ? null : units === void 0 || units.length < 2 ? /* @__PURE__ */ jsx("span", {
|
|
233
|
+
"data-slot": "scrub-input-unit",
|
|
234
|
+
className: "shrink-0 text-fg-subtle",
|
|
235
|
+
children: unit
|
|
236
|
+
}) : /* @__PURE__ */ jsx("button", {
|
|
237
|
+
type: "button",
|
|
238
|
+
"data-slot": "scrub-input-unit",
|
|
239
|
+
"aria-label": `Unit: ${unit}`,
|
|
240
|
+
disabled,
|
|
241
|
+
onPointerDown: (event) => event.stopPropagation(),
|
|
242
|
+
onClick: cycleUnit,
|
|
243
|
+
className: "shrink-0 cursor-pointer rounded-(--radius-control) border-0 bg-transparent p-0 font-mono text-[length:inherit] text-fg-subtle outline-none transition-colors hover:text-fg focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-offset-1 focus-visible:outline-accent disabled:pointer-events-none",
|
|
244
|
+
children: unit
|
|
245
|
+
})]
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
//#endregion
|
|
249
|
+
export { ScrubInput };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { FormControlSize } from "./_chassis.js";
|
|
2
|
+
import { InputProps } from "./input.js";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
//#region src/forms/search-input.d.ts
|
|
5
|
+
interface SearchInputProps extends Omit<InputProps, "type" | "size" | "value" | "defaultValue" | "onValueChange" | "children"> {
|
|
6
|
+
/** Current query. Supply it with `onValueChange` for a controlled input. */
|
|
7
|
+
value?: string;
|
|
8
|
+
/** Initial query for an uncontrolled input. */
|
|
9
|
+
defaultValue?: string;
|
|
10
|
+
/** Fired on every keystroke, and with `""` when the query is cleared. */
|
|
11
|
+
onValueChange?: (value: string) => void;
|
|
12
|
+
/**
|
|
13
|
+
* Quiet period in ms before {@link SearchInputProps.onDebouncedChange} fires.
|
|
14
|
+
* Defaults to `250`. `0` fires it synchronously with every keystroke.
|
|
15
|
+
*/
|
|
16
|
+
debounce?: number;
|
|
17
|
+
/**
|
|
18
|
+
* Fired once the user stops typing for `debounce` ms — the callback to hang a
|
|
19
|
+
* query or a filter recomputation on. Clearing flushes it immediately, because
|
|
20
|
+
* "show me everything again" should never wait.
|
|
21
|
+
*/
|
|
22
|
+
onDebouncedChange?: (value: string) => void;
|
|
23
|
+
/** Fired when the query is cleared, by Escape or by the clear button. */
|
|
24
|
+
onClear?: () => void;
|
|
25
|
+
/** Leading glyph component. Defaults to a magnifier. */
|
|
26
|
+
icon?: React.ElementType;
|
|
27
|
+
/** Accessible name for the clear button. Defaults to `"Clear search"`. */
|
|
28
|
+
clearLabel?: string;
|
|
29
|
+
/** Rung of the control ladder. Defaults to `"md"`. */
|
|
30
|
+
size?: FormControlSize;
|
|
31
|
+
/** Extra classes for the group that wraps the icon, the input and the clear button. */
|
|
32
|
+
className?: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* A filter box: leading magnifier, Escape to clear, and a debounced change
|
|
36
|
+
* callback for whatever the query drives.
|
|
37
|
+
*
|
|
38
|
+
* Escape-to-clear is the reason this is a component and not a recipe. It is the
|
|
39
|
+
* behaviour every console filter needs and the one every hand-rolled search box
|
|
40
|
+
* forgets, and getting it right means also stopping the key from bubbling into a
|
|
41
|
+
* dialog that would otherwise close underneath the user.
|
|
42
|
+
*
|
|
43
|
+
* Debouncing is deliberately separate from `onValueChange`: the input stays
|
|
44
|
+
* responsive on every keystroke while the expensive work waits for the typing to
|
|
45
|
+
* stop. Clearing flushes the debounce rather than scheduling it.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* <SearchInput aria-label="Filter cues" onDebouncedChange={setQuery} />
|
|
49
|
+
* @example
|
|
50
|
+
* <SearchInput value={q} onValueChange={setQ} debounce={0} size="sm" />
|
|
51
|
+
*/
|
|
52
|
+
declare const SearchInput: React.ForwardRefExoticComponent<Omit<SearchInputProps, "ref"> & React.RefAttributes<HTMLInputElement>>;
|
|
53
|
+
//#endregion
|
|
54
|
+
export { SearchInput, SearchInputProps };
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "../lib/cn.js";
|
|
3
|
+
import { InputGroup } from "./input-group.js";
|
|
4
|
+
import { Input } from "./input.js";
|
|
5
|
+
import { CrossGlyph, SearchGlyph } from "./_glyphs.js";
|
|
6
|
+
import * as React from "react";
|
|
7
|
+
import { jsx } from "react/jsx-runtime";
|
|
8
|
+
//#region src/forms/search-input.tsx
|
|
9
|
+
const DEFAULT_DEBOUNCE_MS = 250;
|
|
10
|
+
/**
|
|
11
|
+
* A filter box: leading magnifier, Escape to clear, and a debounced change
|
|
12
|
+
* callback for whatever the query drives.
|
|
13
|
+
*
|
|
14
|
+
* Escape-to-clear is the reason this is a component and not a recipe. It is the
|
|
15
|
+
* behaviour every console filter needs and the one every hand-rolled search box
|
|
16
|
+
* forgets, and getting it right means also stopping the key from bubbling into a
|
|
17
|
+
* dialog that would otherwise close underneath the user.
|
|
18
|
+
*
|
|
19
|
+
* Debouncing is deliberately separate from `onValueChange`: the input stays
|
|
20
|
+
* responsive on every keystroke while the expensive work waits for the typing to
|
|
21
|
+
* stop. Clearing flushes the debounce rather than scheduling it.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* <SearchInput aria-label="Filter cues" onDebouncedChange={setQuery} />
|
|
25
|
+
* @example
|
|
26
|
+
* <SearchInput value={q} onValueChange={setQ} debounce={0} size="sm" />
|
|
27
|
+
*/
|
|
28
|
+
const SearchInput = React.forwardRef(function SearchInput({ className, value, defaultValue, onValueChange, onChange, onKeyDown, debounce = DEFAULT_DEBOUNCE_MS, onDebouncedChange, onClear, icon: Icon = SearchGlyph, clearLabel = "Clear search", size, ...elementProps }, ref) {
|
|
29
|
+
const isControlled = value !== void 0;
|
|
30
|
+
const [internal, setInternal] = React.useState(defaultValue ?? "");
|
|
31
|
+
const current = isControlled ? value : internal;
|
|
32
|
+
const innerRef = React.useRef(null);
|
|
33
|
+
const setRefs = React.useCallback((node) => {
|
|
34
|
+
innerRef.current = node;
|
|
35
|
+
if (typeof ref === "function") ref(node);
|
|
36
|
+
else if (ref !== null) ref.current = node;
|
|
37
|
+
}, [ref]);
|
|
38
|
+
const debouncedRef = React.useRef(onDebouncedChange);
|
|
39
|
+
React.useEffect(() => {
|
|
40
|
+
debouncedRef.current = onDebouncedChange;
|
|
41
|
+
}, [onDebouncedChange]);
|
|
42
|
+
const lastNotified = React.useRef(current);
|
|
43
|
+
React.useEffect(() => {
|
|
44
|
+
const notify = debouncedRef.current;
|
|
45
|
+
if (notify === void 0 || current === lastNotified.current) return;
|
|
46
|
+
if (debounce <= 0) {
|
|
47
|
+
lastNotified.current = current;
|
|
48
|
+
notify(current);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const timer = setTimeout(() => {
|
|
52
|
+
lastNotified.current = current;
|
|
53
|
+
notify(current);
|
|
54
|
+
}, debounce);
|
|
55
|
+
return () => clearTimeout(timer);
|
|
56
|
+
}, [current, debounce]);
|
|
57
|
+
const commit = React.useCallback((next) => {
|
|
58
|
+
if (!isControlled) setInternal(next);
|
|
59
|
+
onValueChange?.(next);
|
|
60
|
+
}, [isControlled, onValueChange]);
|
|
61
|
+
const clear = React.useCallback(() => {
|
|
62
|
+
commit("");
|
|
63
|
+
lastNotified.current = "";
|
|
64
|
+
debouncedRef.current?.("");
|
|
65
|
+
onClear?.();
|
|
66
|
+
innerRef.current?.focus();
|
|
67
|
+
}, [commit, onClear]);
|
|
68
|
+
const handleChange = React.useCallback((event) => {
|
|
69
|
+
onChange?.(event);
|
|
70
|
+
commit(event.currentTarget.value);
|
|
71
|
+
}, [commit, onChange]);
|
|
72
|
+
const handleKeyDown = React.useCallback((event) => {
|
|
73
|
+
onKeyDown?.(event);
|
|
74
|
+
if (event.defaultPrevented || event.key !== "Escape") return;
|
|
75
|
+
if (current === "") return;
|
|
76
|
+
event.preventDefault();
|
|
77
|
+
event.stopPropagation();
|
|
78
|
+
clear();
|
|
79
|
+
}, [
|
|
80
|
+
clear,
|
|
81
|
+
current,
|
|
82
|
+
onKeyDown
|
|
83
|
+
]);
|
|
84
|
+
return /* @__PURE__ */ jsx(InputGroup, {
|
|
85
|
+
size,
|
|
86
|
+
className,
|
|
87
|
+
leading: /* @__PURE__ */ jsx(Icon, {
|
|
88
|
+
"data-slot": "search-icon",
|
|
89
|
+
"aria-hidden": "true",
|
|
90
|
+
className: "size-icon-md"
|
|
91
|
+
}),
|
|
92
|
+
trailing: current === "" ? void 0 : /* @__PURE__ */ jsx("button", {
|
|
93
|
+
type: "button",
|
|
94
|
+
"aria-label": clearLabel,
|
|
95
|
+
onClick: clear,
|
|
96
|
+
className: "flex shrink-0 cursor-pointer items-center justify-center rounded-(--radius-control) text-fg-subtle outline-none transition-colors hover:text-fg focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-offset-1 focus-visible:outline-accent",
|
|
97
|
+
children: /* @__PURE__ */ jsx(CrossGlyph, { className: "size-icon-sm" })
|
|
98
|
+
}),
|
|
99
|
+
children: /* @__PURE__ */ jsx(Input, {
|
|
100
|
+
ref: setRefs,
|
|
101
|
+
type: "search",
|
|
102
|
+
"data-slot": "search-input",
|
|
103
|
+
value: current,
|
|
104
|
+
onChange: handleChange,
|
|
105
|
+
onKeyDown: handleKeyDown,
|
|
106
|
+
className: cn("[&::-webkit-search-cancel-button]:appearance-none"),
|
|
107
|
+
...elementProps
|
|
108
|
+
})
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
//#endregion
|
|
112
|
+
export { SearchInput };
|