@teamofsilicons/silicon-ui 0.0.0-stage → 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/LICENSE +22 -0
- package/README.md +63 -2
- package/components/accordion/accordion.d.ts +13 -0
- package/components/accordion/accordion.js +35 -0
- package/components/accordion/accordion.module.css +15 -0
- package/components/action-button/action-button.d.ts +11 -0
- package/components/action-button/action-button.js +57 -0
- package/components/action-button/action-button.module.css +15 -0
- package/components/activity-heatmap/activity-heatmap.d.ts +37 -0
- package/components/activity-heatmap/activity-heatmap.js +303 -0
- package/components/activity-heatmap/activity-heatmap.module.css +83 -0
- package/components/alert/alert.d.ts +13 -0
- package/components/alert/alert.js +79 -0
- package/components/alert/alert.module.css +15 -0
- package/components/animated-counter/animated-counter.d.ts +13 -0
- package/components/animated-counter/animated-counter.js +73 -0
- package/components/animated-counter/animated-counter.module.css +12 -0
- package/components/announcement-bar/announcement-bar.d.ts +53 -0
- package/components/announcement-bar/announcement-bar.js +187 -0
- package/components/announcement-bar/announcement-bar.module.css +60 -0
- package/components/avatar/avatar.d.ts +8 -0
- package/components/avatar/avatar.js +18 -0
- package/components/avatar/avatar.module.css +60 -0
- package/components/avatar-group/avatar-group.d.ts +13 -0
- package/components/avatar-group/avatar-group.js +24 -0
- package/components/avatar-group/avatar-group.module.css +27 -0
- package/components/badge/badge.d.ts +10 -0
- package/components/badge/badge.js +66 -0
- package/components/badge/badge.module.css +52 -0
- package/components/bar-chart/bar-chart.d.ts +32 -0
- package/components/bar-chart/bar-chart.js +327 -0
- package/components/bar-chart/bar-chart.module.css +49 -0
- package/components/billing-toggle/billing-toggle.d.ts +35 -0
- package/components/billing-toggle/billing-toggle.js +83 -0
- package/components/billing-toggle/billing-toggle.module.css +139 -0
- package/components/bottom-sheet/bottom-sheet.d.ts +27 -0
- package/components/bottom-sheet/bottom-sheet.js +386 -0
- package/components/bottom-sheet/bottom-sheet.module.css +69 -0
- package/components/breadcrumb/breadcrumb.d.ts +13 -0
- package/components/breadcrumb/breadcrumb.js +19 -0
- package/components/breadcrumb/breadcrumb.module.css +12 -0
- package/components/brush-chart/brush-chart.d.ts +45 -0
- package/components/brush-chart/brush-chart.js +426 -0
- package/components/brush-chart/brush-chart.module.css +69 -0
- package/components/button/button.d.ts +10 -0
- package/components/button/button.js +97 -0
- package/components/button/button.module.css +39 -0
- package/components/button-group/button-group.d.ts +59 -0
- package/components/button-group/button-group.js +280 -0
- package/components/button-group/button-group.module.css +87 -0
- package/components/button-link/button-link.d.ts +24 -0
- package/components/button-link/button-link.js +11 -0
- package/components/button-link/button-link.module.css +7 -0
- package/components/calendar/calendar.d.ts +22 -0
- package/components/calendar/calendar.js +253 -0
- package/components/calendar/calendar.module.css +51 -0
- package/components/card/card.d.ts +19 -0
- package/components/card/card.js +108 -0
- package/components/card/card.module.css +51 -0
- package/components/card-stack/card-stack.d.ts +34 -0
- package/components/card-stack/card-stack.js +291 -0
- package/components/card-stack/card-stack.module.css +63 -0
- package/components/carousel/carousel.d.ts +28 -0
- package/components/carousel/carousel.js +332 -0
- package/components/carousel/carousel.module.css +42 -0
- package/components/chat-thread/chat-thread.d.ts +135 -0
- package/components/chat-thread/chat-thread.js +406 -0
- package/components/chat-thread/chat-thread.module.css +242 -0
- package/components/checkbox/checkbox.d.ts +7 -0
- package/components/checkbox/checkbox.js +23 -0
- package/components/checkbox/checkbox.module.css +15 -0
- package/components/chip-group/chip-group.d.ts +24 -0
- package/components/chip-group/chip-group.js +196 -0
- package/components/chip-group/chip-group.module.css +38 -0
- package/components/code-block/code-block.d.ts +11 -0
- package/components/code-block/code-block.js +133 -0
- package/components/code-block/code-block.module.css +43 -0
- package/components/color-picker/color-picker.d.ts +40 -0
- package/components/color-picker/color-picker.js +449 -0
- package/components/color-picker/color-picker.module.css +111 -0
- package/components/combobox/combobox.d.ts +19 -0
- package/components/combobox/combobox.js +167 -0
- package/components/combobox/combobox.module.css +28 -0
- package/components/command-palette/command-palette.d.ts +20 -0
- package/components/command-palette/command-palette.js +151 -0
- package/components/command-palette/command-palette.module.css +94 -0
- package/components/comment-thread/comment-thread.d.ts +120 -0
- package/components/comment-thread/comment-thread.js +343 -0
- package/components/comment-thread/comment-thread.module.css +195 -0
- package/components/confirm-morph/confirm-morph.d.ts +65 -0
- package/components/confirm-morph/confirm-morph.js +355 -0
- package/components/confirm-morph/confirm-morph.module.css +146 -0
- package/components/context-menu/context-menu.d.ts +18 -0
- package/components/context-menu/context-menu.js +148 -0
- package/components/context-menu/context-menu.module.css +17 -0
- package/components/copy-button/copy-button.d.ts +10 -0
- package/components/copy-button/copy-button.js +69 -0
- package/components/copy-button/copy-button.module.css +21 -0
- package/components/countdown/countdown.d.ts +31 -0
- package/components/countdown/countdown.js +177 -0
- package/components/countdown/countdown.module.css +49 -0
- package/components/date-picker/date-picker.d.ts +17 -0
- package/components/date-picker/date-picker.js +100 -0
- package/components/date-picker/date-picker.module.css +34 -0
- package/components/date-range-picker/date-range-picker.d.ts +44 -0
- package/components/date-range-picker/date-range-picker.js +534 -0
- package/components/date-range-picker/date-range-picker.module.css +214 -0
- package/components/dialog/dialog.d.ts +11 -0
- package/components/dialog/dialog.js +52 -0
- package/components/dialog/dialog.module.css +36 -0
- package/components/donut-chart/donut-chart.d.ts +46 -0
- package/components/donut-chart/donut-chart.js +382 -0
- package/components/donut-chart/donut-chart.module.css +48 -0
- package/components/drawer/drawer.d.ts +20 -0
- package/components/drawer/drawer.js +111 -0
- package/components/drawer/drawer.module.css +97 -0
- package/components/dropdown-menu/dropdown-menu.d.ts +16 -0
- package/components/dropdown-menu/dropdown-menu.js +39 -0
- package/components/dropdown-menu/dropdown-menu.module.css +38 -0
- package/components/elastic-slider/elastic-slider.d.ts +39 -0
- package/components/elastic-slider/elastic-slider.js +261 -0
- package/components/elastic-slider/elastic-slider.module.css +40 -0
- package/components/empty-state/empty-state.d.ts +12 -0
- package/components/empty-state/empty-state.js +69 -0
- package/components/empty-state/empty-state.module.css +13 -0
- package/components/expandable-card/expandable-card.d.ts +13 -0
- package/components/expandable-card/expandable-card.js +89 -0
- package/components/expandable-card/expandable-card.module.css +22 -0
- package/components/expanding-button-group/expanding-button-group.d.ts +41 -0
- package/components/expanding-button-group/expanding-button-group.js +258 -0
- package/components/expanding-button-group/expanding-button-group.module.css +44 -0
- package/components/expanding-search/expanding-search.d.ts +38 -0
- package/components/expanding-search/expanding-search.js +242 -0
- package/components/expanding-search/expanding-search.module.css +52 -0
- package/components/file-dropzone/file-dropzone.d.ts +44 -0
- package/components/file-dropzone/file-dropzone.js +379 -0
- package/components/file-dropzone/file-dropzone.module.css +115 -0
- package/components/file-upload/file-upload.d.ts +22 -0
- package/components/file-upload/file-upload.js +192 -0
- package/components/file-upload/file-upload.module.css +49 -0
- package/components/filter-toolbar/filter-toolbar.d.ts +48 -0
- package/components/filter-toolbar/filter-toolbar.js +446 -0
- package/components/filter-toolbar/filter-toolbar.module.css +70 -0
- package/components/floating-button-group/floating-button-group.d.ts +51 -0
- package/components/floating-button-group/floating-button-group.js +215 -0
- package/components/floating-button-group/floating-button-group.module.css +74 -0
- package/components/gauge/gauge.d.ts +18 -0
- package/components/gauge/gauge.js +101 -0
- package/components/gauge/gauge.module.css +22 -0
- package/components/hold-to-confirm/hold-to-confirm.d.ts +25 -0
- package/components/hold-to-confirm/hold-to-confirm.js +165 -0
- package/components/hold-to-confirm/hold-to-confirm.module.css +20 -0
- package/components/hover-card/hover-card.d.ts +38 -0
- package/components/hover-card/hover-card.js +145 -0
- package/components/hover-card/hover-card.module.css +20 -0
- package/components/image-compare/image-compare.d.ts +27 -0
- package/components/image-compare/image-compare.js +267 -0
- package/components/image-compare/image-compare.module.css +25 -0
- package/components/in-view-title/in-view-title.d.ts +17 -0
- package/components/in-view-title/in-view-title.js +70 -0
- package/components/in-view-title/in-view-title.module.css +13 -0
- package/components/inline-edit/inline-edit.d.ts +27 -0
- package/components/inline-edit/inline-edit.js +315 -0
- package/components/inline-edit/inline-edit.module.css +73 -0
- package/components/input/input.d.ts +7 -0
- package/components/input/input.js +60 -0
- package/components/input/input.module.css +22 -0
- package/components/json-viewer/json-viewer.d.ts +48 -0
- package/components/json-viewer/json-viewer.js +463 -0
- package/components/json-viewer/json-viewer.module.css +92 -0
- package/components/line-chart/line-chart.d.ts +58 -0
- package/components/line-chart/line-chart.js +383 -0
- package/components/line-chart/line-chart.module.css +60 -0
- package/components/mention-input/mention-input.d.ts +72 -0
- package/components/mention-input/mention-input.js +417 -0
- package/components/mention-input/mention-input.module.css +134 -0
- package/components/metric-card/metric-card.d.ts +8 -0
- package/components/metric-card/metric-card.js +44 -0
- package/components/metric-card/metric-card.module.css +18 -0
- package/components/money-input/money-input.d.ts +48 -0
- package/components/money-input/money-input.js +503 -0
- package/components/money-input/money-input.module.css +77 -0
- package/components/morph-select/morph-select.d.ts +50 -0
- package/components/morph-select/morph-select.js +395 -0
- package/components/morph-select/morph-select.module.css +88 -0
- package/components/multi-select/multi-select.d.ts +18 -0
- package/components/multi-select/multi-select.js +105 -0
- package/components/multi-select/multi-select.module.css +37 -0
- package/components/notification-center/notification-center.d.ts +24 -0
- package/components/notification-center/notification-center.js +146 -0
- package/components/notification-center/notification-center.module.css +79 -0
- package/components/number-field/number-field.d.ts +41 -0
- package/components/number-field/number-field.js +455 -0
- package/components/number-field/number-field.module.css +71 -0
- package/components/otp-input/otp-input.d.ts +13 -0
- package/components/otp-input/otp-input.js +129 -0
- package/components/otp-input/otp-input.module.css +28 -0
- package/components/pagination/pagination.d.ts +7 -0
- package/components/pagination/pagination.js +60 -0
- package/components/pagination/pagination.module.css +16 -0
- package/components/password-field/password-field.d.ts +14 -0
- package/components/password-field/password-field.js +51 -0
- package/components/password-field/password-field.module.css +27 -0
- package/components/password-strength/password-strength.d.ts +107 -0
- package/components/password-strength/password-strength.js +161 -0
- package/components/password-strength/password-strength.module.css +57 -0
- package/components/phone-input/phone-input.d.ts +73 -0
- package/components/phone-input/phone-input.js +612 -0
- package/components/phone-input/phone-input.module.css +98 -0
- package/components/popover/popover.d.ts +6 -0
- package/components/popover/popover.js +19 -0
- package/components/popover/popover.module.css +32 -0
- package/components/progress/progress.d.ts +8 -0
- package/components/progress/progress.js +42 -0
- package/components/progress/progress.module.css +1 -0
- package/components/radio-cards/radio-cards.d.ts +35 -0
- package/components/radio-cards/radio-cards.js +110 -0
- package/components/radio-cards/radio-cards.module.css +84 -0
- package/components/radio-group/radio-group.d.ts +13 -0
- package/components/radio-group/radio-group.js +55 -0
- package/components/radio-group/radio-group.module.css +30 -0
- package/components/resizable-panels/resizable-panels.d.ts +37 -0
- package/components/resizable-panels/resizable-panels.js +338 -0
- package/components/resizable-panels/resizable-panels.module.css +40 -0
- package/components/rich-text-editor/rich-text-editor.d.ts +55 -0
- package/components/rich-text-editor/rich-text-editor.js +1733 -0
- package/components/rich-text-editor/rich-text-editor.module.css +201 -0
- package/components/ridgeline/ridgeline.d.ts +37 -0
- package/components/ridgeline/ridgeline.js +346 -0
- package/components/ridgeline/ridgeline.module.css +58 -0
- package/components/scroll-area/scroll-area.d.ts +40 -0
- package/components/scroll-area/scroll-area.js +153 -0
- package/components/scroll-area/scroll-area.module.css +79 -0
- package/components/search-field/search-field.d.ts +7 -0
- package/components/search-field/search-field.js +20 -0
- package/components/search-field/search-field.module.css +17 -0
- package/components/segmented-control/segmented-control.d.ts +16 -0
- package/components/segmented-control/segmented-control.js +56 -0
- package/components/segmented-control/segmented-control.module.css +15 -0
- package/components/select/select.d.ts +15 -0
- package/components/select/select.js +36 -0
- package/components/select/select.module.css +35 -0
- package/components/shortcut-recorder/shortcut-recorder.d.ts +108 -0
- package/components/shortcut-recorder/shortcut-recorder.js +380 -0
- package/components/shortcut-recorder/shortcut-recorder.module.css +85 -0
- package/components/signature-pad/signature-pad.d.ts +72 -0
- package/components/signature-pad/signature-pad.js +450 -0
- package/components/signature-pad/signature-pad.module.css +84 -0
- package/components/skeleton/skeleton.d.ts +13 -0
- package/components/skeleton/skeleton.js +53 -0
- package/components/skeleton/skeleton.module.css +11 -0
- package/components/slider/slider.d.ts +45 -0
- package/components/slider/slider.js +347 -0
- package/components/slider/slider.module.css +45 -0
- package/components/slope-chart/slope-chart.d.ts +38 -0
- package/components/slope-chart/slope-chart.js +183 -0
- package/components/slope-chart/slope-chart.module.css +51 -0
- package/components/slot-text/slot-text.d.ts +26 -0
- package/components/slot-text/slot-text.js +119 -0
- package/components/slot-text/slot-text.module.css +19 -0
- package/components/sortable-data-table/sortable-data-table.d.ts +38 -0
- package/components/sortable-data-table/sortable-data-table.js +240 -0
- package/components/sortable-data-table/sortable-data-table.module.css +116 -0
- package/components/sparkline/sparkline.d.ts +19 -0
- package/components/sparkline/sparkline.js +239 -0
- package/components/sparkline/sparkline.module.css +26 -0
- package/components/split-button/split-button.d.ts +19 -0
- package/components/split-button/split-button.js +33 -0
- package/components/split-button/split-button.module.css +51 -0
- package/components/stepper/stepper.d.ts +36 -0
- package/components/stepper/stepper.js +108 -0
- package/components/stepper/stepper.module.css +74 -0
- package/components/streamgraph/streamgraph.d.ts +51 -0
- package/components/streamgraph/streamgraph.js +376 -0
- package/components/streamgraph/streamgraph.module.css +55 -0
- package/components/swipe-actions/swipe-actions.d.ts +35 -0
- package/components/swipe-actions/swipe-actions.js +330 -0
- package/components/swipe-actions/swipe-actions.module.css +64 -0
- package/components/switch/switch.d.ts +7 -0
- package/components/switch/switch.js +45 -0
- package/components/switch/switch.module.css +12 -0
- package/components/tabs/tabs.d.ts +8 -0
- package/components/tabs/tabs.js +138 -0
- package/components/tabs/tabs.module.css +27 -0
- package/components/tag-input/tag-input.d.ts +15 -0
- package/components/tag-input/tag-input.js +150 -0
- package/components/tag-input/tag-input.module.css +34 -0
- package/components/text-morph/text-morph.d.ts +13 -0
- package/components/text-morph/text-morph.js +69 -0
- package/components/text-morph/text-morph.module.css +8 -0
- package/components/text-reveal/text-reveal.d.ts +14 -0
- package/components/text-reveal/text-reveal.js +20 -0
- package/components/text-reveal/text-reveal.module.css +13 -0
- package/components/text-shimmer/text-shimmer.d.ts +18 -0
- package/components/text-shimmer/text-shimmer.js +90 -0
- package/components/text-shimmer/text-shimmer.module.css +9 -0
- package/components/textarea/textarea.d.ts +7 -0
- package/components/textarea/textarea.js +56 -0
- package/components/textarea/textarea.module.css +22 -0
- package/components/theme-switch/theme-switch.d.ts +11 -0
- package/components/theme-switch/theme-switch.js +37 -0
- package/components/theme-switch/theme-switch.module.css +31 -0
- package/components/time-picker/time-picker.d.ts +13 -0
- package/components/time-picker/time-picker.js +95 -0
- package/components/time-picker/time-picker.module.css +23 -0
- package/components/timeline/timeline.d.ts +47 -0
- package/components/timeline/timeline.js +171 -0
- package/components/timeline/timeline.module.css +51 -0
- package/components/toast/toast.d.ts +8 -0
- package/components/toast/toast.js +118 -0
- package/components/toast/toast.module.css +100 -0
- package/components/toast-stack/toast-stack.d.ts +57 -0
- package/components/toast-stack/toast-stack.js +399 -0
- package/components/toast-stack/toast-stack.module.css +61 -0
- package/components/tooltip/tooltip.d.ts +8 -0
- package/components/tooltip/tooltip.js +62 -0
- package/components/tooltip/tooltip.module.css +18 -0
- package/components/tree-view/tree-view.d.ts +17 -0
- package/components/tree-view/tree-view.js +147 -0
- package/components/tree-view/tree-view.module.css +22 -0
- package/components/treemap/treemap.d.ts +35 -0
- package/components/treemap/treemap.js +375 -0
- package/components/treemap/treemap.module.css +59 -0
- package/components/usage-meter/usage-meter.d.ts +24 -0
- package/components/usage-meter/usage-meter.js +296 -0
- package/components/usage-meter/usage-meter.module.css +61 -0
- package/components/user-menu/user-menu.d.ts +62 -0
- package/components/user-menu/user-menu.js +316 -0
- package/components/user-menu/user-menu.module.css +97 -0
- package/components/waffle-chart/waffle-chart.d.ts +39 -0
- package/components/waffle-chart/waffle-chart.js +244 -0
- package/components/waffle-chart/waffle-chart.module.css +52 -0
- package/components.json +1330 -0
- package/lib/escape.d.ts +19 -0
- package/lib/escape.js +50 -0
- package/lib/media.d.ts +353 -0
- package/lib/media.js +65 -0
- package/lib/menu-highlight.d.ts +28 -0
- package/lib/menu-highlight.js +43 -0
- package/lib/motion-tokens.d.ts +60 -0
- package/lib/motion-tokens.js +25 -0
- package/lib/return-focus.d.ts +9 -0
- package/lib/return-focus.js +53 -0
- package/lib/silicon-provider.d.ts +60 -0
- package/lib/silicon-provider.js +47 -0
- package/lib/use-copy-feedback.d.ts +8 -0
- package/lib/use-copy-feedback.js +36 -0
- package/lib/use-today.d.ts +2 -0
- package/lib/use-today.js +29 -0
- package/package.json +66 -4
- package/styles.css +218 -0
- package/styles.css.d.ts +1 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { FocusEvent as ReactFocusEvent } from "react";
|
|
2
|
+
export interface MoneyInputDetails {
|
|
3
|
+
currency: string;
|
|
4
|
+
/** The amount in major units, such as 12.5 for $12.50. */
|
|
5
|
+
major: number | null;
|
|
6
|
+
/** The amount as a full currency string, such as "$12.50". */
|
|
7
|
+
formatted: string;
|
|
8
|
+
}
|
|
9
|
+
export interface MoneyInputProps {
|
|
10
|
+
label: string;
|
|
11
|
+
hideLabel?: boolean;
|
|
12
|
+
/** Amount in minor units (cents for USD, yen for JPY). `null` is empty. */
|
|
13
|
+
value?: number | null;
|
|
14
|
+
defaultValue?: number | null;
|
|
15
|
+
onValueChange?: (value: number | null, details: MoneyInputDetails) => void;
|
|
16
|
+
/** ISO 4217 code, such as "USD". */
|
|
17
|
+
currency?: string;
|
|
18
|
+
defaultCurrency?: string;
|
|
19
|
+
onCurrencyChange?: (currency: string) => void;
|
|
20
|
+
/** Codes offered by the currency menu. Pass one code to hide the menu. */
|
|
21
|
+
currencies?: string[];
|
|
22
|
+
/** Lowest amount in minor units. Checked when the field loses focus. */
|
|
23
|
+
min?: number;
|
|
24
|
+
/** Highest amount in minor units. Typing past it is refused with a nudge. */
|
|
25
|
+
max?: number;
|
|
26
|
+
/** Quick add chips in major units. Pass an empty array to hide them. */
|
|
27
|
+
quickAdd?: number[];
|
|
28
|
+
/** Arrow keys move by this many minor units. Defaults to one major unit; Shift moves ten times as far. */
|
|
29
|
+
step?: number;
|
|
30
|
+
/** Formatting locale. Fixed by default so server and client render the same digits. */
|
|
31
|
+
locale?: string;
|
|
32
|
+
description?: string;
|
|
33
|
+
/** Replaces the built-in range message. */
|
|
34
|
+
error?: string;
|
|
35
|
+
disabled?: boolean;
|
|
36
|
+
/** Adds a hidden input carrying the amount in minor units. */
|
|
37
|
+
name?: string;
|
|
38
|
+
id?: string;
|
|
39
|
+
className?: string;
|
|
40
|
+
onBlur?: (event: ReactFocusEvent<HTMLInputElement>) => void;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* A currency amount field. Digits group as they are typed without moving the caret, the missing cents wait as faint ghosts
|
|
44
|
+
* so the amount never changes width when it settles, long amounts scale down to fit instead of scrolling, and quick add chips
|
|
45
|
+
* and arrow keys roll the digits. The value comes out in minor units, so $12.50 is 1250.
|
|
46
|
+
*/
|
|
47
|
+
export declare const MoneyInput: import("react").ForwardRefExoticComponent<MoneyInputProps & import("react").RefAttributes<HTMLInputElement>>;
|
|
48
|
+
export default MoneyInput;
|
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { forwardRef, useCallback, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useReducer, useRef, useState, useSyncExternalStore } from "react";
|
|
4
|
+
import { AnimatePresence, animate, motion, useMotionValue, useReducedMotion } from "motion/react";
|
|
5
|
+
import { Check, ChevronDown } from "lucide-react";
|
|
6
|
+
import { motionTokens } from "../../lib/motion-tokens.js";
|
|
7
|
+
import styles from "./money-input.module.css";
|
|
8
|
+
const enterEase = [...motionTokens.ease.enter];
|
|
9
|
+
const standardEase = [...motionTokens.ease.standard];
|
|
10
|
+
const { spring, blur, duration } = motionTokens;
|
|
11
|
+
const physical = (visualDuration, bounce) => {
|
|
12
|
+
const root = (2 * Math.PI) / (visualDuration * 1.2);
|
|
13
|
+
return { type: "spring", stiffness: root * root, damping: 2 * (1 - bounce) * root, mass: 1 };
|
|
14
|
+
};
|
|
15
|
+
const GLIDE = physical(.28, .08);
|
|
16
|
+
/** A sideways kick when a limit refuses the change: the amount strains toward the press and springs home. */
|
|
17
|
+
const KICK = physical(.42, .3);
|
|
18
|
+
const MAX_INTEGER_DIGITS = 12;
|
|
19
|
+
const subscribe = () => () => { };
|
|
20
|
+
function useReducedFlag() {
|
|
21
|
+
const hydrated = useSyncExternalStore(subscribe, () => true, () => false);
|
|
22
|
+
return !!useReducedMotion() && hydrated;
|
|
23
|
+
}
|
|
24
|
+
const infoCache = new Map();
|
|
25
|
+
function currencyInfo(code, locale) {
|
|
26
|
+
const cacheKey = `${locale}:${code}`;
|
|
27
|
+
const cached = infoCache.get(cacheKey);
|
|
28
|
+
if (cached)
|
|
29
|
+
return cached;
|
|
30
|
+
const full = new Intl.NumberFormat(locale, { style: "currency", currency: code, currencyDisplay: "narrowSymbol", numberingSystem: "latn" });
|
|
31
|
+
const parts = full.formatToParts(1);
|
|
32
|
+
const symbolAt = parts.findIndex(part => part.type === "currency"), numberAt = parts.findIndex(part => part.type === "integer");
|
|
33
|
+
const digits = full.resolvedOptions().maximumFractionDigits ?? 2;
|
|
34
|
+
let name = code;
|
|
35
|
+
try {
|
|
36
|
+
name = new Intl.DisplayNames([locale], { type: "currency" }).of(code) ?? code;
|
|
37
|
+
}
|
|
38
|
+
catch { /* older engines keep the code */ }
|
|
39
|
+
const info = {
|
|
40
|
+
code, symbol: parts[symbolAt]?.value ?? code, position: symbolAt < numberAt ? "prefix" : "suffix", digits, name: name.charAt(0).toUpperCase() + name.slice(1), full,
|
|
41
|
+
whole: new Intl.NumberFormat(locale, { style: "currency", currency: code, currencyDisplay: "narrowSymbol", maximumFractionDigits: 0, numberingSystem: "latn" }),
|
|
42
|
+
};
|
|
43
|
+
infoCache.set(cacheKey, info);
|
|
44
|
+
return info;
|
|
45
|
+
}
|
|
46
|
+
function localeSymbols(locale) {
|
|
47
|
+
const parts = new Intl.NumberFormat(locale, { numberingSystem: "latn" }).formatToParts(12345.6);
|
|
48
|
+
return { group: parts.find(part => part.type === "group")?.value ?? ",", decimal: parts.find(part => part.type === "decimal")?.value ?? "." };
|
|
49
|
+
}
|
|
50
|
+
/* A draft is the amount as typed in a canonical form: digits with an optional ".", such as "1234.5". */
|
|
51
|
+
const pow10 = (digits) => 10 ** digits;
|
|
52
|
+
function toMinor(draft, digits) {
|
|
53
|
+
if (!draft)
|
|
54
|
+
return null;
|
|
55
|
+
const [whole, fraction = ""] = draft.split(".");
|
|
56
|
+
return Number(whole || "0") * pow10(digits) + Number(fraction.padEnd(digits, "0").slice(0, digits) || "0");
|
|
57
|
+
}
|
|
58
|
+
function fromMinor(minor, digits) {
|
|
59
|
+
if (minor === null || minor === undefined || !Number.isFinite(minor))
|
|
60
|
+
return "";
|
|
61
|
+
const text = String(Math.round(Math.abs(minor))).padStart(digits + 1, "0");
|
|
62
|
+
return digits ? `${text.slice(0, text.length - digits)}.${text.slice(text.length - digits)}` : text;
|
|
63
|
+
}
|
|
64
|
+
/** Settles a draft the way it will be read back: fraction padded to the currency, leading zeros gone. */
|
|
65
|
+
function settle(draft, digits) { return draft ? fromMinor(toMinor(draft, digits), digits) : ""; }
|
|
66
|
+
/** Carries an amount across currencies with different minor units, keeping the major amount. */
|
|
67
|
+
function convertDraft(draft, digits) {
|
|
68
|
+
if (!draft)
|
|
69
|
+
return "";
|
|
70
|
+
const [whole = "", fraction] = draft.split(".");
|
|
71
|
+
if (!digits)
|
|
72
|
+
return whole || "0";
|
|
73
|
+
return fraction === undefined ? whole : `${whole}.${fraction.slice(0, digits)}`;
|
|
74
|
+
}
|
|
75
|
+
function groupInteger(whole, group) { return whole.replace(/\B(?=(\d{3})+(?!\d))/g, group); }
|
|
76
|
+
function displayOf(draft, symbols) {
|
|
77
|
+
if (!draft)
|
|
78
|
+
return "";
|
|
79
|
+
const [whole = "", fraction] = draft.split(".");
|
|
80
|
+
return groupInteger(whole, symbols.group) + (fraction !== undefined ? symbols.decimal + fraction : "");
|
|
81
|
+
}
|
|
82
|
+
/** Columns keyed by place value, so 9 → 10 keeps the ones column the ones column. Missing fraction digits are ghosts that hold their width. */
|
|
83
|
+
function partsOf(draft, digits, symbols) {
|
|
84
|
+
const [whole = "", fraction] = draft ? draft.split(".") : [];
|
|
85
|
+
const parts = [];
|
|
86
|
+
const intDigits = whole || "0";
|
|
87
|
+
const ghostInt = !whole;
|
|
88
|
+
[...intDigits].forEach((char, index) => {
|
|
89
|
+
const place = intDigits.length - 1 - index;
|
|
90
|
+
parts.push({ key: `i${place}`, char, ghost: ghostInt });
|
|
91
|
+
if (place > 0 && place % 3 === 0)
|
|
92
|
+
parts.push({ key: `g${place}`, char: symbols.group, ghost: ghostInt });
|
|
93
|
+
});
|
|
94
|
+
if (digits) {
|
|
95
|
+
parts.push({ key: "d", char: symbols.decimal, ghost: fraction === undefined });
|
|
96
|
+
for (let index = 0; index < digits; index++)
|
|
97
|
+
parts.push({ key: `f${index}`, char: fraction?.[index] ?? "0", ghost: fraction === undefined || index >= fraction.length });
|
|
98
|
+
}
|
|
99
|
+
return parts;
|
|
100
|
+
}
|
|
101
|
+
/* Columns open their width while the digit rises in the direction of change. Typing cuts straight to the result so the caret is never behind. */
|
|
102
|
+
const column = {
|
|
103
|
+
enter: { width: 0, opacity: 0 },
|
|
104
|
+
center: { width: "auto", opacity: 1, transition: { width: spring.morph, opacity: { duration: duration.fast } } },
|
|
105
|
+
exit: { width: 0, opacity: 0, transition: { width: spring.smooth, opacity: { duration: duration.instant } } },
|
|
106
|
+
};
|
|
107
|
+
const columnCut = { enter: { width: "auto", opacity: 1 }, center: { width: "auto", opacity: 1, transition: { duration: 0 } }, exit: { width: 0, opacity: 0, transition: { duration: 0 } } };
|
|
108
|
+
const glyph = {
|
|
109
|
+
enter: (dir) => ({ y: `${dir * .55}em`, opacity: 0, filter: `blur(${blur.soft}px)` }),
|
|
110
|
+
center: { y: 0, opacity: 1, filter: "blur(0px)", transitionEnd: { filter: "none" }, transition: { y: spring.snappy, opacity: { duration: duration.fast, ease: enterEase }, filter: { duration: duration.exit, ease: enterEase } } },
|
|
111
|
+
exit: (dir) => ({ y: `${dir * -.55}em`, opacity: 0, filter: `blur(${blur.subtle}px)`, transition: { duration: duration.instant, ease: standardEase } }),
|
|
112
|
+
};
|
|
113
|
+
const glyphCut = { enter: { y: 0, opacity: 1, filter: "none" }, center: { y: 0, opacity: 1, filter: "none", transition: { duration: 0 } }, exit: { opacity: 0, transition: { duration: 0 } } };
|
|
114
|
+
const glyphFade = { enter: { y: 0, opacity: 0, filter: "none" }, center: { y: 0, opacity: 1, filter: "none", transition: { duration: duration.instant } }, exit: { opacity: 0, transition: { duration: 0 } } };
|
|
115
|
+
function Glyphs({ parts, direction, instant, reduced }) {
|
|
116
|
+
const columnVariants = instant || reduced ? columnCut : column;
|
|
117
|
+
const glyphVariants = instant ? glyphCut : reduced ? glyphFade : glyph;
|
|
118
|
+
return _jsx(AnimatePresence, { initial: false, custom: direction, children: parts.map(part => _jsxs(motion.span, { className: styles.column, "data-ghost": part.ghost || undefined, custom: direction, variants: columnVariants, initial: "enter", animate: "center", exit: "exit", children: [_jsx("span", { className: styles.sizer, children: part.char }), _jsx(AnimatePresence, { initial: false, custom: direction, children: _jsx(motion.span, { className: styles.glyph, custom: direction, variants: glyphVariants, initial: "enter", animate: "center", exit: "exit", children: part.char }, part.char) })] }, part.key)) });
|
|
119
|
+
}
|
|
120
|
+
/** The currency symbol swaps with a short roll while its slot springs to the new width. */
|
|
121
|
+
function SymbolSlot({ symbol, reduced }) {
|
|
122
|
+
const sizerRef = useRef(null);
|
|
123
|
+
const width = useMotionValue("auto");
|
|
124
|
+
const measured = useRef(false);
|
|
125
|
+
useLayoutEffect(() => {
|
|
126
|
+
const node = sizerRef.current;
|
|
127
|
+
if (!node || typeof ResizeObserver === "undefined")
|
|
128
|
+
return;
|
|
129
|
+
const observer = new ResizeObserver(() => {
|
|
130
|
+
if (!measured.current || reduced)
|
|
131
|
+
width.jump(node.offsetWidth);
|
|
132
|
+
else
|
|
133
|
+
animate(width, node.offsetWidth, spring.morph);
|
|
134
|
+
measured.current = true;
|
|
135
|
+
});
|
|
136
|
+
observer.observe(node);
|
|
137
|
+
return () => observer.disconnect();
|
|
138
|
+
}, [reduced, width]);
|
|
139
|
+
return _jsxs(motion.span, { className: styles.symbol, style: { width }, "aria-hidden": "true", children: [_jsx("span", { ref: sizerRef, className: styles.symbolSizer, children: symbol }), _jsx(AnimatePresence, { initial: false, mode: "popLayout", children: _jsx(motion.span, { className: styles.symbolGlyph, initial: reduced ? { opacity: 0 } : { opacity: 0, y: ".4em", filter: `blur(${blur.soft}px)` }, animate: { opacity: 1, y: 0, filter: "blur(0px)" }, exit: { opacity: 0, y: reduced ? 0 : "-.3em", transition: { duration: duration.instant } }, transition: reduced ? { duration: duration.instant } : { y: spring.snappy, opacity: { duration: duration.fast }, filter: { duration: duration.exit } }, children: symbol }, symbol) })] });
|
|
140
|
+
}
|
|
141
|
+
const isSignificant = (char, decimal) => /\d/.test(char) || char === decimal;
|
|
142
|
+
function positionAfter(text, count, decimal) {
|
|
143
|
+
if (count <= 0)
|
|
144
|
+
return 0;
|
|
145
|
+
let seen = 0;
|
|
146
|
+
for (let index = 0; index < text.length; index++)
|
|
147
|
+
if (isSignificant(text[index], decimal) && ++seen === count)
|
|
148
|
+
return index + 1;
|
|
149
|
+
return text.length;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* A currency amount field. Digits group as they are typed without moving the caret, the missing cents wait as faint ghosts
|
|
153
|
+
* so the amount never changes width when it settles, long amounts scale down to fit instead of scrolling, and quick add chips
|
|
154
|
+
* and arrow keys roll the digits. The value comes out in minor units, so $12.50 is 1250.
|
|
155
|
+
*/
|
|
156
|
+
export const MoneyInput = forwardRef(function MoneyInput({ label, hideLabel = false, value, defaultValue = null, onValueChange, currency: currencyProp, defaultCurrency = "USD", onCurrencyChange, currencies = ["USD", "EUR", "GBP", "JPY", "CAD", "AUD", "CHF", "INR"], min, max, quickAdd = [10, 50, 100], step: stepProp, locale = "en-US", description, error, disabled = false, name, id, className, onBlur, }, forwardedRef) {
|
|
157
|
+
const reduced = useReducedFlag();
|
|
158
|
+
const uid = useId();
|
|
159
|
+
const inputId = id ?? `${uid}-amount`;
|
|
160
|
+
const hintId = `${inputId}-hint`, errorId = `${inputId}-error`, rangeId = `${inputId}-range`;
|
|
161
|
+
const [innerCurrency, setInnerCurrency] = useState(defaultCurrency);
|
|
162
|
+
const code = currencyProp ?? innerCurrency;
|
|
163
|
+
const info = useMemo(() => currencyInfo(code, locale), [code, locale]);
|
|
164
|
+
const symbols = useMemo(() => localeSymbols(locale), [locale]);
|
|
165
|
+
const step = stepProp && stepProp > 0 ? stepProp : pow10(info.digits);
|
|
166
|
+
const [draft, setDraft] = useState(() => fromMinor(value !== undefined ? value : defaultValue, info.digits));
|
|
167
|
+
const minor = toMinor(draft, info.digits);
|
|
168
|
+
const [seenValue, setSeenValue] = useState(value);
|
|
169
|
+
const [source, setSource] = useState("external");
|
|
170
|
+
const [direction, setDirection] = useState(1);
|
|
171
|
+
if (value !== seenValue) {
|
|
172
|
+
setSeenValue(value);
|
|
173
|
+
if (value !== undefined && value !== minor) {
|
|
174
|
+
setDraft(fromMinor(value, info.digits));
|
|
175
|
+
setSource("external");
|
|
176
|
+
setDirection((value ?? 0) >= (minor ?? 0) ? 1 : -1);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const [focused, setFocused] = useState(false);
|
|
180
|
+
const [touched, setTouched] = useState(false);
|
|
181
|
+
const [limitFlash, setLimitFlash] = useState(0);
|
|
182
|
+
const [announcement, setAnnouncement] = useState("");
|
|
183
|
+
const [, rerender] = useReducer((count) => count + 1, 0);
|
|
184
|
+
const inputRef = useRef(null);
|
|
185
|
+
useImperativeHandle(forwardedRef, () => inputRef.current);
|
|
186
|
+
const pendingCaret = useRef(null);
|
|
187
|
+
const latest = useRef({ minor, draft });
|
|
188
|
+
useLayoutEffect(() => { latest.current = { minor, draft }; });
|
|
189
|
+
const money = useCallback((amount) => amount === null ? "" : info.full.format(amount / pow10(info.digits)), [info]);
|
|
190
|
+
const display = displayOf(draft, symbols);
|
|
191
|
+
const parts = useMemo(() => partsOf(draft, info.digits, symbols), [draft, info.digits, symbols]);
|
|
192
|
+
/* ---------------------------------------------- Limits and feedback ---------------------------------------------- */
|
|
193
|
+
const bumpX = useMotionValue(0);
|
|
194
|
+
const flashTimer = useRef(undefined);
|
|
195
|
+
useEffect(() => () => window.clearTimeout(flashTimer.current), []);
|
|
196
|
+
function refuse(toward, limit) {
|
|
197
|
+
if (!reduced)
|
|
198
|
+
animate(bumpX, 0, { ...KICK, velocity: toward * 160 });
|
|
199
|
+
setLimitFlash(count => count + 1);
|
|
200
|
+
window.clearTimeout(flashTimer.current);
|
|
201
|
+
flashTimer.current = window.setTimeout(() => setLimitFlash(0), 1400);
|
|
202
|
+
setAnnouncement(`${toward > 0 ? "Maximum" : "Minimum"} is ${money(limit)}`);
|
|
203
|
+
}
|
|
204
|
+
function emit(nextMinor, nextInfo = info) {
|
|
205
|
+
onValueChange?.(nextMinor, { currency: nextInfo.code, major: nextMinor === null ? null : nextMinor / pow10(nextInfo.digits), formatted: nextMinor === null ? "" : nextInfo.full.format(nextMinor / pow10(nextInfo.digits)) });
|
|
206
|
+
}
|
|
207
|
+
function setAmount(nextDraft, nextSource) {
|
|
208
|
+
const nextMinor = toMinor(nextDraft, info.digits);
|
|
209
|
+
setDirection((nextMinor ?? 0) >= (latest.current.minor ?? 0) ? 1 : -1);
|
|
210
|
+
setSource(nextSource);
|
|
211
|
+
setDraft(nextDraft);
|
|
212
|
+
latest.current = { minor: nextMinor, draft: nextDraft };
|
|
213
|
+
if (nextMinor !== minor)
|
|
214
|
+
emit(nextMinor);
|
|
215
|
+
if (nextSource !== "type")
|
|
216
|
+
setAnnouncement(money(nextMinor));
|
|
217
|
+
}
|
|
218
|
+
/* ---------------------------------------------- Typing ---------------------------------------------- */
|
|
219
|
+
/** Reads what the input holds into a draft and counts the digits and decimal before the caret, so the caret can return to the same spot. */
|
|
220
|
+
function sanitize(raw, caret) {
|
|
221
|
+
let out = "", before = 0, dot = false;
|
|
222
|
+
for (let index = 0; index < raw.length; index++) {
|
|
223
|
+
const char = raw[index];
|
|
224
|
+
let kept = "";
|
|
225
|
+
if (/\d/.test(char))
|
|
226
|
+
kept = char;
|
|
227
|
+
else if ((char === symbols.decimal || (char === "." && symbols.group !== ".")) && info.digits > 0 && !dot) {
|
|
228
|
+
kept = ".";
|
|
229
|
+
dot = true;
|
|
230
|
+
}
|
|
231
|
+
if (!kept)
|
|
232
|
+
continue;
|
|
233
|
+
out += kept;
|
|
234
|
+
if (index < caret)
|
|
235
|
+
before++;
|
|
236
|
+
}
|
|
237
|
+
const pieces = out.split(".");
|
|
238
|
+
let whole = pieces[0];
|
|
239
|
+
const fraction = pieces[1];
|
|
240
|
+
const stripped = whole.length - (whole.replace(/^0+/, "").length);
|
|
241
|
+
whole = whole.replace(/^0+/, "");
|
|
242
|
+
let shift = -Math.min(stripped, before);
|
|
243
|
+
if (fraction !== undefined && !whole) {
|
|
244
|
+
whole = "0";
|
|
245
|
+
shift += 1;
|
|
246
|
+
}
|
|
247
|
+
if (fraction === undefined && !whole && stripped) {
|
|
248
|
+
whole = "0";
|
|
249
|
+
shift += 1;
|
|
250
|
+
}
|
|
251
|
+
if (fraction !== undefined && fraction.length > info.digits)
|
|
252
|
+
return null;
|
|
253
|
+
if (whole.length > MAX_INTEGER_DIGITS)
|
|
254
|
+
return null;
|
|
255
|
+
return { draft: fraction !== undefined ? `${whole}.${fraction}` : whole, before: Math.max(0, before + shift) };
|
|
256
|
+
}
|
|
257
|
+
function applyRaw(raw, caret) {
|
|
258
|
+
const result = sanitize(raw, caret);
|
|
259
|
+
// A refused keystroke keeps the caret where it was: one significant character before where the browser left it.
|
|
260
|
+
if (!result) {
|
|
261
|
+
pendingCaret.current = Math.max(0, [...raw.slice(0, caret)].filter(char => isSignificant(char, symbols.decimal) || char === ".").length - 1);
|
|
262
|
+
rerender();
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
const nextMinor = toMinor(result.draft, info.digits);
|
|
266
|
+
if (max !== undefined && nextMinor !== null && nextMinor > max) {
|
|
267
|
+
pendingCaret.current = Math.max(0, result.before - 1);
|
|
268
|
+
refuse(1, max);
|
|
269
|
+
rerender();
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
pendingCaret.current = result.before;
|
|
273
|
+
setAmount(result.draft, "type");
|
|
274
|
+
}
|
|
275
|
+
useLayoutEffect(() => {
|
|
276
|
+
const input = inputRef.current, count = pendingCaret.current;
|
|
277
|
+
pendingCaret.current = null;
|
|
278
|
+
if (!input || count === null || document.activeElement !== input)
|
|
279
|
+
return;
|
|
280
|
+
const position = positionAfter(input.value, count, symbols.decimal);
|
|
281
|
+
input.setSelectionRange(position, position);
|
|
282
|
+
});
|
|
283
|
+
function onKeyDown(event) {
|
|
284
|
+
const input = event.currentTarget;
|
|
285
|
+
const moves = { ArrowUp: 1, ArrowDown: -1, PageUp: 10, PageDown: -10 };
|
|
286
|
+
if (event.key in moves) {
|
|
287
|
+
event.preventDefault();
|
|
288
|
+
const amount = moves[event.key] * step * (event.shiftKey && Math.abs(moves[event.key]) === 1 ? 10 : 1);
|
|
289
|
+
nudge(amount, "key");
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (event.key === "Enter") {
|
|
293
|
+
setDraft(settle(draft, info.digits));
|
|
294
|
+
pendingCaret.current = Number.MAX_SAFE_INTEGER;
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
const start = input.selectionStart ?? 0, end = input.selectionEnd ?? 0;
|
|
298
|
+
if (start !== end || event.metaKey || event.ctrlKey || event.altKey)
|
|
299
|
+
return;
|
|
300
|
+
// Deleting a group separator deletes the digit before it instead of doing nothing.
|
|
301
|
+
if (event.key === "Backspace" && start > 1 && input.value[start - 1] === symbols.group) {
|
|
302
|
+
event.preventDefault();
|
|
303
|
+
applyRaw(input.value.slice(0, start - 2) + input.value.slice(start - 1), start - 2);
|
|
304
|
+
}
|
|
305
|
+
if (event.key === "Delete" && input.value[start] === symbols.group) {
|
|
306
|
+
event.preventDefault();
|
|
307
|
+
applyRaw(input.value.slice(0, start + 1) + input.value.slice(start + 2), start);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
function onPaste(event) {
|
|
311
|
+
const text = event.clipboardData.getData("text").trim();
|
|
312
|
+
if (!text)
|
|
313
|
+
return;
|
|
314
|
+
event.preventDefault();
|
|
315
|
+
// "$1,234.56", "1.234,56 €", "1234.5": a final separator followed by one or two digits is the decimal; the rest are groups.
|
|
316
|
+
const match = /[.,](\d{1,2})\s*\D*$/.exec(text);
|
|
317
|
+
const whole = (match ? text.slice(0, match.index) : text).replace(/\D/g, "");
|
|
318
|
+
const pasted = match && info.digits ? `${whole}.${match[1]}` : whole;
|
|
319
|
+
if (!/\d/.test(pasted))
|
|
320
|
+
return;
|
|
321
|
+
const input = event.currentTarget;
|
|
322
|
+
const start = input.selectionStart ?? 0, end = input.selectionEnd ?? 0;
|
|
323
|
+
const inserted = pasted.replace(".", symbols.decimal);
|
|
324
|
+
const replaceAll = start === 0 && end === input.value.length || !input.value;
|
|
325
|
+
const raw = replaceAll ? inserted : input.value.slice(0, start) + inserted + input.value.slice(end);
|
|
326
|
+
applyRaw(raw, replaceAll ? raw.length : start + inserted.length);
|
|
327
|
+
}
|
|
328
|
+
/* ---------------------------------------------- Steps and chips ---------------------------------------------- */
|
|
329
|
+
function nudge(amount, nextSource) {
|
|
330
|
+
const from = latest.current.minor ?? 0;
|
|
331
|
+
const floor = min ?? 0, ceiling = max ?? Number.MAX_SAFE_INTEGER;
|
|
332
|
+
let next = from + amount;
|
|
333
|
+
if (next > ceiling) {
|
|
334
|
+
if (from >= ceiling) {
|
|
335
|
+
refuse(1, ceiling);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
next = ceiling;
|
|
339
|
+
refuse(1, ceiling);
|
|
340
|
+
}
|
|
341
|
+
if (next < floor) {
|
|
342
|
+
if (from <= floor && latest.current.minor !== null) {
|
|
343
|
+
refuse(-1, floor);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
next = floor;
|
|
347
|
+
}
|
|
348
|
+
pendingCaret.current = Number.MAX_SAFE_INTEGER;
|
|
349
|
+
setAmount(fromMinor(next, info.digits), nextSource);
|
|
350
|
+
}
|
|
351
|
+
/* ---------------------------------------------- Fit to width ---------------------------------------------- */
|
|
352
|
+
const wrapRef = useRef(null);
|
|
353
|
+
const groupRef = useRef(null);
|
|
354
|
+
const scale = useMotionValue(1);
|
|
355
|
+
useLayoutEffect(() => {
|
|
356
|
+
const wrap = wrapRef.current, group = groupRef.current;
|
|
357
|
+
if (!wrap || !group || typeof ResizeObserver === "undefined")
|
|
358
|
+
return;
|
|
359
|
+
let first = true;
|
|
360
|
+
const fit = () => {
|
|
361
|
+
const natural = group.offsetWidth, room = wrap.clientWidth;
|
|
362
|
+
const next = natural > 0 && room > 0 ? Math.min(1, room / natural) : 1;
|
|
363
|
+
if (first || reduced)
|
|
364
|
+
scale.jump(next);
|
|
365
|
+
else
|
|
366
|
+
animate(scale, next, spring.smooth);
|
|
367
|
+
first = false;
|
|
368
|
+
};
|
|
369
|
+
const observer = new ResizeObserver(fit);
|
|
370
|
+
observer.observe(wrap);
|
|
371
|
+
observer.observe(group);
|
|
372
|
+
return () => observer.disconnect();
|
|
373
|
+
}, [reduced, scale]);
|
|
374
|
+
/* ---------------------------------------------- Currency menu ---------------------------------------------- */
|
|
375
|
+
const [menuOpen, setMenuOpen] = useState(false);
|
|
376
|
+
const [activeCode, setActiveCode] = useState(code);
|
|
377
|
+
const menuRef = useRef(null);
|
|
378
|
+
const listRef = useRef(null);
|
|
379
|
+
const menuButtonRef = useRef(null);
|
|
380
|
+
const optionRefs = useRef(new Map());
|
|
381
|
+
const options = useMemo(() => currencies.map(entry => currencyInfo(entry, locale)), [currencies, locale]);
|
|
382
|
+
const canSwitch = options.length > 1;
|
|
383
|
+
const openMenu = () => { if (disabled)
|
|
384
|
+
return; setActiveCode(code); setMenuOpen(true); };
|
|
385
|
+
const closeMenu = useCallback((focusButton) => { setMenuOpen(false); if (focusButton)
|
|
386
|
+
menuButtonRef.current?.focus({ preventScroll: true }); }, []);
|
|
387
|
+
useLayoutEffect(() => { if (menuOpen)
|
|
388
|
+
listRef.current?.focus({ preventScroll: true }); }, [menuOpen]);
|
|
389
|
+
useEffect(() => {
|
|
390
|
+
if (!menuOpen)
|
|
391
|
+
return;
|
|
392
|
+
const down = (event) => { if (!menuRef.current?.contains(event.target))
|
|
393
|
+
closeMenu(false); };
|
|
394
|
+
document.addEventListener("pointerdown", down);
|
|
395
|
+
return () => document.removeEventListener("pointerdown", down);
|
|
396
|
+
}, [closeMenu, menuOpen]);
|
|
397
|
+
const hy = useMotionValue(0), hh = useMotionValue(0), ho = useMotionValue(0);
|
|
398
|
+
useLayoutEffect(() => {
|
|
399
|
+
const node = menuOpen ? optionRefs.current.get(activeCode) : undefined;
|
|
400
|
+
if (!node) {
|
|
401
|
+
ho.jump(0);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
if (ho.get() < .05 || reduced) {
|
|
405
|
+
hy.jump(node.offsetTop);
|
|
406
|
+
hh.jump(node.offsetHeight);
|
|
407
|
+
}
|
|
408
|
+
else {
|
|
409
|
+
animate(hy, node.offsetTop, GLIDE);
|
|
410
|
+
animate(hh, node.offsetHeight, GLIDE);
|
|
411
|
+
}
|
|
412
|
+
ho.jump(1);
|
|
413
|
+
node.scrollIntoView?.({ block: "nearest" });
|
|
414
|
+
}, [activeCode, hh, ho, hy, menuOpen, reduced]);
|
|
415
|
+
function chooseCurrency(next) {
|
|
416
|
+
closeMenu(true);
|
|
417
|
+
if (next.code === code)
|
|
418
|
+
return;
|
|
419
|
+
const nextDraft = focused ? convertDraft(draft, next.digits) : settle(convertDraft(draft, next.digits), next.digits);
|
|
420
|
+
const nextMinor = toMinor(nextDraft, next.digits);
|
|
421
|
+
if (currencyProp === undefined)
|
|
422
|
+
setInnerCurrency(next.code);
|
|
423
|
+
onCurrencyChange?.(next.code);
|
|
424
|
+
setSource("external");
|
|
425
|
+
setDraft(nextDraft);
|
|
426
|
+
latest.current = { minor: nextMinor, draft: nextDraft };
|
|
427
|
+
emit(nextMinor, next);
|
|
428
|
+
setAnnouncement(`${next.name}${nextMinor === null ? "" : `, ${next.full.format(nextMinor / pow10(next.digits))}`}`);
|
|
429
|
+
}
|
|
430
|
+
const typeahead = useRef({ buffer: "", at: 0 });
|
|
431
|
+
function onMenuKeyDown(event) {
|
|
432
|
+
const at = options.findIndex(option => option.code === activeCode);
|
|
433
|
+
const go = (index) => { event.preventDefault(); setActiveCode(options[Math.max(0, Math.min(options.length - 1, index))].code); };
|
|
434
|
+
switch (event.key) {
|
|
435
|
+
case "ArrowDown":
|
|
436
|
+
go(at + 1);
|
|
437
|
+
return;
|
|
438
|
+
case "ArrowUp":
|
|
439
|
+
go(at - 1);
|
|
440
|
+
return;
|
|
441
|
+
case "Home":
|
|
442
|
+
go(0);
|
|
443
|
+
return;
|
|
444
|
+
case "End":
|
|
445
|
+
go(options.length - 1);
|
|
446
|
+
return;
|
|
447
|
+
case "Enter":
|
|
448
|
+
case " ":
|
|
449
|
+
event.preventDefault();
|
|
450
|
+
if (options[at])
|
|
451
|
+
chooseCurrency(options[at]);
|
|
452
|
+
return;
|
|
453
|
+
case "Escape":
|
|
454
|
+
event.preventDefault();
|
|
455
|
+
event.stopPropagation();
|
|
456
|
+
closeMenu(true);
|
|
457
|
+
return;
|
|
458
|
+
case "Tab":
|
|
459
|
+
closeMenu(false);
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
if (event.key.length === 1 && /\w/.test(event.key)) {
|
|
463
|
+
const now = performance.now(), state = typeahead.current;
|
|
464
|
+
state.buffer = now - state.at > 600 ? event.key.toLowerCase() : state.buffer + event.key.toLowerCase();
|
|
465
|
+
state.at = now;
|
|
466
|
+
const hit = options.find(option => option.code.toLowerCase().startsWith(state.buffer) || option.name.toLowerCase().startsWith(state.buffer));
|
|
467
|
+
if (hit) {
|
|
468
|
+
event.preventDefault();
|
|
469
|
+
setActiveCode(hit.code);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
/* ---------------------------------------------- Messages ---------------------------------------------- */
|
|
474
|
+
const range = min !== undefined && max !== undefined ? `${money(min)} to ${money(max)}` : max !== undefined ? `Up to ${money(max)}` : min !== undefined ? `At least ${money(min)}` : "";
|
|
475
|
+
const outOfRange = minor !== null && ((min !== undefined && minor < min) || (max !== undefined && minor > max));
|
|
476
|
+
const belowMin = minor !== null && min !== undefined && minor < min;
|
|
477
|
+
const builtIn = touched && !focused && outOfRange ? belowMin ? `The minimum is ${money(min ?? 0)}` : `The maximum is ${money(max ?? 0)}` : undefined;
|
|
478
|
+
const message = error ?? builtIn;
|
|
479
|
+
const invalid = !!message;
|
|
480
|
+
const describedBy = [range ? rangeId : null, description ? hintId : null, message ? errorId : null].filter(Boolean).join(" ") || undefined;
|
|
481
|
+
const instant = source === "type";
|
|
482
|
+
const chips = quickAdd.filter(amount => amount > 0);
|
|
483
|
+
return _jsxs("div", { className: [styles.field, className].filter(Boolean).join(" "), "data-disabled": disabled || undefined, children: [_jsx("label", { htmlFor: inputId, className: hideLabel ? styles.srOnly : styles.label, children: label }), _jsxs("div", { className: styles.control, "data-invalid": invalid || undefined, "data-focused": focused || undefined, children: [_jsx("div", { ref: wrapRef, className: styles.amountWrap, onMouseDown: event => { if (event.target !== inputRef.current)
|
|
484
|
+
event.preventDefault(); }, onClick: event => { if (!disabled && event.target !== inputRef.current) {
|
|
485
|
+
const input = inputRef.current;
|
|
486
|
+
input?.focus();
|
|
487
|
+
input?.setSelectionRange(input.value.length, input.value.length);
|
|
488
|
+
} }, children: _jsxs(motion.div, { ref: groupRef, className: styles.group, style: { scale, x: bumpX }, children: [info.position === "prefix" && _jsx(SymbolSlot, { symbol: info.symbol, reduced: reduced }), _jsxs("span", { className: styles.number, children: [_jsx("span", { className: styles.mirror, "aria-hidden": "true", "data-empty": !draft || undefined, children: _jsx(Glyphs, { parts: parts, direction: direction, instant: instant, reduced: reduced }) }), _jsx("input", { ref: inputRef, id: inputId, className: styles.input, type: "text", role: "spinbutton", inputMode: info.digits ? "decimal" : "numeric", autoComplete: "off", spellCheck: false, value: display, disabled: disabled, "aria-invalid": invalid || undefined, "aria-describedby": describedBy, "aria-label": hideLabel ? label : undefined, "aria-valuenow": minor === null ? undefined : minor / pow10(info.digits), "aria-valuetext": minor === null ? "Empty" : money(minor), "aria-valuemin": min === undefined ? undefined : min / pow10(info.digits), "aria-valuemax": max === undefined ? undefined : max / pow10(info.digits), onChange: event => applyRaw(event.currentTarget.value, event.currentTarget.selectionStart ?? event.currentTarget.value.length), onKeyDown: onKeyDown, onPaste: onPaste, onFocus: () => setFocused(true), onBlur: event => { setFocused(false); setTouched(true); if (draft)
|
|
489
|
+
setDraft(settle(draft, info.digits)); onBlur?.(event); } })] }), info.position === "suffix" && _jsx(SymbolSlot, { symbol: info.symbol, reduced: reduced })] }) }), canSwitch ? _jsxs("div", { ref: menuRef, className: styles.menuAnchor, children: [_jsxs("button", { ref: menuButtonRef, type: "button", className: styles.currencyButton, disabled: disabled, "aria-haspopup": "listbox", "aria-expanded": menuOpen, "aria-controls": `${uid}-currencies`, "aria-label": `Currency, ${info.name}`, "data-open": menuOpen || undefined, onClick: () => (menuOpen ? closeMenu(true) : openMenu()), onKeyDown: event => { if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
490
|
+
event.preventDefault();
|
|
491
|
+
openMenu();
|
|
492
|
+
} }, children: [_jsx("span", { className: styles.code, children: code }), _jsx(motion.span, { className: styles.chevron, "aria-hidden": "true", initial: false, animate: { rotate: menuOpen ? 180 : 0 }, transition: reduced ? { duration: 0 } : GLIDE, children: _jsx(ChevronDown, { size: 16, strokeWidth: 1.75 }) })] }), _jsx(AnimatePresence, { children: menuOpen && _jsx(motion.div, { className: styles.menu, initial: reduced ? { opacity: 0 } : { opacity: 0, scale: .96, y: -4, filter: `blur(${blur.subtle}px)` }, animate: { opacity: 1, scale: 1, y: 0, filter: "blur(0px)" }, exit: reduced ? { opacity: 0, transition: { duration: duration.instant } } : { opacity: 0, scale: .97, y: -2, filter: `blur(${blur.subtle}px)`, transition: { duration: duration.instant, ease: standardEase } }, transition: reduced ? { duration: duration.instant } : { ...spring.snappy, opacity: { duration: duration.fast, ease: enterEase }, filter: { duration: duration.fast, ease: enterEase } }, children: _jsxs("div", { ref: listRef, id: `${uid}-currencies`, className: styles.list, role: "listbox", tabIndex: -1, "aria-label": "Currency", "aria-activedescendant": `${uid}-cur-${activeCode}`, onKeyDown: onMenuKeyDown, children: [_jsx(motion.span, { className: styles.highlight, style: { y: hy, height: hh, opacity: ho }, "aria-hidden": "true" }), options.map(option => _jsxs("div", { id: `${uid}-cur-${option.code}`, role: "option", "aria-selected": option.code === code, className: styles.option, "data-active": option.code === activeCode || undefined, ref: node => { if (node)
|
|
493
|
+
optionRefs.current.set(option.code, node);
|
|
494
|
+
else
|
|
495
|
+
optionRefs.current.delete(option.code); }, onPointerMove: event => { if (event.pointerType === "mouse" && option.code !== activeCode)
|
|
496
|
+
setActiveCode(option.code); }, onPointerDown: event => event.preventDefault(), onClick: () => chooseCurrency(option), children: [_jsx("span", { className: styles.optionSymbol, "aria-hidden": "true", children: option.symbol }), _jsx("span", { className: styles.optionCode, children: option.code }), _jsx("span", { className: styles.optionName, children: option.name }), _jsx("span", { className: styles.check, "data-on": option.code === code || undefined, "aria-hidden": "true", children: _jsx(Check, { size: 16, strokeWidth: 1.75 }) })] }, option.code))] }) }, "menu") })] }) : _jsx("span", { className: styles.codeStatic, children: code })] }), (chips.length > 0 || range) && _jsxs("div", { className: styles.toolbar, children: [chips.length > 0 && _jsx("div", { className: styles.chips, role: "group", "aria-label": "Quick add", children: chips.map(amount => {
|
|
497
|
+
const add = amount * pow10(info.digits);
|
|
498
|
+
const atMax = max !== undefined && (minor ?? 0) >= max;
|
|
499
|
+
return _jsxs("button", { type: "button", className: styles.chip, disabled: disabled, "aria-disabled": atMax || undefined, "aria-controls": inputId, "aria-label": `Add ${info.whole.format(amount)}`, onClick: () => nudge(add, "chip"), children: ["+", info.whole.format(amount)] }, amount);
|
|
500
|
+
}) }), range && _jsx("span", { id: rangeId, className: styles.range, "data-flash": limitFlash > 0 || undefined, children: range })] }), _jsx(AnimatePresence, { initial: false, children: (message || description) && _jsx(motion.span, { className: styles.messageSlot, initial: reduced ? false : { height: 0, opacity: 0 }, animate: { height: "auto", opacity: 1 }, exit: { height: 0, opacity: 0, transition: reduced ? { duration: 0 } : { height: spring.smooth, opacity: { duration: duration.instant } } }, transition: reduced ? { duration: 0 } : { height: spring.smooth, opacity: { duration: duration.fast } }, children: _jsx(AnimatePresence, { initial: false, mode: "popLayout", children: _jsx(motion.span, { id: message ? errorId : hintId, className: message ? styles.error : styles.hint, role: message ? "alert" : undefined, initial: reduced ? { opacity: 0 } : { opacity: 0, y: "0.35em", filter: `blur(${blur.soft}px)` }, animate: { opacity: 1, y: 0, filter: "blur(0px)" }, exit: { opacity: 0, transition: { duration: duration.instant } }, transition: { duration: reduced ? duration.instant : duration.standard, ease: enterEase }, children: message ?? description }, message ?? description) }) }, "message") }), name && _jsx("input", { type: "hidden", name: name, value: minor ?? "" }), _jsx("span", { className: styles.srOnly, role: "status", "aria-live": "polite", children: announcement })] });
|
|
501
|
+
});
|
|
502
|
+
MoneyInput.displayName = "MoneyInput";
|
|
503
|
+
export default MoneyInput;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/* No row gap: grid tracks clamp a negative margin at zero, so a closed message row would still pay the gap. Each row carries its own space instead. */
|
|
2
|
+
.field { display: grid; min-width: 0; font-family: var(--font-body); letter-spacing: var(--tracking-body); }
|
|
3
|
+
.label { justify-self: start; margin-bottom: var(--space-related); color: var(--foreground); font-size: var(--text-sm); font-weight: 500; line-height: var(--leading-body); }
|
|
4
|
+
|
|
5
|
+
/* One tall shell: the amount on the left, the currency on the right. Set --money-input-height to resize it. */
|
|
6
|
+
.control { position: relative; display: flex; min-width: 0; height: var(--money-input-height, 76px); box-sizing: border-box; align-items: center; gap: var(--space-related); padding: 0 8px 0 18px; border: 1px solid var(--border-strong); border-radius: var(--radius-container); corner-shape: var(--corner-shape); background: var(--surface); transition: border-color var(--duration-fast) var(--ease-standard); }
|
|
7
|
+
@media (hover: hover) and (pointer: fine) { .control:hover:not([data-invalid]) { border-color: var(--foreground); } }
|
|
8
|
+
.control[data-focused] { border-color: var(--foreground); }
|
|
9
|
+
.control[data-invalid] { border-color: var(--danger); }
|
|
10
|
+
.field[data-disabled] .control { opacity: .5; background: var(--surface-muted); }
|
|
11
|
+
|
|
12
|
+
/* The amount scales down to fit rather than scroll, so a long number is always read whole. */
|
|
13
|
+
.amountWrap { position: relative; display: flex; min-width: 0; height: 100%; flex: 1; align-items: center; overflow: hidden; cursor: text; }
|
|
14
|
+
.field[data-disabled] .amountWrap { cursor: not-allowed; }
|
|
15
|
+
.group { display: inline-flex; flex: none; align-items: baseline; transform-origin: 0 50%; white-space: nowrap; color: var(--foreground); font-family: var(--font-display); font-size: var(--text-3xl); font-weight: 500; line-height: 1.2; letter-spacing: var(--tracking-display); font-variant-numeric: tabular-nums; font-kerning: none; }
|
|
16
|
+
|
|
17
|
+
.symbol { position: relative; display: inline-block; margin-right: .06em; color: var(--text-secondary); white-space: pre; }
|
|
18
|
+
.group > .symbol:last-child { margin-right: 0; margin-left: .18em; }
|
|
19
|
+
.symbolSizer { position: absolute; top: 0; left: 0; visibility: hidden; white-space: pre; }
|
|
20
|
+
.symbolGlyph { display: inline-block; white-space: pre; }
|
|
21
|
+
|
|
22
|
+
/* The input only draws the caret and selection. Its text is always painted by the mirror, so settled, rolling, and typed amounts share one position. */
|
|
23
|
+
.number { position: relative; display: inline-flex; }
|
|
24
|
+
.mirror { display: inline-flex; }
|
|
25
|
+
.input { position: absolute; z-index: 1; top: 0; left: 0; width: calc(100% + .5em); height: 100%; margin: 0; padding: 0; border: 0; background: transparent; color: transparent; caret-color: var(--foreground); font: inherit; font-variant-numeric: inherit; font-kerning: none; letter-spacing: inherit; line-height: inherit; outline: none; }
|
|
26
|
+
.input::selection { background: color-mix(in oklab, var(--foreground) 16%, transparent); color: transparent; }
|
|
27
|
+
.input:disabled { cursor: not-allowed; }
|
|
28
|
+
|
|
29
|
+
/* Each character is a column: a hidden sizer holds its width and the glyph rolls inside, feathered at the top and bottom edge. */
|
|
30
|
+
.column { --feather: .18em; position: relative; display: inline-block; overflow-x: visible; overflow-y: clip; margin-block: calc(var(--feather) * -1); padding-block: var(--feather); mask-image: linear-gradient(to bottom, transparent, #000 calc(var(--feather) * 1.4), #000 calc(100% - var(--feather) * 1.4), transparent); transition: color var(--duration-standard) var(--ease-standard); }
|
|
31
|
+
/* Missing cents wait as ghosts, so settling the amount only changes their color. */
|
|
32
|
+
.column[data-ghost] { color: color-mix(in oklab, var(--text-muted) 55%, transparent); }
|
|
33
|
+
.sizer { visibility: hidden; white-space: pre; }
|
|
34
|
+
.glyph { position: absolute; inset: var(--feather) 0; text-align: center; white-space: pre; }
|
|
35
|
+
|
|
36
|
+
/* Currency switch: plain text on the shell, a quiet fill on hover and while open. It answers a press with color, since it anchors a menu. */
|
|
37
|
+
.menuAnchor { position: relative; flex: none; }
|
|
38
|
+
.currencyButton { display: inline-flex; height: 36px; align-items: center; gap: 4px; padding: 0 8px 0 12px; border: 0; border-radius: var(--radius-full); corner-shape: round; background: transparent; color: var(--foreground); cursor: pointer; font: inherit; font-size: var(--text-sm); font-weight: 500; -webkit-tap-highlight-color: transparent; transition: background-color var(--duration-fast) var(--ease-standard); }
|
|
39
|
+
@media (hover: hover) and (pointer: fine) { .currencyButton:hover:not(:disabled) { background: var(--surface-muted); } }
|
|
40
|
+
.currencyButton:active:not(:disabled), .currencyButton[data-open] { background: var(--surface-muted); }
|
|
41
|
+
.currencyButton:disabled { cursor: not-allowed; }
|
|
42
|
+
.code { font-variant-numeric: tabular-nums; }
|
|
43
|
+
.codeStatic { flex: none; padding: 0 10px; color: var(--text-secondary); font-size: var(--text-sm); font-weight: 500; }
|
|
44
|
+
.chevron { display: grid; width: 16px; height: 16px; place-items: center; color: var(--text-muted); }
|
|
45
|
+
|
|
46
|
+
/* The menu grows out of the currency button's corner and opens 8px below the shell, whatever its height. */
|
|
47
|
+
.menu { --drop: calc((var(--money-input-height, 76px) - 36px) / 2 + var(--space-related)); position: absolute; z-index: 30; top: calc(100% + var(--drop)); right: -8px; width: min(256px, calc(100vw - 2rem)); box-sizing: border-box; padding: 6px; border: 1px solid var(--border); border-radius: var(--radius-container); corner-shape: var(--corner-shape); background: var(--surface-raised); box-shadow: var(--shadow-floating); transform-origin: calc(100% - 44px) calc(var(--drop) * -1 - 18px); }
|
|
48
|
+
.list { position: relative; display: grid; max-height: 288px; gap: 2px; overflow-y: auto; overscroll-behavior: contain; outline: none; scrollbar-width: thin; }
|
|
49
|
+
.highlight { position: absolute; top: 0; right: 0; left: 0; border-radius: var(--radius-control); corner-shape: var(--corner-shape); background: color-mix(in oklab, var(--foreground) 6.5%, transparent); pointer-events: none; }
|
|
50
|
+
.option { position: relative; display: flex; min-height: 36px; box-sizing: border-box; align-items: center; gap: 10px; padding: 0 10px; border-radius: var(--radius-control); corner-shape: var(--corner-shape); color: var(--text-secondary); cursor: pointer; font-size: var(--text-sm); user-select: none; -webkit-user-select: none; transition: color var(--duration-fast) var(--ease-standard); }
|
|
51
|
+
.option[data-active], .option[aria-selected="true"] { color: var(--foreground); }
|
|
52
|
+
.optionSymbol { width: 22px; flex: none; color: var(--text-muted); text-align: center; }
|
|
53
|
+
.optionCode { flex: none; font-weight: 500; font-variant-numeric: tabular-nums; }
|
|
54
|
+
.optionName { min-width: 0; flex: 1; overflow: hidden; color: var(--text-muted); font-size: var(--text-xs); text-overflow: ellipsis; white-space: nowrap; }
|
|
55
|
+
.check { display: grid; width: 16px; height: 16px; flex: none; place-items: center; color: var(--foreground); opacity: 0; transform: scale(.6); transition: opacity var(--duration-fast) var(--ease-standard), transform var(--duration-spring) var(--ease-spring); }
|
|
56
|
+
.check[data-on] { opacity: 1; transform: none; }
|
|
57
|
+
|
|
58
|
+
/* Quick add chips and the allowed range share one row under the shell. */
|
|
59
|
+
.toolbar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space-related) var(--space-group); padding-top: var(--space-group); }
|
|
60
|
+
.chips { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
61
|
+
.chip { height: 30px; padding: 0 12px; border: 1px solid var(--border); border-radius: var(--radius-full); corner-shape: round; background: var(--surface); color: var(--text-secondary); cursor: pointer; font: inherit; font-size: var(--text-xs); font-weight: 500; font-variant-numeric: tabular-nums; -webkit-tap-highlight-color: transparent; touch-action: manipulation; transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard), border-color var(--duration-fast) var(--ease-standard), transform var(--duration-spring) var(--ease-spring); }
|
|
62
|
+
@media (hover: hover) and (pointer: fine) { .chip:hover:not(:disabled) { background: var(--surface-muted); color: var(--foreground); } }
|
|
63
|
+
.chip:active:not(:disabled) { transform: scale(.96); transition-duration: var(--duration-fast), var(--duration-fast), var(--duration-fast), 100ms; }
|
|
64
|
+
.chip[aria-disabled="true"] { opacity: .45; }
|
|
65
|
+
.chip:disabled { opacity: .45; cursor: not-allowed; }
|
|
66
|
+
.range { color: var(--text-muted); font-size: var(--text-xs); font-variant-numeric: tabular-nums; transition: color var(--duration-standard) var(--ease-standard); }
|
|
67
|
+
.range[data-flash] { color: var(--warning); transition-duration: var(--duration-instant); }
|
|
68
|
+
|
|
69
|
+
.messageSlot { position: relative; display: block; overflow: hidden; }
|
|
70
|
+
.hint, .error { display: block; padding-top: var(--space-related); font-size: var(--text-xs); line-height: var(--leading-body); }
|
|
71
|
+
.hint { color: var(--text-muted); }
|
|
72
|
+
.error { color: var(--danger); }
|
|
73
|
+
|
|
74
|
+
.srOnly { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; }
|
|
75
|
+
|
|
76
|
+
@media (max-width: 420px) { .control { padding-left: 14px; } .group { font-size: var(--text-2xl); } }
|
|
77
|
+
@media (prefers-reduced-motion: reduce) { .control, .column, .check, .chip, .range { transition-duration: 0ms; } .chip:active:not(:disabled) { transform: none; } }
|