@teamofsilicons/silicon-ui 0.0.0-stage → 0.2.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 +81 -2
- package/check.mjs +236 -0
- 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/eslint.js +53 -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 +76 -4
- package/styles.css +218 -0
- package/styles.css.d.ts +1 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export interface AnnouncementAction {
|
|
3
|
+
label: string;
|
|
4
|
+
href?: string;
|
|
5
|
+
onClick?: () => void;
|
|
6
|
+
}
|
|
7
|
+
export interface Announcement {
|
|
8
|
+
/** Stable id, reported to callbacks. */
|
|
9
|
+
id: string;
|
|
10
|
+
message: ReactNode;
|
|
11
|
+
action?: AnnouncementAction;
|
|
12
|
+
/** Counts down to a moment, for example the end of a sale. */
|
|
13
|
+
countdown?: {
|
|
14
|
+
to: Date | string | number;
|
|
15
|
+
label?: string;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A slim bar for the top of a page. It can rotate several messages, each rising in from below while the bar springs to
|
|
20
|
+
* the new height; rotation pauses on hover, focus, or a hidden tab, and `controls` adds previous, next, and a pause ring. Messages can carry a
|
|
21
|
+
* call to action and a live countdown. Dismissing collapses the height smoothly so the page below eases up, and with an
|
|
22
|
+
* `id` the dismissal is remembered in `localStorage`.
|
|
23
|
+
*/
|
|
24
|
+
export interface AnnouncementBarProps {
|
|
25
|
+
messages: Announcement[];
|
|
26
|
+
/** Remembers dismissal under this id. Change the id to show a new campaign to everyone again. */
|
|
27
|
+
id?: string;
|
|
28
|
+
/** Whether the bar is shown. Leave it out to let the component manage it. */
|
|
29
|
+
open?: boolean;
|
|
30
|
+
defaultOpen?: boolean;
|
|
31
|
+
onOpenChange?: (open: boolean) => void;
|
|
32
|
+
/** Index of the visible message. */
|
|
33
|
+
index?: number;
|
|
34
|
+
defaultIndex?: number;
|
|
35
|
+
onIndexChange?: (index: number) => void;
|
|
36
|
+
/** Milliseconds each message stays before the next one. Defaults to 6000. */
|
|
37
|
+
interval?: number;
|
|
38
|
+
/** Rotate automatically. Pauses on hover, focus, or a hidden tab, and is turned off when the visitor prefers reduced motion. Defaults to true. */
|
|
39
|
+
autoPlay?: boolean;
|
|
40
|
+
/** Show previous, next, and pause controls when there are several messages. Defaults to false: only the close button shows. */
|
|
41
|
+
controls?: boolean;
|
|
42
|
+
dismissible?: boolean;
|
|
43
|
+
tone?: "neutral" | "inverted";
|
|
44
|
+
onAction?: (announcement: Announcement) => void;
|
|
45
|
+
onCountdownEnd?: (announcement: Announcement) => void;
|
|
46
|
+
/** Accessible name of the region. */
|
|
47
|
+
label?: string;
|
|
48
|
+
className?: string;
|
|
49
|
+
}
|
|
50
|
+
/** Forgets a remembered dismissal so the bar with this id shows again. */
|
|
51
|
+
export declare function clearAnnouncementDismissal(id: string): void;
|
|
52
|
+
export declare const AnnouncementBar: import("react").ForwardRefExoticComponent<AnnouncementBarProps & import("react").RefAttributes<HTMLElement>>;
|
|
53
|
+
export default AnnouncementBar;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { forwardRef, useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from "react";
|
|
4
|
+
import { AnimatePresence, animate, motion, useIsPresent, useMotionValue, useReducedMotion, useTransform } from "motion/react";
|
|
5
|
+
import { ArrowRight, ChevronDown, ChevronUp, Pause, Play, X } from "lucide-react";
|
|
6
|
+
import { motionTokens } from "../../lib/motion-tokens.js";
|
|
7
|
+
import styles from "./announcement-bar.module.css";
|
|
8
|
+
const storageKey = (id) => `silicon-announcement:${id}`;
|
|
9
|
+
/** Forgets a remembered dismissal so the bar with this id shows again. */
|
|
10
|
+
export function clearAnnouncementDismissal(id) {
|
|
11
|
+
try {
|
|
12
|
+
window.localStorage.removeItem(storageKey(id));
|
|
13
|
+
}
|
|
14
|
+
catch { /* Storage can be blocked. */ }
|
|
15
|
+
}
|
|
16
|
+
const enter = [...motionTokens.ease.enter];
|
|
17
|
+
const standard = [...motionTokens.ease.standard];
|
|
18
|
+
const physical = (visualDuration, bounce) => {
|
|
19
|
+
const root = 2 * Math.PI / (visualDuration * 1.2);
|
|
20
|
+
return { type: "spring", stiffness: root * root, damping: 2 * (1 - bounce) * root, mass: 1 };
|
|
21
|
+
};
|
|
22
|
+
const RISE = physical(.46, .1), HEIGHT = physical(.42, 0), COLLAPSE = physical(.44, 0);
|
|
23
|
+
/** Next rises from below and leaves upward; previous runs the other way. */
|
|
24
|
+
const faceVariants = {
|
|
25
|
+
hidden: (direction) => ({ opacity: 0, y: `${direction * 70}%`, filter: `blur(${motionTokens.blur.soft}px)` }),
|
|
26
|
+
shown: { opacity: 1, y: "0%", filter: "blur(0px)", transition: { y: RISE, opacity: { duration: motionTokens.duration.standard, ease: enter }, filter: { duration: motionTokens.duration.standard, ease: enter } } },
|
|
27
|
+
gone: (direction) => ({ opacity: 0, y: `${direction * -60}%`, filter: `blur(${motionTokens.blur.soft}px)`, transition: { y: RISE, opacity: { duration: motionTokens.duration.fast, ease: standard }, filter: { duration: motionTokens.duration.fast, ease: standard } } }),
|
|
28
|
+
};
|
|
29
|
+
const fadeVariants = { hidden: { opacity: 0 }, shown: { opacity: 1, transition: { duration: motionTokens.duration.standard, ease: standard } }, gone: { opacity: 0, transition: { duration: motionTokens.duration.instant, ease: standard } } };
|
|
30
|
+
function parts(ms) {
|
|
31
|
+
const total = Math.max(0, Math.floor(ms / 1000));
|
|
32
|
+
return { d: Math.floor(total / 86400), h: Math.floor(total % 86400 / 3600), m: Math.floor(total % 3600 / 60), s: total % 60 };
|
|
33
|
+
}
|
|
34
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
35
|
+
/**
|
|
36
|
+
* One character cell. A changed digit drops in from above while the old one falls away, like a counter winding down.
|
|
37
|
+
* The invisible sizer keeps the cell in the text flow, so it shares the baseline of the words around it.
|
|
38
|
+
*/
|
|
39
|
+
function Digit({ char, reduced }) {
|
|
40
|
+
return _jsxs("span", { className: styles.digit, children: [_jsx("span", { className: styles.digitSizer, "aria-hidden": "true", children: "0" }), _jsx(AnimatePresence, { initial: false, children: _jsx(motion.span, { className: styles.digitFace, initial: reduced ? { opacity: 0 } : { y: "-70%", opacity: 0 }, animate: { y: "0%", opacity: 1 }, exit: reduced ? { opacity: 0 } : { y: "70%", opacity: 0 }, transition: reduced ? { duration: motionTokens.duration.instant } : { y: physical(.32, .08), opacity: { duration: motionTokens.duration.fast, ease: standard } }, children: char }, char) })] });
|
|
41
|
+
}
|
|
42
|
+
function Countdown({ to, label, reduced, onEnd }) {
|
|
43
|
+
const target = new Date(to).getTime();
|
|
44
|
+
const [now, setNow] = useState(null);
|
|
45
|
+
const ended = useRef(false);
|
|
46
|
+
const endRef = useRef(onEnd);
|
|
47
|
+
useEffect(() => { endRef.current = onEnd; }, [onEnd]);
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
let timer = 0;
|
|
50
|
+
const tick = () => {
|
|
51
|
+
const current = Date.now();
|
|
52
|
+
setNow(current);
|
|
53
|
+
if (current >= target) {
|
|
54
|
+
if (!ended.current) {
|
|
55
|
+
ended.current = true;
|
|
56
|
+
endRef.current?.();
|
|
57
|
+
}
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
// Wake just after each whole second so digits change on the beat.
|
|
61
|
+
timer = window.setTimeout(tick, 1000 - (current % 1000) + 8);
|
|
62
|
+
};
|
|
63
|
+
timer = window.setTimeout(tick, 0);
|
|
64
|
+
return () => window.clearTimeout(timer);
|
|
65
|
+
}, [target]);
|
|
66
|
+
const { d, h, m, s } = parts(now === null ? 0 : target - now);
|
|
67
|
+
const pending = now === null;
|
|
68
|
+
const spoken = pending ? "" : `${d ? `${d} days ` : ""}${h} hours ${m} minutes`;
|
|
69
|
+
const digits = (value, key) => value.split("").map((char, index) => _jsx(Digit, { char: pending ? "0" : char, reduced: reduced }, `${key}${value.length - index}`));
|
|
70
|
+
return _jsxs("span", { className: styles.countdown, children: [label ? _jsx("span", { className: styles.countdownLabel, children: label }) : null, _jsxs("span", { role: "timer", "aria-live": "off", "aria-label": spoken, className: styles.time, "data-pending": pending || undefined, children: [d ? _jsxs(_Fragment, { children: [_jsxs("span", { className: styles.unit, children: [digits(String(d), "d"), _jsx("span", { className: styles.unitMark, "aria-hidden": "true", children: "d" })] }), _jsx("span", { className: styles.gap, "aria-hidden": "true" })] }) : null, digits(pad(h), "h"), _jsx("span", { className: styles.sep, "aria-hidden": "true", children: ":" }), digits(pad(m), "m"), _jsx("span", { className: styles.sep, "aria-hidden": "true", children: ":" }), digits(pad(s), "s")] })] });
|
|
71
|
+
}
|
|
72
|
+
function Face({ announcement, direction, reduced, position, total, onSize, onAction, onCountdownEnd }) {
|
|
73
|
+
const ref = useRef(null);
|
|
74
|
+
const present = useIsPresent();
|
|
75
|
+
useLayoutEffect(() => {
|
|
76
|
+
const node = ref.current;
|
|
77
|
+
if (!node || !present)
|
|
78
|
+
return;
|
|
79
|
+
const report = () => onSize(node.offsetHeight);
|
|
80
|
+
report();
|
|
81
|
+
if (typeof ResizeObserver === "undefined")
|
|
82
|
+
return;
|
|
83
|
+
const observer = new ResizeObserver(report);
|
|
84
|
+
observer.observe(node);
|
|
85
|
+
return () => observer.disconnect();
|
|
86
|
+
}, [onSize, present]);
|
|
87
|
+
const { action, countdown } = announcement;
|
|
88
|
+
const cta = action ? action.href
|
|
89
|
+
? _jsxs("a", { className: styles.cta, href: action.href, onClick: () => { action.onClick?.(); onAction?.(announcement); }, children: [action.label, _jsx(ArrowRight, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" })] })
|
|
90
|
+
: _jsxs("button", { type: "button", className: styles.cta, onClick: () => { action.onClick?.(); onAction?.(announcement); }, children: [action.label, _jsx(ArrowRight, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" })] }) : null;
|
|
91
|
+
return _jsx(motion.div, { ref: ref, className: styles.face, custom: direction, variants: reduced ? fadeVariants : faceVariants, initial: "hidden", animate: "shown", exit: "gone", role: total > 1 ? "group" : undefined, "aria-roledescription": total > 1 ? "slide" : undefined, "aria-label": total > 1 ? `${position} of ${total}` : undefined, inert: !present || undefined, children: _jsxs("p", { className: styles.message, children: [_jsx("span", { className: styles.text, children: announcement.message }), countdown ? _jsx(Countdown, { to: countdown.to, label: countdown.label, reduced: reduced, onEnd: () => onCountdownEnd?.(announcement) }) : null, cta] }) });
|
|
92
|
+
}
|
|
93
|
+
const RING = 2 * Math.PI * 8;
|
|
94
|
+
export const AnnouncementBar = forwardRef(function AnnouncementBar({ messages, id, open: openProp, defaultOpen = true, onOpenChange, index: indexProp, defaultIndex = 0, onIndexChange, interval = 6000, autoPlay = true, controls = false, dismissible = true, tone = "neutral", onAction, onCountdownEnd, label = "Announcements", className, }, ref) {
|
|
95
|
+
const reduced = !!useReducedMotion();
|
|
96
|
+
const uid = useId();
|
|
97
|
+
const total = messages.length;
|
|
98
|
+
/* Open state: controlled, or internal and hidden at once when this id was dismissed before. */
|
|
99
|
+
const [openInternal, setOpenInternal] = useState(defaultOpen);
|
|
100
|
+
const [remembered, setRemembered] = useState(false);
|
|
101
|
+
useLayoutEffect(() => {
|
|
102
|
+
if (!id || openProp !== undefined)
|
|
103
|
+
return;
|
|
104
|
+
let stored = false;
|
|
105
|
+
try {
|
|
106
|
+
stored = window.localStorage.getItem(storageKey(id)) === "dismissed";
|
|
107
|
+
}
|
|
108
|
+
catch { /* Storage can be blocked. */ }
|
|
109
|
+
// Storage is only readable on the client; hide before paint so a dismissed bar never flashes.
|
|
110
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
111
|
+
if (stored)
|
|
112
|
+
setRemembered(true);
|
|
113
|
+
}, [id, openProp]);
|
|
114
|
+
const open = openProp ?? (openInternal && !remembered);
|
|
115
|
+
const dismiss = () => {
|
|
116
|
+
if (id) {
|
|
117
|
+
try {
|
|
118
|
+
window.localStorage.setItem(storageKey(id), "dismissed");
|
|
119
|
+
}
|
|
120
|
+
catch { /* Storage can be blocked. */ }
|
|
121
|
+
}
|
|
122
|
+
if (openProp === undefined)
|
|
123
|
+
setOpenInternal(false);
|
|
124
|
+
onOpenChange?.(false);
|
|
125
|
+
};
|
|
126
|
+
/* Rotation. */
|
|
127
|
+
const [indexInternal, setIndexInternal] = useState(defaultIndex);
|
|
128
|
+
const current = total ? ((indexProp ?? indexInternal) % total + total) % total : 0;
|
|
129
|
+
const [direction, setDirection] = useState(1);
|
|
130
|
+
const go = useCallback((step) => {
|
|
131
|
+
if (total < 2)
|
|
132
|
+
return;
|
|
133
|
+
const next = ((current + step) % total + total) % total;
|
|
134
|
+
setDirection(step > 0 ? 1 : -1);
|
|
135
|
+
if (indexProp === undefined)
|
|
136
|
+
setIndexInternal(next);
|
|
137
|
+
onIndexChange?.(next);
|
|
138
|
+
}, [current, indexProp, onIndexChange, total]);
|
|
139
|
+
const [userPaused, setUserPaused] = useState(false);
|
|
140
|
+
const [hovered, setHovered] = useState(false);
|
|
141
|
+
const [focused, setFocused] = useState(false);
|
|
142
|
+
const [hidden, setHidden] = useState(false);
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
const read = () => setHidden(document.visibilityState === "hidden");
|
|
145
|
+
document.addEventListener("visibilitychange", read);
|
|
146
|
+
return () => document.removeEventListener("visibilitychange", read);
|
|
147
|
+
}, []);
|
|
148
|
+
const rotating = autoPlay && !reduced && total > 1;
|
|
149
|
+
const running = rotating && open && !userPaused && !hovered && !focused && !hidden;
|
|
150
|
+
/* The ring is the clock: its progress runs to one, then advances. Pausing stops it where it is; resuming finishes the rest. */
|
|
151
|
+
const progress = useMotionValue(0);
|
|
152
|
+
const dash = useTransform(progress, value => RING * (1 - value));
|
|
153
|
+
const goRef = useRef(go);
|
|
154
|
+
useEffect(() => { goRef.current = go; }, [go]);
|
|
155
|
+
useEffect(() => { progress.jump(0); }, [current, progress]);
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
if (!running)
|
|
158
|
+
return;
|
|
159
|
+
let controls = animate(progress, 1, {
|
|
160
|
+
duration: interval / 1000 * (1 - progress.get()), ease: "linear",
|
|
161
|
+
onComplete: () => { controls = null; goRef.current(1); },
|
|
162
|
+
});
|
|
163
|
+
return () => controls?.stop();
|
|
164
|
+
}, [running, current, interval, progress]);
|
|
165
|
+
/* The viewport springs to the height of the current message, which can wrap on narrow screens. */
|
|
166
|
+
const height = useMotionValue("auto");
|
|
167
|
+
const measured = useRef(0);
|
|
168
|
+
const onSize = useCallback((next) => {
|
|
169
|
+
if (Math.abs(next - measured.current) < .5)
|
|
170
|
+
return;
|
|
171
|
+
const first = measured.current === 0;
|
|
172
|
+
measured.current = next;
|
|
173
|
+
if (first || reduced)
|
|
174
|
+
height.jump(next);
|
|
175
|
+
else
|
|
176
|
+
animate(height, next, HEIGHT);
|
|
177
|
+
}, [height, reduced]);
|
|
178
|
+
const announcement = messages[current];
|
|
179
|
+
if (!announcement)
|
|
180
|
+
return null;
|
|
181
|
+
const navigable = controls && total > 1;
|
|
182
|
+
const controlCount = (navigable ? 2 + (rotating ? 1 : 0) : 0) + (dismissible ? 1 : 0);
|
|
183
|
+
return _jsx(AnimatePresence, { initial: false, children: open ? _jsx(motion.section, { ref: ref, className: [styles.collapse, className].filter(Boolean).join(" "), "aria-label": label, "aria-roledescription": total > 1 ? "carousel" : undefined, initial: reduced ? { opacity: 0 } : { height: 0, opacity: 0 }, animate: { height: "auto", opacity: 1 }, exit: reduced ? { opacity: 0, transition: { duration: motionTokens.duration.fast } } : { height: 0, opacity: 0, transition: { height: COLLAPSE, opacity: { duration: motionTokens.duration.exit, ease: standard } } }, transition: reduced ? { duration: motionTokens.duration.fast } : { height: COLLAPSE, opacity: { duration: motionTokens.duration.standard, ease: enter } }, onPointerEnter: event => { if (event.pointerType === "mouse")
|
|
184
|
+
setHovered(true); }, onPointerLeave: () => setHovered(false), onFocus: () => setFocused(true), onBlur: event => { if (!event.currentTarget.contains(event.relatedTarget))
|
|
185
|
+
setFocused(false); }, children: _jsxs("div", { className: styles.bar, "data-tone": tone, style: { "--controls": controlCount }, children: [_jsx("div", { className: styles.side, "aria-hidden": "true" }), _jsx(motion.div, { className: styles.viewport, style: { height }, id: `${uid}-slides`, "aria-live": running ? "off" : "polite", "aria-atomic": "false", children: _jsx(AnimatePresence, { initial: false, custom: direction, children: _jsx(Face, { announcement: announcement, direction: direction, reduced: reduced, position: current + 1, total: total, onSize: onSize, onAction: onAction, onCountdownEnd: onCountdownEnd }, announcement.id) }) }), _jsxs("div", { className: styles.controls, children: [navigable ? _jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: styles.icon, "aria-label": "Previous announcement", "aria-controls": `${uid}-slides`, onClick: () => go(-1), children: _jsx(ChevronUp, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }), rotating ? _jsxs("button", { type: "button", className: styles.ring, "aria-label": userPaused ? "Resume announcements" : "Pause announcements", "aria-pressed": userPaused, onClick: () => setUserPaused(paused => !paused), children: [_jsxs("svg", { viewBox: "0 0 20 20", "aria-hidden": "true", children: [_jsx("circle", { cx: "10", cy: "10", r: "8", className: styles.ringTrack }), _jsx(motion.circle, { cx: "10", cy: "10", r: "8", className: styles.ringFill, strokeDasharray: RING, style: { strokeDashoffset: dash } })] }), _jsx("span", { className: styles.ringIcon, children: userPaused ? _jsx(Play, { size: 9, strokeWidth: 2.4, "aria-hidden": "true" }) : _jsx(Pause, { size: 9, strokeWidth: 2.4, "aria-hidden": "true" }) })] }) : null, _jsx("button", { type: "button", className: styles.icon, "aria-label": "Next announcement", "aria-controls": `${uid}-slides`, onClick: () => go(1), children: _jsx(ChevronDown, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) })] }) : null, dismissible ? _jsx("button", { type: "button", className: styles.icon, "aria-label": "Dismiss", onClick: dismiss, children: _jsx(X, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }) : null] })] }) }, "bar") : null });
|
|
186
|
+
});
|
|
187
|
+
export default AnnouncementBar;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/* The outer section animates its height to zero on dismiss, so whatever sits below eases up with it. */
|
|
2
|
+
.collapse { overflow: hidden; container-type: inline-size; font-family: var(--font-body); letter-spacing: var(--tracking-body); }
|
|
3
|
+
|
|
4
|
+
/* The side column mirrors the controls, so the message stays centered on the page rather than in the leftover space. */
|
|
5
|
+
.bar { --icon: 32px; --control-gap: 2px; --muted: var(--text-secondary); display: grid; grid-template-columns: calc(var(--controls, 0) * var(--icon) + max(var(--controls, 0) - 1, 0) * var(--control-gap)) minmax(0, 1fr) auto; align-items: center; gap: var(--space-group); padding: 6px 8px 6px 16px; border-bottom: 1px solid var(--border); background: var(--surface-muted); color: var(--foreground); }
|
|
6
|
+
.bar[data-tone="inverted"] { --muted: color-mix(in oklab, var(--background) 66%, transparent); border-bottom-color: transparent; background: var(--foreground); color: var(--background); }
|
|
7
|
+
|
|
8
|
+
.side { min-width: 0; }
|
|
9
|
+
.viewport { position: relative; min-height: 32px; overflow: hidden; }
|
|
10
|
+
|
|
11
|
+
/* Faces overlap in place; the entering one reports its height and the viewport springs to it. */
|
|
12
|
+
.face { position: absolute; top: 0; right: 0; left: 0; display: flex; min-height: 32px; align-items: center; justify-content: center; }
|
|
13
|
+
.message { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: center; gap: 2px 12px; margin: 0; padding: 6px 0; font-size: var(--text-sm); line-height: var(--leading-body); text-align: center; text-wrap: balance; }
|
|
14
|
+
.text { min-width: 0; }
|
|
15
|
+
|
|
16
|
+
/* The countdown is one run of text: every cell shares the line box, so digits, colons, and the day mark sit on the message baseline. */
|
|
17
|
+
.countdown { display: inline-flex; align-items: baseline; gap: 6px; color: var(--muted); white-space: nowrap; }
|
|
18
|
+
.countdownLabel { color: inherit; }
|
|
19
|
+
.time { display: inline-flex; align-items: baseline; color: var(--foreground); font-variant-numeric: tabular-nums; font-weight: 500; line-height: inherit; }
|
|
20
|
+
.time[data-pending] { opacity: 0; }
|
|
21
|
+
.bar[data-tone="inverted"] .time { color: var(--background); }
|
|
22
|
+
.digit { position: relative; display: inline-block; overflow: hidden; overflow: clip; text-align: center; }
|
|
23
|
+
.digitSizer { visibility: hidden; }
|
|
24
|
+
.digitFace { position: absolute; inset: 0; display: block; }
|
|
25
|
+
.unit { display: inline-flex; align-items: baseline; }
|
|
26
|
+
.unitMark { margin-left: 1px; color: var(--muted); font-weight: 400; }
|
|
27
|
+
/* "case" lifts the colon to the middle of the figures, which it loses once the digits are split into cells. */
|
|
28
|
+
.sep { display: inline-block; padding-inline: 1px; color: var(--muted); font-feature-settings: "case"; font-weight: 400; }
|
|
29
|
+
.gap { display: inline-block; width: .4em; }
|
|
30
|
+
|
|
31
|
+
.cta { display: inline-flex; align-items: center; gap: 4px; padding: 0; border: 0; background: none; color: inherit; cursor: pointer; font: inherit; font-weight: 500; text-decoration: none; white-space: nowrap; -webkit-tap-highlight-color: transparent; }
|
|
32
|
+
.cta svg { transition: transform var(--duration-fast) var(--ease-standard); }
|
|
33
|
+
|
|
34
|
+
.controls { display: flex; align-items: center; gap: var(--control-gap); }
|
|
35
|
+
.icon, .ring { position: relative; display: grid; width: var(--icon); height: var(--icon); flex: none; place-items: center; padding: 0; border: 0; border-radius: 50%; corner-shape: round; background: transparent; color: var(--muted); cursor: pointer; -webkit-tap-highlight-color: transparent; transition: color var(--duration-fast) var(--ease-standard), background-color var(--duration-fast) var(--ease-standard); }
|
|
36
|
+
.icon:active, .ring:active { background: color-mix(in oklab, currentColor 12%, transparent); color: inherit; }
|
|
37
|
+
.ring svg { width: 20px; height: 20px; overflow: visible; transform: rotate(-90deg); }
|
|
38
|
+
.ringTrack { fill: none; stroke: currentColor; stroke-opacity: .22; stroke-width: 1.75; }
|
|
39
|
+
.ringFill { fill: none; stroke: currentColor; stroke-linecap: round; stroke-width: 1.75; }
|
|
40
|
+
.ringIcon { position: absolute; inset: 0; display: grid; place-items: center; }
|
|
41
|
+
.ringIcon svg { width: 9px; height: 9px; transform: none; fill: currentColor; }
|
|
42
|
+
.bar[data-tone="inverted"] .icon, .bar[data-tone="inverted"] .ring { color: var(--muted); }
|
|
43
|
+
|
|
44
|
+
@media (hover: hover) and (pointer: fine) {
|
|
45
|
+
.icon:hover, .ring:hover { background: color-mix(in oklab, currentColor 10%, transparent); }
|
|
46
|
+
.bar .icon:hover, .bar .ring:hover { color: var(--foreground); }
|
|
47
|
+
.bar[data-tone="inverted"] .icon:hover, .bar[data-tone="inverted"] .ring:hover { color: var(--background); }
|
|
48
|
+
.cta:hover svg { transform: translateX(2px); }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/* Narrow bars drop the mirror column and read from the start edge. */
|
|
52
|
+
@container (max-width: 560px) {
|
|
53
|
+
.bar { grid-template-columns: 0 minmax(0, 1fr) auto; gap: 0 var(--space-related); padding-left: 14px; }
|
|
54
|
+
.face { justify-content: flex-start; }
|
|
55
|
+
.message { justify-content: flex-start; column-gap: 10px; text-align: left; }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@media (prefers-reduced-motion: reduce) {
|
|
59
|
+
.icon, .ring, .cta svg { transition: none; }
|
|
60
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type HTMLAttributes } from "react";
|
|
2
|
+
export interface AvatarProps extends HTMLAttributes<HTMLSpanElement> {
|
|
3
|
+
name: string;
|
|
4
|
+
src?: string;
|
|
5
|
+
size?: "sm" | "md" | "lg" | "xl";
|
|
6
|
+
status?: "online" | "offline";
|
|
7
|
+
}
|
|
8
|
+
export declare function Avatar({ name, src, size, status, className, ...props }: AvatarProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useLayoutEffect, useRef, useState } from "react";
|
|
4
|
+
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
|
|
5
|
+
import { SiliconImage } from "../../lib/silicon-provider.js";
|
|
6
|
+
import { motionTokens } from "../../lib/motion-tokens.js";
|
|
7
|
+
import styles from "./avatar.module.css";
|
|
8
|
+
export function Avatar({ name, src, size = "md", status, className, ...props }) {
|
|
9
|
+
const initials = name.trim().split(/\s+/).slice(0, 2).map(part => part[0]?.toUpperCase()).join("");
|
|
10
|
+
const reduceMotion = !!useReducedMotion();
|
|
11
|
+
const image = useRef(null);
|
|
12
|
+
const [failedSrc, setFailedSrc] = useState();
|
|
13
|
+
// A photo that is already decoded shows at once. One that is still loading waits, then fades in from a soft blur.
|
|
14
|
+
useLayoutEffect(() => { const node = image.current; if (node && !node.complete)
|
|
15
|
+
node.dataset.loading = ""; }, [src]);
|
|
16
|
+
const showImage = src && failedSrc !== src;
|
|
17
|
+
return _jsxs("span", { ...props, className: [styles.avatar, styles[size], className].filter(Boolean).join(" "), role: "img", "aria-label": `${name}${status ? `, ${status}` : ""}`, children: [showImage ? _jsx(SiliconImage, { ref: image, src: src, alt: "", fill: true, sizes: size === "xl" ? "88px" : size === "lg" ? "48px" : size === "md" ? "36px" : "28px", onLoad: event => { delete event.currentTarget.dataset.loading; }, onError: () => setFailedSrc(src) }, src) : _jsx("span", { className: src ? styles.fallback : undefined, "aria-hidden": "true", children: initials }), _jsx(AnimatePresence, { initial: false, children: status && _jsx(motion.i, { className: [styles.status, styles[status]].join(" "), "aria-hidden": "true", initial: { opacity: 0, scale: .6 }, animate: { opacity: 1, scale: 1 }, exit: { opacity: 0, scale: .6, transition: { duration: reduceMotion ? 0 : motionTokens.duration.fast } }, transition: reduceMotion ? { duration: 0 } : motionTokens.spring.snappy }, status) })] });
|
|
18
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
.avatar {
|
|
2
|
+
position: relative;
|
|
3
|
+
display: inline-grid;
|
|
4
|
+
flex: 0 0 auto;
|
|
5
|
+
place-items: center;
|
|
6
|
+
overflow: visible;
|
|
7
|
+
border: 1px solid var(--border);
|
|
8
|
+
border-radius: var(--avatar-radius); corner-shape: var(--avatar-corner-shape);
|
|
9
|
+
background: var(--surface-muted);
|
|
10
|
+
color: var(--foreground);
|
|
11
|
+
font-size: var(--text-xs);
|
|
12
|
+
font-weight: 500;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.avatar img {
|
|
16
|
+
pointer-events: none;
|
|
17
|
+
border-radius: inherit; corner-shape: inherit;
|
|
18
|
+
object-fit: cover;
|
|
19
|
+
transition: opacity var(--duration-standard) var(--ease-enter), filter var(--duration-standard) var(--ease-enter);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* Set only while a photo is still downloading after mount, so cached photos never fade. */
|
|
23
|
+
.avatar img[data-loading] {
|
|
24
|
+
opacity: 0;
|
|
25
|
+
filter: blur(4px);
|
|
26
|
+
transition: none;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.fallback { animation: avatar-fallback-in var(--duration-standard) var(--ease-enter) both; }
|
|
30
|
+
|
|
31
|
+
.sm { width: 28px; height: 28px; font-size: 10px; }
|
|
32
|
+
.md { width: 36px; height: 36px; }
|
|
33
|
+
.lg { width: 48px; height: 48px; font-size: var(--text-sm); }
|
|
34
|
+
.xl { width: 88px; height: 88px; font-size: var(--text-xl); }
|
|
35
|
+
|
|
36
|
+
.status {
|
|
37
|
+
position: absolute;
|
|
38
|
+
right: 0;
|
|
39
|
+
bottom: 0;
|
|
40
|
+
z-index: 2;
|
|
41
|
+
width: 10px;
|
|
42
|
+
height: 10px;
|
|
43
|
+
border: 2px solid var(--surface);
|
|
44
|
+
border-radius: var(--radius-full); corner-shape: round;
|
|
45
|
+
box-shadow: 0 0 0 1px color-mix(in oklab, var(--border) 70%, transparent);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.sm .status { width: 8px; height: 8px; border-width: 1.5px; }
|
|
49
|
+
.lg .status { width: 12px; height: 12px; }
|
|
50
|
+
.xl .status { width: 14px; height: 14px; right: 4px; bottom: 4px; }
|
|
51
|
+
.online { background: var(--success); }
|
|
52
|
+
.offline { background: var(--text-muted); }
|
|
53
|
+
|
|
54
|
+
@keyframes avatar-fallback-in { from { opacity: 0; } to { opacity: 1; } }
|
|
55
|
+
|
|
56
|
+
@media (prefers-reduced-motion: reduce) {
|
|
57
|
+
.avatar img { transition: none; }
|
|
58
|
+
.avatar img[data-loading] { filter: none; }
|
|
59
|
+
.fallback { animation: none; }
|
|
60
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface AvatarGroupMember {
|
|
2
|
+
name: string;
|
|
3
|
+
src?: string;
|
|
4
|
+
status?: "online" | "offline";
|
|
5
|
+
}
|
|
6
|
+
export interface AvatarGroupProps {
|
|
7
|
+
members: AvatarGroupMember[];
|
|
8
|
+
max?: number;
|
|
9
|
+
size?: "sm" | "md" | "lg";
|
|
10
|
+
label?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function AvatarGroup({ members, max, size, label }: AvatarGroupProps): import("react").JSX.Element;
|
|
13
|
+
export default AvatarGroup;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
|
|
5
|
+
import { motionTokens } from "../../lib/motion-tokens.js";
|
|
6
|
+
import { Avatar } from "../avatar/avatar.js";
|
|
7
|
+
import styles from "./avatar-group.module.css";
|
|
8
|
+
/** The overflow count rolls the way it moved: more people rise in from below, fewer drop in from above. */
|
|
9
|
+
const rise = { hidden: (direction) => ({ opacity: 0, y: `${.4 * direction}em`, filter: `blur(${motionTokens.blur.subtle}px)` }), shown: { opacity: 1, y: 0, filter: "blur(0px)", transition: { duration: motionTokens.duration.standard, ease: [...motionTokens.ease.enter] } }, gone: (direction) => ({ opacity: 0, y: `${-.4 * direction}em`, filter: `blur(${motionTokens.blur.subtle}px)`, transition: { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.standard] } }) };
|
|
10
|
+
// Same keys as `rise` so the settled style is identical whichever branch renders on the server.
|
|
11
|
+
const fade = { hidden: { opacity: 0, y: 0, filter: "blur(0px)" }, shown: { opacity: 1, y: 0, filter: "blur(0px)", transition: { duration: motionTokens.duration.instant } }, gone: { opacity: 0, y: 0, filter: "blur(0px)", transition: { duration: motionTokens.duration.instant } } };
|
|
12
|
+
// A person joining or leaving opens or closes their slot, so the rest of the stack slides instead of jumping.
|
|
13
|
+
const slot = { initial: { width: 0, opacity: 0, scale: .9 }, animate: { width: "auto", opacity: 1, scale: 1 }, exit: { width: 0, opacity: 0, scale: .9 } };
|
|
14
|
+
export function AvatarGroup({ members, max = 4, size = "md", label = "Team members" }) {
|
|
15
|
+
const reduceMotion = !!useReducedMotion();
|
|
16
|
+
const visible = members.slice(0, Math.max(0, max));
|
|
17
|
+
const overflow = Math.max(0, members.length - visible.length);
|
|
18
|
+
const transition = reduceMotion ? { duration: 0 } : motionTokens.spring.morph;
|
|
19
|
+
const [count, setCount] = useState({ overflow, direction: 1 });
|
|
20
|
+
if (count.overflow !== overflow)
|
|
21
|
+
setCount({ overflow, direction: overflow < count.overflow ? -1 : 1 });
|
|
22
|
+
return _jsx("div", { className: [styles.group, styles[size]].join(" "), role: "group", "aria-label": label, style: { "--count": visible.length + (overflow > 0 ? 1 : 0) }, children: _jsxs(AnimatePresence, { initial: false, children: [visible.map((member, index) => _jsx(motion.span, { className: styles.slot, style: { "--index": index }, ...slot, transition: transition, children: _jsxs("span", { className: styles.lift, children: [_jsx(Avatar, { className: styles.avatar, name: member.name, src: member.src, status: member.status, size: size }), _jsx("span", { className: styles.tip, "aria-hidden": "true", children: member.name })] }) }, member.name)), overflow > 0 ? _jsx(motion.span, { className: styles.slot, style: { "--index": visible.length }, ...slot, transition: transition, children: _jsx("span", { className: [styles.lift, styles.overflow, styles[size]].join(" "), role: "img", "aria-label": `${overflow} more ${label.toLowerCase()}`, children: _jsx(AnimatePresence, { mode: "popLayout", initial: false, custom: count.direction, children: _jsxs(motion.span, { className: styles.count, custom: count.direction, variants: reduceMotion ? fade : rise, initial: "hidden", animate: "shown", exit: "gone", "aria-hidden": "true", children: ["+", overflow] }, overflow) }) }) }, "overflow") : null] }) });
|
|
23
|
+
}
|
|
24
|
+
export default AvatarGroup;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
.group { --avatar-size: 36px; display: inline-flex; height: var(--avatar-size); align-items: center; padding-left: 6px; }
|
|
2
|
+
.group.sm { --avatar-size: 28px; }
|
|
3
|
+
.group.lg { --avatar-size: 48px; }
|
|
4
|
+
/* Each slot is the avatar's visible width, so a slot that opens or closes moves the rest of the stack smoothly. */
|
|
5
|
+
.slot { position: relative; display: inline-flex; flex: 0 0 auto; height: var(--avatar-size); align-items: center; }
|
|
6
|
+
/* One wrapper per person carries every hover move as a single translate, so the stack never changes size. */
|
|
7
|
+
.lift { --fan: 0px; --aside: 0px; --rise: 0px; position: relative; display: inline-flex; margin-left: -6px; border-radius: var(--avatar-radius); corner-shape: var(--avatar-corner-shape); transform: translate(calc(var(--fan) + var(--aside)), var(--rise)); transition: transform var(--duration-standard) var(--ease-standard), box-shadow var(--duration-fast) var(--ease-standard); }
|
|
8
|
+
.avatar, .overflow { border: 2px solid var(--surface); box-shadow: 0 0 0 1px var(--border); transition: box-shadow var(--duration-fast) var(--ease-standard); }
|
|
9
|
+
.overflow { position: relative; display: inline-grid; place-items: center; overflow: hidden; border-radius: var(--avatar-radius); corner-shape: var(--avatar-corner-shape); background: var(--surface-muted); color: var(--text-secondary); font-size: var(--text-xs); font-weight: 500; font-variant-numeric: tabular-nums; }
|
|
10
|
+
.count { display: inline-block; }
|
|
11
|
+
.overflow.sm { width: 28px; height: 28px; font-size: 10px; }
|
|
12
|
+
.overflow.md { width: 36px; height: 36px; }
|
|
13
|
+
.overflow.lg { width: 48px; height: 48px; font-size: var(--text-sm); }
|
|
14
|
+
/* The name floats above the stack, outside the layout, so showing it never moves anything. */
|
|
15
|
+
.tip { position: absolute; z-index: 2; bottom: calc(100% + 8px); left: 50%; padding: 4px 8px; border-radius: var(--radius-full); corner-shape: round; background: var(--foreground); color: var(--background); font-size: var(--text-xs); font-weight: 500; line-height: var(--leading-body); white-space: nowrap; pointer-events: none; opacity: 0; transform: translate(-50%, 3px); transition: opacity var(--duration-fast) var(--ease-standard), transform var(--duration-fast) var(--ease-standard); }
|
|
16
|
+
/* Pointing at the stack loosens its overlap from the center; the person under the pointer lifts with a crisp ring and names themselves, and their neighbors ease aside. */
|
|
17
|
+
@media (hover: hover) and (pointer: fine) {
|
|
18
|
+
.group:hover .lift { --fan: calc((var(--index) - (var(--count) - 1) / 2) * 4px); }
|
|
19
|
+
.slot:hover { z-index: 1; }
|
|
20
|
+
.slot:hover .lift { --rise: -2px; }
|
|
21
|
+
.slot:hover ~ .slot .lift { --aside: 3px; }
|
|
22
|
+
.slot:has(~ .slot:hover) .lift { --aside: -3px; }
|
|
23
|
+
.slot:hover .avatar, .slot:hover .overflow { box-shadow: 0 0 0 1px var(--border-strong); }
|
|
24
|
+
.slot:hover .tip { opacity: 1; transform: translate(-50%, 0); transition-delay: 60ms; }
|
|
25
|
+
}
|
|
26
|
+
/* Reduced motion keeps the stack still; the ring and name still answer the pointer. */
|
|
27
|
+
@media (prefers-reduced-motion: reduce) { .lift { transform: none !important; transition: none; } .tip { transform: translate(-50%, 0); transition: none; } }
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
2
|
+
export type BadgeTone = "neutral" | "success" | "info" | "warning" | "danger";
|
|
3
|
+
export type BadgeSize = "sm" | "md";
|
|
4
|
+
export interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {
|
|
5
|
+
tone?: BadgeTone;
|
|
6
|
+
size?: BadgeSize;
|
|
7
|
+
icon?: ReactNode;
|
|
8
|
+
}
|
|
9
|
+
export declare function Badge({ tone, size, icon, className, children, ...props }: BadgeProps): import("react").JSX.Element;
|
|
10
|
+
export default Badge;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { isValidElement, useEffect, useLayoutEffect, useRef } from "react";
|
|
4
|
+
import { AnimatePresence, animate, motion, useIsPresent, useMotionValue, useReducedMotion } from "motion/react";
|
|
5
|
+
import { motionTokens } from "../../lib/motion-tokens.js";
|
|
6
|
+
import styles from "./badge.module.css";
|
|
7
|
+
const exitFast = { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.standard] };
|
|
8
|
+
const textIn = { opacity: 0, y: "0.3em", filter: `blur(${motionTokens.blur.soft}px)` };
|
|
9
|
+
const textOut = { opacity: 0, y: "-0.3em", filter: `blur(${motionTokens.blur.subtle}px)`, transition: exitFast };
|
|
10
|
+
const iconIn = { opacity: 0, scale: .6, filter: `blur(${motionTokens.blur.subtle}px)` };
|
|
11
|
+
const shown = { opacity: 1, y: "0em", scale: 1, filter: "blur(0px)" };
|
|
12
|
+
const fadeOnly = { opacity: 0, transition: { duration: motionTokens.duration.instant } };
|
|
13
|
+
/** Outgoing copies are hidden from assistive tech while they fade, so only the current text is read. */
|
|
14
|
+
function Swap(props) {
|
|
15
|
+
const present = useIsPresent();
|
|
16
|
+
return _jsx(motion.span, { ...props, "aria-hidden": present ? props["aria-hidden"] : true });
|
|
17
|
+
}
|
|
18
|
+
/** A new icon component crossfades in; re-rendering the same icon stays still. */
|
|
19
|
+
function iconKey(icon) {
|
|
20
|
+
if (!isValidElement(icon))
|
|
21
|
+
return "icon";
|
|
22
|
+
const type = icon.type;
|
|
23
|
+
return typeof type === "string" ? type : type.displayName ?? type.name ?? "icon";
|
|
24
|
+
}
|
|
25
|
+
export function Badge({ tone = "neutral", size = "md", icon, className, children, ...props }) {
|
|
26
|
+
const reduce = useReducedMotion();
|
|
27
|
+
const text = typeof children === "string" || typeof children === "number" ? String(children) : null;
|
|
28
|
+
const glyphKey = icon ? iconKey(icon) : "";
|
|
29
|
+
const body = useRef(null);
|
|
30
|
+
const content = useRef(null);
|
|
31
|
+
// Width stays auto at rest. Only a new label or icon springs it from the old size to the new one; passive reflows (a font swap, a hidden parent) follow instantly.
|
|
32
|
+
const width = useMotionValue("auto");
|
|
33
|
+
const changedAt = useRef(0);
|
|
34
|
+
useLayoutEffect(() => { changedAt.current = performance.now(); }, [text, glyphKey]);
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
const node = content.current;
|
|
37
|
+
if (!node || typeof ResizeObserver === "undefined")
|
|
38
|
+
return;
|
|
39
|
+
let last;
|
|
40
|
+
let controls;
|
|
41
|
+
const settle = () => { width.jump("auto"); if (body.current)
|
|
42
|
+
body.current.style.width = "auto"; };
|
|
43
|
+
const observer = new ResizeObserver(([entry]) => {
|
|
44
|
+
if (!entry)
|
|
45
|
+
return;
|
|
46
|
+
const next = entry.borderBoxSize?.[0]?.inlineSize ?? node.offsetWidth;
|
|
47
|
+
const current = width.get();
|
|
48
|
+
const from = typeof current === "number" ? current : last;
|
|
49
|
+
last = next;
|
|
50
|
+
controls?.stop();
|
|
51
|
+
if (reduce || from === undefined || from === next || performance.now() - changedAt.current > 120)
|
|
52
|
+
return settle();
|
|
53
|
+
// Pin the old width before this frame paints, then spring to the new one.
|
|
54
|
+
if (body.current)
|
|
55
|
+
body.current.style.width = `${from}px`;
|
|
56
|
+
controls = animate(width, [from, next], { ...motionTokens.spring.morph, onComplete: settle });
|
|
57
|
+
});
|
|
58
|
+
observer.observe(node);
|
|
59
|
+
return () => { observer.disconnect(); controls?.stop(); };
|
|
60
|
+
}, [width, reduce]);
|
|
61
|
+
const enter = reduce ? { duration: motionTokens.duration.instant } : { duration: motionTokens.duration.standard, ease: [...motionTokens.ease.enter] };
|
|
62
|
+
const classes = [styles.badge, styles[tone], styles[size], className].filter(Boolean).join(" ");
|
|
63
|
+
// The pill follows its content: text and icon swap in place while the width springs to the new size.
|
|
64
|
+
return _jsx("span", { ...props, className: classes, children: _jsx(motion.span, { ref: body, className: styles.body, style: { width }, children: _jsxs("span", { ref: content, className: styles.content, children: [icon ? _jsx("span", { className: styles.icon, "aria-hidden": "true", children: _jsx(AnimatePresence, { mode: "popLayout", initial: false, children: _jsx(Swap, { className: styles.glyph, initial: reduce ? { opacity: 0 } : iconIn, animate: shown, exit: reduce ? fadeOnly : { ...iconIn, transition: exitFast }, transition: reduce ? enter : motionTokens.spring.snappy, children: icon }, glyphKey) }) }) : null, text === null ? children : _jsx("span", { className: styles.label, children: _jsx(AnimatePresence, { mode: "popLayout", initial: false, children: _jsx(Swap, { className: styles.text, initial: reduce ? { opacity: 0 } : textIn, animate: shown, exit: reduce ? fadeOnly : textOut, transition: enter, children: text }, text) }) })] }) }) });
|
|
65
|
+
}
|
|
66
|
+
export default Badge;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
.badge {
|
|
2
|
+
display: inline-flex;
|
|
3
|
+
min-height: 26px;
|
|
4
|
+
align-items: center;
|
|
5
|
+
border: 1px solid var(--badge-border, var(--border));
|
|
6
|
+
border-radius: var(--radius-full); corner-shape: round;
|
|
7
|
+
padding: 0 10px;
|
|
8
|
+
color: var(--badge-foreground, var(--foreground));
|
|
9
|
+
background: var(--badge-background, var(--surface));
|
|
10
|
+
font-size: var(--text-xs);
|
|
11
|
+
font-weight: 500;
|
|
12
|
+
letter-spacing: -.01em;
|
|
13
|
+
line-height: 1;
|
|
14
|
+
white-space: nowrap;
|
|
15
|
+
/* Clip keeps the inline baseline while the pill morphs around swapped text. */
|
|
16
|
+
overflow: clip;
|
|
17
|
+
transition: border-color var(--duration-standard) var(--ease-standard), background-color var(--duration-standard) var(--ease-standard), color var(--duration-standard) var(--ease-standard);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@media (hover: hover) and (pointer: fine) {
|
|
21
|
+
.badge:hover { border-color: color-mix(in oklab, var(--badge-color, var(--foreground)) 32%, var(--border)); background: color-mix(in oklab, var(--badge-color, var(--foreground)) 10%, var(--surface)); }
|
|
22
|
+
}
|
|
23
|
+
.sm { min-height: 22px; padding: 0 8px; font-size: 11px; }
|
|
24
|
+
.md { font-size: var(--text-xs); }
|
|
25
|
+
|
|
26
|
+
.body { display: block; }
|
|
27
|
+
.content { position: relative; display: inline-flex; width: max-content; align-items: center; gap: 5px; }
|
|
28
|
+
.sm .content { gap: 4px; }
|
|
29
|
+
.label, .glyph { position: relative; display: inline-flex; align-items: center; }
|
|
30
|
+
.text { display: block; }
|
|
31
|
+
|
|
32
|
+
.icon {
|
|
33
|
+
position: relative;
|
|
34
|
+
display: inline-flex;
|
|
35
|
+
width: auto;
|
|
36
|
+
height: auto;
|
|
37
|
+
flex: 0 0 auto;
|
|
38
|
+
align-items: center;
|
|
39
|
+
justify-content: center;
|
|
40
|
+
color: var(--badge-color, var(--foreground));
|
|
41
|
+
line-height: 1;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.neutral { --badge-color: var(--text-secondary); --badge-foreground: var(--text-secondary); --badge-background: var(--surface-muted); --badge-border: var(--border); }
|
|
45
|
+
.success { --badge-color: var(--success); --badge-foreground: var(--success); --badge-background: color-mix(in oklab, var(--success) 10%, var(--surface)); --badge-border: color-mix(in oklab, var(--success) 25%, var(--border)); }
|
|
46
|
+
.info { --badge-color: var(--accent); --badge-foreground: var(--accent-strong); --badge-background: var(--accent-subtle); --badge-border: color-mix(in oklab, var(--accent) 24%, var(--border)); }
|
|
47
|
+
.warning { --badge-color: var(--warning); --badge-foreground: var(--warning); --badge-background: color-mix(in oklab, var(--warning) 11%, var(--surface)); --badge-border: color-mix(in oklab, var(--warning) 27%, var(--border)); }
|
|
48
|
+
.danger { --badge-color: var(--danger); --badge-foreground: var(--danger); --badge-background: color-mix(in oklab, var(--danger) 10%, var(--surface)); --badge-border: color-mix(in oklab, var(--danger) 26%, var(--border)); }
|
|
49
|
+
|
|
50
|
+
@media (prefers-reduced-motion: reduce) {
|
|
51
|
+
.badge { transition-duration: var(--duration-instant); }
|
|
52
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface BarChartDatum {
|
|
2
|
+
/** Stable identity, such as an ISO date. A bar whose key survives a data change keeps its place, so switching ranges morphs instead of redrawing. */
|
|
3
|
+
key: string;
|
|
4
|
+
/** Full label for the headline and screen readers, such as "Tue, Sep 15". */
|
|
5
|
+
label: string;
|
|
6
|
+
/** Short label under the bar, such as "T" or "Sep 14". Leave it out to keep the axis quiet. */
|
|
7
|
+
axisLabel?: string;
|
|
8
|
+
value: number;
|
|
9
|
+
}
|
|
10
|
+
/** Use a bar chart for one measure across a run of days, weeks, or months, when people compare each period against the rest and against the average. */
|
|
11
|
+
export interface BarChartProps {
|
|
12
|
+
data: BarChartDatum[];
|
|
13
|
+
/** What is measured, such as "Active minutes". Names the chart for assistive technology. */
|
|
14
|
+
label: string;
|
|
15
|
+
/** The range on show, such as "Sep 15–21, 2026". It rests under the headline. */
|
|
16
|
+
period: string;
|
|
17
|
+
/** Unit after each value, such as "min". */
|
|
18
|
+
unit?: string;
|
|
19
|
+
/** Headline label at rest, above the average. */
|
|
20
|
+
averageLabel?: string;
|
|
21
|
+
/** Headline label while a bar is scrubbed. */
|
|
22
|
+
valueLabel?: string;
|
|
23
|
+
/** Header of the first column in the data table read by screen readers. */
|
|
24
|
+
categoryLabel?: string;
|
|
25
|
+
/** Draws the average as a reference line that springs to each new range. */
|
|
26
|
+
showAverage?: boolean;
|
|
27
|
+
/** Plot height in pixels. */
|
|
28
|
+
height?: number;
|
|
29
|
+
formatValue?: (value: number) => string;
|
|
30
|
+
}
|
|
31
|
+
export declare function BarChart({ data, label, period, unit, averageLabel, valueLabel, categoryLabel, showAverage, height, formatValue }: BarChartProps): import("react").JSX.Element;
|
|
32
|
+
export default BarChart;
|