@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,242 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useCallback, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { flushSync } from "react-dom";
|
|
5
|
+
import { AnimatePresence, animate, motion, useMotionValue, useMotionValueEvent, useReducedMotion } from "motion/react";
|
|
6
|
+
import { Search, X } from "lucide-react";
|
|
7
|
+
import { motionTokens } from "../../lib/motion-tokens.js";
|
|
8
|
+
import styles from "./expanding-search.module.css";
|
|
9
|
+
const enter = [...motionTokens.ease.enter];
|
|
10
|
+
const standard = [...motionTokens.ease.standard];
|
|
11
|
+
const FALLBACK_SIZE = 44;
|
|
12
|
+
/** Title starts beat word starts, which beat matches inside a word, which beat keyword and meta matches. Ties keep the original order. */
|
|
13
|
+
function rank(items, query, limit) {
|
|
14
|
+
const q = query.toLocaleLowerCase();
|
|
15
|
+
const scored = [];
|
|
16
|
+
items.forEach((item, order) => {
|
|
17
|
+
const title = item.title.toLocaleLowerCase();
|
|
18
|
+
const at = title.indexOf(q);
|
|
19
|
+
const extra = [item.meta ?? "", ...(item.keywords ?? [])].some(word => word.toLocaleLowerCase().includes(q));
|
|
20
|
+
const score = at === 0 ? 0 : at > 0 && /[\s\-/]/.test(title[at - 1]) ? 1 : at > 0 ? 2 : extra ? 3 : -1;
|
|
21
|
+
if (score >= 0)
|
|
22
|
+
scored.push({ item, score, order });
|
|
23
|
+
});
|
|
24
|
+
return scored.sort((a, b) => a.score - b.score || a.order - b.order).slice(0, limit).map(entry => entry.item);
|
|
25
|
+
}
|
|
26
|
+
function groupBy(list, source) {
|
|
27
|
+
const order = new Map();
|
|
28
|
+
source.forEach((item, index) => { const name = item.group ?? ""; if (!order.has(name))
|
|
29
|
+
order.set(name, index); });
|
|
30
|
+
const groups = new Map();
|
|
31
|
+
list.forEach(item => { const name = item.group ?? ""; groups.set(name, [...(groups.get(name) ?? []), item]); });
|
|
32
|
+
return [...groups].sort((a, b) => (order.get(a[0]) ?? 0) - (order.get(b[0]) ?? 0)).map(([name, items]) => ({ name, items }));
|
|
33
|
+
}
|
|
34
|
+
function Match({ text, query }) {
|
|
35
|
+
const at = query ? text.toLocaleLowerCase().indexOf(query.toLocaleLowerCase()) : -1;
|
|
36
|
+
if (at < 0)
|
|
37
|
+
return text;
|
|
38
|
+
return _jsxs(_Fragment, { children: [text.slice(0, at), _jsx("mark", { className: styles.mark, children: text.slice(at, at + query.length) }), text.slice(at + query.length)] });
|
|
39
|
+
}
|
|
40
|
+
/** The panel shares the field's width value, so on first open it unfolds out of the field; afterwards only its height follows the content. */
|
|
41
|
+
function Panel({ width, reduced, children }) {
|
|
42
|
+
const panelRef = useRef(null);
|
|
43
|
+
const bodyRef = useRef(null);
|
|
44
|
+
const height = useMotionValue(0);
|
|
45
|
+
useLayoutEffect(() => {
|
|
46
|
+
const panel = panelRef.current, body = bodyRef.current;
|
|
47
|
+
if (!panel || !body || typeof ResizeObserver === "undefined")
|
|
48
|
+
return;
|
|
49
|
+
const observer = new ResizeObserver(() => {
|
|
50
|
+
const next = body.offsetHeight + panel.offsetHeight - panel.clientHeight;
|
|
51
|
+
if (reduced)
|
|
52
|
+
height.jump(next);
|
|
53
|
+
else
|
|
54
|
+
animate(height, next, motionTokens.spring.smooth);
|
|
55
|
+
});
|
|
56
|
+
observer.observe(body);
|
|
57
|
+
return () => observer.disconnect();
|
|
58
|
+
}, [height, reduced]);
|
|
59
|
+
// Reduced motion skips the unfold: the panel is its content's height from the first frame and only fades.
|
|
60
|
+
return _jsx(motion.div, { ref: panelRef, className: styles.panel, style: { width, height: reduced ? "auto" : height }, initial: { opacity: 0 }, animate: { opacity: 1, transition: { duration: reduced ? .15 : motionTokens.duration.fast, ease: enter } }, exit: { opacity: 0, transition: { duration: reduced ? .1 : motionTokens.duration.instant, ease: standard } }, children: _jsx("div", { ref: bodyRef, className: styles.body, children: children }) });
|
|
61
|
+
}
|
|
62
|
+
/** Offsets ignore transforms, so they describe where rows sit in the list, not where a gliding row happens to be mid-flight. */
|
|
63
|
+
function offsetWithin(node, container) {
|
|
64
|
+
let top = 0;
|
|
65
|
+
let current = node;
|
|
66
|
+
while (current && current !== container) {
|
|
67
|
+
top += current.offsetTop;
|
|
68
|
+
current = current.offsetParent;
|
|
69
|
+
}
|
|
70
|
+
return top;
|
|
71
|
+
}
|
|
72
|
+
function Listbox({ id, label, groups, query, activeIndex, optionId, reduced, keyboard, onHover, onChoose }) {
|
|
73
|
+
const listRef = useRef(null);
|
|
74
|
+
const flat = groups.flatMap(group => group.items);
|
|
75
|
+
const layoutKey = flat.map(item => item.id).join("|");
|
|
76
|
+
const activeItem = flat[activeIndex];
|
|
77
|
+
const activeId = activeItem ? optionId(activeItem) : "";
|
|
78
|
+
/** The highlight lives inside the active row, so it rides that row wherever it glides. Moving to another row, it starts where the old one was and springs home. */
|
|
79
|
+
const travel = useMotionValue(0);
|
|
80
|
+
const fade = useMotionValue(1);
|
|
81
|
+
const last = useRef(null);
|
|
82
|
+
useLayoutEffect(() => {
|
|
83
|
+
const list = listRef.current;
|
|
84
|
+
const row = activeId ? document.getElementById(activeId) : null;
|
|
85
|
+
if (!list || !row) {
|
|
86
|
+
last.current = null;
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const top = offsetWithin(row, list);
|
|
90
|
+
const previous = last.current;
|
|
91
|
+
last.current = { id: activeId, set: layoutKey, top };
|
|
92
|
+
// Same row: its highlight is already in place, and any travel still in flight stays relative to it.
|
|
93
|
+
if (previous?.id === activeId)
|
|
94
|
+
return;
|
|
95
|
+
if (previous && previous.set === layoutKey && !reduced) {
|
|
96
|
+
const velocity = travel.getVelocity();
|
|
97
|
+
travel.jump(previous.top + travel.get() - top);
|
|
98
|
+
animate(travel, 0, { ...motionTokens.spring.snappy, velocity });
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
// Nothing was highlighted, or new results arrived: it fades in on its row, without travel.
|
|
102
|
+
travel.jump(0);
|
|
103
|
+
if (reduced)
|
|
104
|
+
fade.jump(1);
|
|
105
|
+
else {
|
|
106
|
+
fade.jump(0);
|
|
107
|
+
animate(fade, 1, { duration: motionTokens.duration.instant, ease: enter });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (keyboard.current)
|
|
111
|
+
row.scrollIntoView({ block: "nearest" });
|
|
112
|
+
}, [activeId, layoutKey, reduced, travel, fade, keyboard]);
|
|
113
|
+
return _jsx("div", { ref: listRef, id: id, role: "listbox", "aria-label": label, className: styles.listbox, children: _jsx(AnimatePresence, { mode: "popLayout", initial: false, children: groups.map(group => {
|
|
114
|
+
const headingId = `${id}-${group.name || "results"}`.replace(/\s+/g, "-");
|
|
115
|
+
return _jsxs(motion.div, { role: "group", "aria-labelledby": group.name ? headingId : undefined, className: styles.group, layout: reduced ? false : "position", layoutDependency: layoutKey, initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0, transition: { duration: .08 } }, transition: { opacity: { duration: motionTokens.duration.fast, ease: enter }, layout: motionTokens.spring.smooth }, children: [group.name && _jsx("div", { id: headingId, className: styles.groupLabel, role: "presentation", children: group.name }), _jsx(AnimatePresence, { mode: "popLayout", initial: false, children: group.items.map(item => {
|
|
116
|
+
const position = flat.indexOf(item);
|
|
117
|
+
const active = position === activeIndex;
|
|
118
|
+
return _jsxs(motion.div, { id: optionId(item), role: "option", "aria-selected": active, "data-active": active || undefined, className: styles.option, layout: reduced ? false : "position", layoutDependency: layoutKey, initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0, transition: { duration: .08 } }, transition: { opacity: { duration: motionTokens.duration.fast, ease: enter }, layout: motionTokens.spring.smooth }, onPointerMove: () => { if (!active) {
|
|
119
|
+
keyboard.current = false;
|
|
120
|
+
onHover(position);
|
|
121
|
+
} }, onMouseDown: event => event.preventDefault(), onClick: () => onChoose(item), children: [active && _jsx(motion.span, { className: styles.highlight, style: { y: travel, opacity: fade }, "aria-hidden": "true" }), item.icon && _jsx("span", { className: styles.optionIcon, "aria-hidden": "true", children: item.icon }), _jsxs("span", { className: styles.optionText, children: [_jsx("span", { className: styles.optionTitle, children: _jsx(Match, { text: item.title, query: query }) }), item.meta && _jsx("span", { className: styles.optionMeta, children: item.meta })] })] }, item.id);
|
|
122
|
+
}) })] }, group.name);
|
|
123
|
+
}) }) });
|
|
124
|
+
}
|
|
125
|
+
export function ExpandingSearch({ label, items, suggestions = [], suggestionsLabel = "Recent", placeholder, onSelect, onExpandedChange, expandedWidth = 360, maxResults = 6, anchor = "end", emptyHint = "Try a shorter word or check the spelling.", className }) {
|
|
126
|
+
const id = useId();
|
|
127
|
+
const reduced = useReducedMotion() ?? false;
|
|
128
|
+
const rootRef = useRef(null);
|
|
129
|
+
const shellRef = useRef(null);
|
|
130
|
+
const inputRef = useRef(null);
|
|
131
|
+
const buttonRef = useRef(null);
|
|
132
|
+
const [expanded, setExpanded] = useState(false);
|
|
133
|
+
const [focused, setFocused] = useState(false);
|
|
134
|
+
const [query, setQuery] = useState("");
|
|
135
|
+
const [active, setActive] = useState(-1);
|
|
136
|
+
const open = useRef(false);
|
|
137
|
+
const size = useRef({ collapsed: FALLBACK_SIZE, expanded: expandedWidth });
|
|
138
|
+
const width = useMotionValue(FALLBACK_SIZE);
|
|
139
|
+
/** True while the last highlight move came from the keyboard, so only keyboard moves scroll the list. */
|
|
140
|
+
const keyboard = useRef(false);
|
|
141
|
+
// The corner radius follows the width: a circle while collapsed, the control radius once open. It rides the same spring, so the two never drift apart.
|
|
142
|
+
const syncShape = useCallback((value) => {
|
|
143
|
+
const { collapsed, expanded: full } = size.current;
|
|
144
|
+
shellRef.current?.style.setProperty("--es-open", String(Math.min(1, Math.max(0, (value - collapsed) / Math.max(1, full - collapsed)))));
|
|
145
|
+
}, []);
|
|
146
|
+
useMotionValueEvent(width, "change", syncShape);
|
|
147
|
+
// The field grows to its own limit or its container, whichever is smaller. Container resizes are followed at once; they are not the person's action.
|
|
148
|
+
useLayoutEffect(() => {
|
|
149
|
+
const root = rootRef.current, shell = shellRef.current;
|
|
150
|
+
if (!root || !shell)
|
|
151
|
+
return;
|
|
152
|
+
const measure = () => {
|
|
153
|
+
const collapsed = shell.offsetHeight || FALLBACK_SIZE;
|
|
154
|
+
const full = Math.max(collapsed, Math.min(expandedWidth, root.clientWidth));
|
|
155
|
+
size.current = { collapsed, expanded: full };
|
|
156
|
+
root.style.setProperty("--es-width", `${full}px`);
|
|
157
|
+
const goal = open.current ? full : collapsed;
|
|
158
|
+
if (width.isAnimating())
|
|
159
|
+
animate(width, goal, motionTokens.spring.morph);
|
|
160
|
+
else
|
|
161
|
+
width.jump(goal);
|
|
162
|
+
syncShape(width.get());
|
|
163
|
+
};
|
|
164
|
+
measure();
|
|
165
|
+
if (typeof ResizeObserver === "undefined")
|
|
166
|
+
return;
|
|
167
|
+
const observer = new ResizeObserver(measure);
|
|
168
|
+
observer.observe(root);
|
|
169
|
+
return () => observer.disconnect();
|
|
170
|
+
}, [expandedWidth, width, syncShape]);
|
|
171
|
+
// Opening grows on the morph spring with a hint of overshoot; folding back settles without one, so the circle never dips below its own size.
|
|
172
|
+
const morphTo = (goal) => { if (reduced)
|
|
173
|
+
width.jump(goal);
|
|
174
|
+
else
|
|
175
|
+
animate(width, goal, goal >= width.get() ? motionTokens.spring.morph : motionTokens.spring.smooth); };
|
|
176
|
+
// Focus moves inside the same click, so touch keyboards open; the field is inert until this flush makes it focusable.
|
|
177
|
+
function expand() {
|
|
178
|
+
if (open.current)
|
|
179
|
+
return;
|
|
180
|
+
open.current = true;
|
|
181
|
+
flushSync(() => { setExpanded(true); setFocused(true); });
|
|
182
|
+
inputRef.current?.focus({ preventScroll: true });
|
|
183
|
+
morphTo(size.current.expanded);
|
|
184
|
+
onExpandedChange?.(true);
|
|
185
|
+
}
|
|
186
|
+
function collapse(returnFocus) {
|
|
187
|
+
if (!open.current)
|
|
188
|
+
return;
|
|
189
|
+
open.current = false;
|
|
190
|
+
flushSync(() => { setExpanded(false); setFocused(false); setQuery(""); setActive(-1); });
|
|
191
|
+
if (returnFocus)
|
|
192
|
+
buttonRef.current?.focus({ preventScroll: true });
|
|
193
|
+
morphTo(size.current.collapsed);
|
|
194
|
+
onExpandedChange?.(false);
|
|
195
|
+
}
|
|
196
|
+
const trimmed = query.trim();
|
|
197
|
+
const results = useMemo(() => trimmed ? rank(items, trimmed, maxResults) : [], [items, trimmed, maxResults]);
|
|
198
|
+
const mode = !trimmed ? (suggestions.length ? "suggestions" : null) : results.length ? "results" : "empty";
|
|
199
|
+
const groups = useMemo(() => mode === "suggestions" ? [{ name: suggestionsLabel, items: suggestions }] : mode === "results" ? groupBy(results, items) : [], [mode, suggestions, suggestionsLabel, results, items]);
|
|
200
|
+
const flat = groups.flatMap(group => group.items);
|
|
201
|
+
const activeIndex = Math.min(active, flat.length - 1);
|
|
202
|
+
const activeItem = flat[activeIndex];
|
|
203
|
+
const optionId = (item) => `${id}-${mode}-${item.id}`;
|
|
204
|
+
const listboxId = `${id}-${mode}-listbox`;
|
|
205
|
+
const panelOpen = expanded && focused && mode !== null;
|
|
206
|
+
const announcement = !panelOpen ? "" : mode === "empty" ? `No results for ${trimmed}` : mode === "results" ? `${flat.length} ${flat.length === 1 ? "result" : "results"}` : `${flat.length} ${suggestionsLabel.toLocaleLowerCase()} ${flat.length === 1 ? "item" : "items"}`;
|
|
207
|
+
function choose(item) {
|
|
208
|
+
collapse(true);
|
|
209
|
+
onSelect?.(item);
|
|
210
|
+
}
|
|
211
|
+
function handleKeyDown(event) {
|
|
212
|
+
if (event.key === "Escape") {
|
|
213
|
+
event.preventDefault();
|
|
214
|
+
collapse(true);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (!panelOpen || !flat.length)
|
|
218
|
+
return;
|
|
219
|
+
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
220
|
+
event.preventDefault();
|
|
221
|
+
const step = event.key === "ArrowDown" ? 1 : -1;
|
|
222
|
+
keyboard.current = true;
|
|
223
|
+
setActive(activeIndex < 0 ? (step > 0 ? 0 : flat.length - 1) : (activeIndex + step + flat.length) % flat.length);
|
|
224
|
+
}
|
|
225
|
+
else if (event.key === "Enter" && activeItem) {
|
|
226
|
+
event.preventDefault();
|
|
227
|
+
choose(activeItem);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
const clearHidden = reduced ? { opacity: 0 } : { opacity: 0, scale: .6, filter: `blur(${motionTokens.blur.subtle}px)` };
|
|
231
|
+
return _jsxs("div", { ref: rootRef, className: [styles.root, className].filter(Boolean).join(" "), "data-anchor": anchor, children: [_jsxs(motion.div, { ref: shellRef, className: styles.shell, "data-expanded": expanded || undefined, style: { width }, children: [_jsx("button", { ref: buttonRef, type: "button", className: styles.trigger, "aria-label": label, hidden: expanded, onClick: expand }), _jsx(Search, { className: styles.icon, width: 18, height: 18, strokeWidth: 1.75, "aria-hidden": "true" }), _jsx(motion.input, { ref: inputRef, className: styles.input, type: "text", role: "combobox", inert: !expanded, value: query, placeholder: placeholder ?? label, "aria-label": label, "aria-expanded": panelOpen, "aria-controls": panelOpen && mode !== "empty" ? listboxId : undefined, "aria-autocomplete": "list", "aria-activedescendant": panelOpen && activeItem ? optionId(activeItem) : undefined, autoComplete: "off", autoCorrect: "off", spellCheck: false, enterKeyHint: "search", initial: false, animate: { opacity: expanded ? 1 : 0 }, transition: expanded ? { duration: reduced ? .15 : .2, ease: enter, delay: reduced ? 0 : .08 } : { duration: reduced ? .1 : .08, ease: standard }, onChange: event => { const next = event.target.value; setQuery(next); setActive(next.trim() ? 0 : -1); keyboard.current = false; }, onFocus: () => setFocused(true), onBlur: () => {
|
|
232
|
+
// Switching windows keeps the search as it was; the field refocuses on return.
|
|
233
|
+
if (typeof document !== "undefined" && !document.hasFocus())
|
|
234
|
+
return;
|
|
235
|
+
setFocused(false);
|
|
236
|
+
if (open.current && !inputRef.current?.value.trim())
|
|
237
|
+
collapse(false);
|
|
238
|
+
}, onKeyDown: handleKeyDown }), _jsx(AnimatePresence, { initial: false, children: expanded && query && _jsx(motion.button, { type: "button", tabIndex: -1, className: styles.clear, "aria-label": "Clear search", onMouseDown: event => event.preventDefault(), onClick: () => { setQuery(""); setActive(-1); inputRef.current?.focus({ preventScroll: true }); }, initial: clearHidden, animate: { opacity: 1, scale: 1, filter: "blur(0px)" }, exit: { ...clearHidden, transition: { duration: reduced ? .1 : motionTokens.duration.instant, ease: standard } }, transition: reduced ? { duration: .15 } : { ...motionTokens.spring.snappy, opacity: { duration: motionTokens.duration.fast }, filter: { duration: motionTokens.duration.fast } }, children: _jsx(X, { width: 16, height: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }, "clear") })] }), _jsx(AnimatePresence, { children: panelOpen && _jsx(Panel, { width: width, reduced: reduced, children: _jsx(AnimatePresence, { mode: "popLayout", initial: false, children: _jsx(motion.div, { className: styles.view, initial: reduced ? { opacity: 0 } : { opacity: 0, filter: `blur(${motionTokens.blur.subtle}px)` }, animate: { opacity: 1, filter: "blur(0px)" }, exit: { opacity: 0, transition: { duration: .1, ease: standard } }, transition: { duration: reduced ? .15 : motionTokens.duration.fast, ease: enter }, children: mode === "empty"
|
|
239
|
+
? _jsxs("div", { className: styles.empty, children: [_jsxs("p", { className: styles.emptyTitle, children: ["No results for \u201C", trimmed, "\u201D"] }), _jsx("p", { className: styles.emptyHint, children: emptyHint })] })
|
|
240
|
+
: _jsx(Listbox, { id: listboxId, label: mode === "suggestions" ? suggestionsLabel : `Results for ${trimmed}`, groups: groups, query: mode === "results" ? trimmed : "", activeIndex: activeIndex, optionId: optionId, reduced: reduced, keyboard: keyboard, onHover: setActive, onChoose: choose }) }, mode) }) }, "panel") }), _jsx("span", { className: styles.srOnly, role: "status", "aria-live": "polite", children: announcement })] });
|
|
241
|
+
}
|
|
242
|
+
export default ExpandingSearch;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/* The root is the lane the field may grow into. Only the shell and the panel take pointer events, so an idle lane never blocks what sits under it. */
|
|
2
|
+
.root { position: relative; width: 100%; min-width: var(--control-height-md); height: var(--control-height-md); pointer-events: none; }
|
|
3
|
+
/* One shell is both the button and the field. Its width rides a spring and its radius follows the width, from a circle to the control radius. */
|
|
4
|
+
.shell { position: absolute; top: 0; right: 0; height: 100%; overflow: hidden; border: 1px solid var(--border); border-radius: calc(var(--control-height-md) / 2 - (var(--control-height-md) / 2 - var(--radius-control)) * var(--es-open, 0)); corner-shape: round; background: var(--surface); color: var(--text-secondary); pointer-events: auto; transition: border-color var(--duration-fast) var(--ease-standard), background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard), box-shadow var(--duration-standard) var(--ease-standard), transform var(--duration-spring) var(--ease-spring); }
|
|
5
|
+
.root[data-anchor="start"] .shell { right: auto; left: 0; }
|
|
6
|
+
.shell[data-expanded] { border-color: var(--border-strong); color: var(--foreground); }
|
|
7
|
+
.shell:has(> .trigger:focus-visible), .shell:has(> .input:focus) { border-color: var(--foreground); box-shadow: 0 0 0 3px var(--focus-ring); color: var(--foreground); }
|
|
8
|
+
@media (hover: hover) and (pointer: fine) {
|
|
9
|
+
.shell:not([data-expanded]):hover { background: var(--surface-muted); color: var(--foreground); }
|
|
10
|
+
.shell[data-expanded]:not(:has(> .input:focus)):hover { border-color: var(--foreground); }
|
|
11
|
+
.clear:hover { background: var(--surface-muted); color: var(--foreground); }
|
|
12
|
+
}
|
|
13
|
+
/* A compact press while it is still a button; the morph takes over on release. */
|
|
14
|
+
.shell:not([data-expanded]):has(> .trigger:active) { transform: scale(.97); transition-duration: var(--duration-fast), var(--duration-fast), var(--duration-fast), var(--duration-standard), var(--duration-instant); }
|
|
15
|
+
.shell[data-expanded] { corner-shape: var(--corner-shape); }
|
|
16
|
+
.trigger { position: absolute; z-index: 1; inset: 0; margin: 0; padding: 0; border: 0; border-radius: inherit; corner-shape: inherit; background: transparent; cursor: pointer; -webkit-tap-highlight-color: transparent; }
|
|
17
|
+
.trigger:focus-visible { outline: none; }
|
|
18
|
+
/* The glass sits at the leading edge, so it is centered in the circle and rides that edge to its place in the open field. */
|
|
19
|
+
.icon { position: absolute; top: calc(50% - 9px); left: calc(var(--control-height-md) / 2 - 10px); pointer-events: none; }
|
|
20
|
+
.input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; padding: 0 40px 0 38px; border: 0; background: transparent; color: var(--foreground); font: inherit; font-size: var(--text-sm); letter-spacing: var(--tracking-body); text-overflow: ellipsis; outline: none; }
|
|
21
|
+
.input:focus-visible { outline: none; }
|
|
22
|
+
.input::placeholder { color: var(--text-muted); opacity: 1; }
|
|
23
|
+
.input[inert] { pointer-events: none; }
|
|
24
|
+
.clear { position: absolute; z-index: 1; top: calc(50% - 14px); right: 7px; display: grid; width: 28px; height: 28px; place-items: center; margin: 0; padding: 0; border: 0; border-radius: var(--radius-full); corner-shape: round; background: transparent; color: var(--text-muted); cursor: pointer; transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard); -webkit-tap-highlight-color: transparent; }
|
|
25
|
+
|
|
26
|
+
/* A floating layer: its width is the field's, so it unfolds from the field on first open; its content keeps the final width and rides the leading edge. */
|
|
27
|
+
.panel { position: absolute; z-index: 40; top: calc(100% + 8px); right: 0; overflow: hidden; border: 1px solid var(--border); border-radius: var(--radius-container); corner-shape: var(--corner-shape); background: var(--surface-raised); box-shadow: var(--shadow-floating); pointer-events: auto; }
|
|
28
|
+
.root[data-anchor="start"] .panel { right: auto; left: 0; }
|
|
29
|
+
.body { position: relative; width: calc(var(--es-width, 360px) - 2px); max-height: min(22rem, 60vh); overflow-x: hidden; overflow-y: auto; overscroll-behavior: contain; padding: 6px; scrollbar-width: thin; }
|
|
30
|
+
.view { position: relative; }
|
|
31
|
+
/* A flex gap, not margins: a group popping out while it leaves never lets the next one's margin collapse through the list. */
|
|
32
|
+
.listbox { position: relative; isolation: isolate; display: flex; flex-direction: column; gap: 4px; }
|
|
33
|
+
.group { position: relative; }
|
|
34
|
+
.groupLabel { padding: 8px 12px 4px; color: var(--text-muted); font-size: var(--text-xs); font-weight: 500; line-height: var(--leading-body); }
|
|
35
|
+
/* Nested corners step down from the panel by its inset. The highlight paints under every row's content, so text never hides while it travels. */
|
|
36
|
+
.option { position: relative; display: flex; min-height: 48px; align-items: center; gap: 12px; padding: 6px 12px; border-radius: calc(var(--radius-container) - 6px); corner-shape: var(--corner-shape); color: var(--foreground); cursor: pointer; user-select: none; -webkit-tap-highlight-color: transparent; }
|
|
37
|
+
.highlight { position: absolute; z-index: -1; inset: 0; border-radius: inherit; corner-shape: inherit; background: color-mix(in oklab, var(--foreground) 6%, transparent); pointer-events: none; }
|
|
38
|
+
.optionIcon { display: grid; flex: none; width: 18px; place-items: center; color: var(--text-muted); transition: color var(--duration-fast) var(--ease-standard); }
|
|
39
|
+
.option[data-active] .optionIcon { color: var(--foreground); }
|
|
40
|
+
.optionText { display: grid; min-width: 0; }
|
|
41
|
+
.optionTitle, .optionMeta { overflow: hidden; line-height: var(--leading-body); text-overflow: ellipsis; white-space: nowrap; }
|
|
42
|
+
.optionTitle { color: var(--foreground); font-size: var(--text-sm); }
|
|
43
|
+
.optionMeta { color: var(--text-muted); font-size: var(--text-xs); }
|
|
44
|
+
.mark { background: none; color: var(--foreground); font-weight: 500; }
|
|
45
|
+
.empty { display: grid; gap: 2px; padding: 12px 12px 14px; }
|
|
46
|
+
.emptyTitle { margin: 0; overflow-wrap: anywhere; color: var(--foreground); font-size: var(--text-sm); font-weight: 500; line-height: var(--leading-body); }
|
|
47
|
+
.emptyHint { margin: 0; color: var(--text-muted); font-size: var(--text-xs); line-height: var(--leading-body); }
|
|
48
|
+
.srOnly { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
|
|
49
|
+
@media (prefers-reduced-motion: reduce) {
|
|
50
|
+
.shell, .clear, .optionIcon { transition: none; }
|
|
51
|
+
.shell:not([data-expanded]):has(> .trigger:active) { transform: none; }
|
|
52
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export type FileDropzoneStatus = "uploading" | "uploaded" | "failed";
|
|
2
|
+
/** One row in the file list. A row without a status is a plain selection. */
|
|
3
|
+
/** `preview` is a thumbnail URL; image files added by the visitor get one automatically. */
|
|
4
|
+
export type FileDropzoneItem = {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
size: number;
|
|
8
|
+
status?: FileDropzoneStatus;
|
|
9
|
+
progress?: number;
|
|
10
|
+
error?: string;
|
|
11
|
+
retryable?: boolean;
|
|
12
|
+
file?: File;
|
|
13
|
+
preview?: string;
|
|
14
|
+
};
|
|
15
|
+
/** Report 0 to 100 through onProgress, resolve when the file lands, or reject with an Error whose message becomes the row's reason. Removing the row aborts the signal. */
|
|
16
|
+
export type FileDropzoneUpload = (item: FileDropzoneItem, options: {
|
|
17
|
+
onProgress: (percent: number) => void;
|
|
18
|
+
signal: AbortSignal;
|
|
19
|
+
}) => Promise<void>;
|
|
20
|
+
export type FileDropzoneProps = {
|
|
21
|
+
accept?: string;
|
|
22
|
+
multiple?: boolean;
|
|
23
|
+
maxFiles?: number;
|
|
24
|
+
onFilesChange?: (files: File[]) => void;
|
|
25
|
+
label?: string;
|
|
26
|
+
description?: string;
|
|
27
|
+
/** Rows present at mount, such as files uploaded earlier. They render in place without an entrance. */
|
|
28
|
+
defaultItems?: FileDropzoneItem[];
|
|
29
|
+
/** Uploads each added file. Without it the list shows plain selections. */
|
|
30
|
+
onUpload?: FileDropzoneUpload;
|
|
31
|
+
/** Files above this many bytes fail with a size reason and never upload. */
|
|
32
|
+
maxSize?: number;
|
|
33
|
+
/** Render the file list under the drop target or inside its edge. */
|
|
34
|
+
listPlacement?: "below" | "inside";
|
|
35
|
+
/** Replaces the small line under the description. */
|
|
36
|
+
note?: string;
|
|
37
|
+
/** Label while files hover over the target. */
|
|
38
|
+
dropLabel?: string;
|
|
39
|
+
/** Once the list holds this many files, the prompt folds to a slim bar so the list has room. A file dragged over brings the icon back. */
|
|
40
|
+
compactAt?: number;
|
|
41
|
+
};
|
|
42
|
+
export declare function formatFileSize(bytes: number): string;
|
|
43
|
+
export declare function FileDropzone({ accept, multiple, maxFiles, onFilesChange, label, description, defaultItems, onUpload, maxSize, listPlacement, note, dropLabel, compactAt }: FileDropzoneProps): import("react").JSX.Element;
|
|
44
|
+
export default FileDropzone;
|