@sincpro/web-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 +21 -0
- package/README.md +184 -2
- package/dist/components/activity/Activity.d.ts +69 -0
- package/dist/components/activity/Activity.js +279 -0
- package/dist/components/activity/Messages.d.ts +66 -0
- package/dist/components/activity/Messages.js +46 -0
- package/dist/components/activity/Timeline.d.ts +21 -0
- package/dist/components/activity/Timeline.js +30 -0
- package/dist/components/activity/index.d.ts +3 -0
- package/dist/components/activity/index.js +4 -0
- package/dist/components/assist/AssistMark.d.ts +64 -0
- package/dist/components/assist/AssistMark.js +108 -0
- package/dist/components/assist/AssistPending.d.ts +24 -0
- package/dist/components/assist/AssistPending.js +26 -0
- package/dist/components/assist/AssistSuggestion.d.ts +42 -0
- package/dist/components/assist/AssistSuggestion.js +55 -0
- package/dist/components/assist/ChangeReview.d.ts +56 -0
- package/dist/components/assist/ChangeReview.js +121 -0
- package/dist/components/assist/assist.d.ts +77 -0
- package/dist/components/assist/assist.js +87 -0
- package/dist/components/assist/index.d.ts +5 -0
- package/dist/components/assist/index.js +7 -0
- package/dist/components/form/Form.d.ts +52 -0
- package/dist/components/form/Form.js +49 -0
- package/dist/components/form/Sheet.d.ts +47 -0
- package/dist/components/form/Sheet.js +90 -0
- package/dist/components/form/index.d.ts +2 -0
- package/dist/components/form/index.js +4 -0
- package/dist/components/frame/Chart.d.ts +13 -0
- package/dist/components/frame/Chart.js +24 -0
- package/dist/components/frame/FrameView.d.ts +33 -0
- package/dist/components/frame/FrameView.js +125 -0
- package/dist/components/frame/index.d.ts +3 -0
- package/dist/components/frame/index.js +3 -0
- package/dist/components/frame/model.d.ts +43 -0
- package/dist/components/frame/model.js +101 -0
- package/dist/components/index.d.ts +10 -0
- package/dist/components/index.js +11 -0
- package/dist/components/lines/LineItems.d.ts +111 -0
- package/dist/components/lines/LineItems.js +317 -0
- package/dist/components/lines/index.d.ts +1 -0
- package/dist/components/lines/index.js +2 -0
- package/dist/components/lines/lines.d.ts +23 -0
- package/dist/components/lines/lines.js +26 -0
- package/dist/components/listing/ColumnsMenu.d.ts +20 -0
- package/dist/components/listing/ColumnsMenu.js +46 -0
- package/dist/components/listing/GroupingLevels.d.ts +35 -0
- package/dist/components/listing/GroupingLevels.js +45 -0
- package/dist/components/listing/Listing.d.ts +27 -0
- package/dist/components/listing/Listing.js +86 -0
- package/dist/components/listing/ListingBar.d.ts +112 -0
- package/dist/components/listing/ListingBar.js +144 -0
- package/dist/components/listing/Pagination.d.ts +36 -0
- package/dist/components/listing/Pagination.js +69 -0
- package/dist/components/listing/RecordCard.d.ts +33 -0
- package/dist/components/listing/RecordCard.js +22 -0
- package/dist/components/listing/RecordLink.d.ts +7 -0
- package/dist/components/listing/RecordLink.js +22 -0
- package/dist/components/listing/Row.d.ts +110 -0
- package/dist/components/listing/Row.js +123 -0
- package/dist/components/listing/SortMenu.d.ts +62 -0
- package/dist/components/listing/SortMenu.js +105 -0
- package/dist/components/listing/ViewSwitcher.d.ts +24 -0
- package/dist/components/listing/ViewSwitcher.js +16 -0
- package/dist/components/listing/columns.d.ts +47 -0
- package/dist/components/listing/columns.js +72 -0
- package/dist/components/listing/grouping.d.ts +35 -0
- package/dist/components/listing/grouping.js +47 -0
- package/dist/components/listing/index.d.ts +17 -0
- package/dist/components/listing/index.js +20 -0
- package/dist/components/listing/modelColumns.d.ts +50 -0
- package/dist/components/listing/modelColumns.js +103 -0
- package/dist/components/listing/pages.d.ts +78 -0
- package/dist/components/listing/pages.js +135 -0
- package/dist/components/listing/selection.d.ts +21 -0
- package/dist/components/listing/selection.js +43 -0
- package/dist/components/listing/set.d.ts +48 -0
- package/dist/components/listing/set.js +24 -0
- package/dist/components/listing/tree.d.ts +28 -0
- package/dist/components/listing/tree.js +23 -0
- package/dist/components/navigation/CommandPalette.d.ts +40 -0
- package/dist/components/navigation/CommandPalette.js +114 -0
- package/dist/components/navigation/NavigationTree.d.ts +31 -0
- package/dist/components/navigation/NavigationTree.js +79 -0
- package/dist/components/navigation/commands.d.ts +30 -0
- package/dist/components/navigation/commands.js +30 -0
- package/dist/components/navigation/index.d.ts +3 -0
- package/dist/components/navigation/index.js +4 -0
- package/dist/components/record/Breadcrumb.d.ts +42 -0
- package/dist/components/record/Breadcrumb.js +98 -0
- package/dist/components/record/DirtyActions.d.ts +26 -0
- package/dist/components/record/DirtyActions.js +22 -0
- package/dist/components/record/Pager.d.ts +18 -0
- package/dist/components/record/Pager.js +33 -0
- package/dist/components/record/RecordHeader.d.ts +75 -0
- package/dist/components/record/RecordHeader.js +104 -0
- package/dist/components/record/RecordNotices.d.ts +37 -0
- package/dist/components/record/RecordNotices.js +75 -0
- package/dist/components/record/RecordSkeleton.d.ts +3 -0
- package/dist/components/record/RecordSkeleton.js +21 -0
- package/dist/components/record/SmartButtons.d.ts +35 -0
- package/dist/components/record/SmartButtons.js +38 -0
- package/dist/components/record/StatusBar.d.ts +51 -0
- package/dist/components/record/StatusBar.js +168 -0
- package/dist/components/record/ThumbBar.d.ts +36 -0
- package/dist/components/record/ThumbBar.js +86 -0
- package/dist/components/record/index.d.ts +6 -0
- package/dist/components/record/index.js +8 -0
- package/dist/components/search/CriteriaBuilder.d.ts +58 -0
- package/dist/components/search/CriteriaBuilder.js +278 -0
- package/dist/components/search/CriteriaChips.d.ts +22 -0
- package/dist/components/search/CriteriaChips.js +64 -0
- package/dist/components/search/FilterBuilder.d.ts +142 -0
- package/dist/components/search/FilterBuilder.js +421 -0
- package/dist/components/search/JoinWord.d.ts +8 -0
- package/dist/components/search/JoinWord.js +47 -0
- package/dist/components/search/JsonEditor.d.ts +19 -0
- package/dist/components/search/JsonEditor.js +78 -0
- package/dist/components/search/SearchBar.d.ts +78 -0
- package/dist/components/search/SearchBar.js +387 -0
- package/dist/components/search/criteria.d.ts +116 -0
- package/dist/components/search/criteria.js +159 -0
- package/dist/components/search/datePeriods.d.ts +122 -0
- package/dist/components/search/datePeriods.js +134 -0
- package/dist/components/search/dateRanges.d.ts +54 -0
- package/dist/components/search/dateRanges.js +89 -0
- package/dist/components/search/index.d.ts +9 -0
- package/dist/components/search/index.js +20 -0
- package/dist/components/search/model.d.ts +54 -0
- package/dist/components/search/model.js +37 -0
- package/dist/components/search/query.d.ts +166 -0
- package/dist/components/search/query.js +436 -0
- package/dist/components/shell/AppFrame.d.ts +14 -0
- package/dist/components/shell/AppFrame.js +80 -0
- package/dist/components/shell/TopBar.d.ts +73 -0
- package/dist/components/shell/TopBar.js +54 -0
- package/dist/components/shell/headerSlot.d.ts +17 -0
- package/dist/components/shell/headerSlot.js +21 -0
- package/dist/components/shell/index.d.ts +2 -0
- package/dist/components/shell/index.js +3 -0
- package/dist/foundations/aria.d.ts +14 -0
- package/dist/foundations/aria.js +18 -0
- package/dist/foundations/calendar.d.ts +28 -0
- package/dist/foundations/calendar.js +183 -0
- package/dist/foundations/copy.d.ts +703 -0
- package/dist/foundations/copy.es.d.ts +2 -0
- package/dist/foundations/copy.es.js +430 -0
- package/dist/foundations/copy.js +520 -0
- package/dist/foundations/density.d.ts +16 -0
- package/dist/foundations/density.js +21 -0
- package/dist/foundations/grouping.d.ts +281 -0
- package/dist/foundations/grouping.js +0 -0
- package/dist/foundations/index.d.ts +14 -0
- package/dist/foundations/index.js +30 -0
- package/dist/foundations/layout.d.ts +9 -0
- package/dist/foundations/layout.js +9 -0
- package/dist/foundations/pivot.d.ts +222 -0
- package/dist/foundations/pivot.js +198 -0
- package/dist/foundations/pivotSheet.d.ts +61 -0
- package/dist/foundations/pivotSheet.js +0 -0
- package/dist/foundations/slots.d.ts +67 -0
- package/dist/foundations/slots.js +63 -0
- package/dist/foundations/sticky.d.ts +53 -0
- package/dist/foundations/sticky.js +150 -0
- package/dist/foundations/surface.d.ts +9 -0
- package/dist/foundations/surface.js +26 -0
- package/dist/foundations/theme.d.ts +42 -0
- package/dist/foundations/theme.js +88 -0
- package/dist/foundations/types.d.ts +23 -0
- package/dist/foundations/types.js +50 -0
- package/dist/foundations/variants.d.ts +19 -0
- package/dist/foundations/variants.js +48 -0
- package/dist/foundations/xlsx.d.ts +86 -0
- package/dist/foundations/xlsx.js +0 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +19 -0
- package/dist/primitives/block/Block.d.ts +66 -0
- package/dist/primitives/block/Block.js +34 -0
- package/dist/primitives/block/Line.d.ts +18 -0
- package/dist/primitives/block/Line.js +45 -0
- package/dist/primitives/block/SectionGrid.d.ts +30 -0
- package/dist/primitives/block/SectionGrid.js +29 -0
- package/dist/primitives/block/Steps.d.ts +76 -0
- package/dist/primitives/block/Steps.js +43 -0
- package/dist/primitives/block/index.d.ts +4 -0
- package/dist/primitives/block/index.js +7 -0
- package/dist/primitives/button/Button.d.ts +137 -0
- package/dist/primitives/button/Button.js +234 -0
- package/dist/primitives/button/index.d.ts +1 -0
- package/dist/primitives/button/index.js +2 -0
- package/dist/primitives/cell/CellStack.d.ts +47 -0
- package/dist/primitives/cell/CellStack.js +33 -0
- package/dist/primitives/cell/index.d.ts +1 -0
- package/dist/primitives/cell/index.js +3 -0
- package/dist/primitives/choice/ComboBox.d.ts +19 -0
- package/dist/primitives/choice/ComboBox.js +52 -0
- package/dist/primitives/choice/RadioGroup.d.ts +18 -0
- package/dist/primitives/choice/RadioGroup.js +50 -0
- package/dist/primitives/choice/SearchField.d.ts +33 -0
- package/dist/primitives/choice/SearchField.js +46 -0
- package/dist/primitives/choice/SegmentedControl.d.ts +16 -0
- package/dist/primitives/choice/SegmentedControl.js +69 -0
- package/dist/primitives/choice/Slider.d.ts +16 -0
- package/dist/primitives/choice/Slider.js +46 -0
- package/dist/primitives/choice/Switch.d.ts +14 -0
- package/dist/primitives/choice/Switch.js +65 -0
- package/dist/primitives/choice/ThemeSwitcher.d.ts +10 -0
- package/dist/primitives/choice/ThemeSwitcher.js +25 -0
- package/dist/primitives/choice/index.d.ts +8 -0
- package/dist/primitives/choice/index.js +11 -0
- package/dist/primitives/choice/options.d.ts +28 -0
- package/dist/primitives/choice/options.js +6 -0
- package/dist/primitives/date/Calendar.d.ts +31 -0
- package/dist/primitives/date/Calendar.js +134 -0
- package/dist/primitives/date/DatePicker.d.ts +42 -0
- package/dist/primitives/date/DatePicker.js +103 -0
- package/dist/primitives/date/DateTime.d.ts +35 -0
- package/dist/primitives/date/DateTime.js +100 -0
- package/dist/primitives/date/index.d.ts +3 -0
- package/dist/primitives/date/index.js +6 -0
- package/dist/primitives/dialog/Dialog.d.ts +118 -0
- package/dist/primitives/dialog/Dialog.js +121 -0
- package/dist/primitives/dialog/Drill.d.ts +90 -0
- package/dist/primitives/dialog/Drill.js +172 -0
- package/dist/primitives/dialog/index.d.ts +2 -0
- package/dist/primitives/dialog/index.js +4 -0
- package/dist/primitives/disclosure/Disclosure.d.ts +36 -0
- package/dist/primitives/disclosure/Disclosure.js +52 -0
- package/dist/primitives/disclosure/index.d.ts +1 -0
- package/dist/primitives/disclosure/index.js +2 -0
- package/dist/primitives/field/Checks.d.ts +57 -0
- package/dist/primitives/field/Checks.js +72 -0
- package/dist/primitives/field/DropZone.d.ts +42 -0
- package/dist/primitives/field/DropZone.js +52 -0
- package/dist/primitives/field/Field.d.ts +178 -0
- package/dist/primitives/field/Field.js +251 -0
- package/dist/primitives/field/Suggest.d.ts +48 -0
- package/dist/primitives/field/Suggest.js +43 -0
- package/dist/primitives/field/aria.d.ts +61 -0
- package/dist/primitives/field/aria.js +70 -0
- package/dist/primitives/field/context.d.ts +30 -0
- package/dist/primitives/field/context.js +13 -0
- package/dist/primitives/field/copy.d.ts +16 -0
- package/dist/primitives/field/copy.js +69 -0
- package/dist/primitives/field/index.d.ts +9 -0
- package/dist/primitives/field/index.js +15 -0
- package/dist/primitives/field/layout.d.ts +24 -0
- package/dist/primitives/field/layout.js +10 -0
- package/dist/primitives/field/mode.d.ts +9 -0
- package/dist/primitives/field/mode.js +13 -0
- package/dist/primitives/field/note.d.ts +55 -0
- package/dist/primitives/field/note.js +117 -0
- package/dist/primitives/hint/Hint.d.ts +31 -0
- package/dist/primitives/hint/Hint.js +42 -0
- package/dist/primitives/hint/index.d.ts +1 -0
- package/dist/primitives/hint/index.js +1 -0
- package/dist/primitives/index.d.ts +20 -0
- package/dist/primitives/index.js +21 -0
- package/dist/primitives/menu/Menu.d.ts +128 -0
- package/dist/primitives/menu/Menu.js +89 -0
- package/dist/primitives/menu/index.d.ts +1 -0
- package/dist/primitives/menu/index.js +2 -0
- package/dist/primitives/notice/Callout.d.ts +49 -0
- package/dist/primitives/notice/Callout.js +28 -0
- package/dist/primitives/notice/Flag.d.ts +13 -0
- package/dist/primitives/notice/Flag.js +31 -0
- package/dist/primitives/notice/Notice.d.ts +45 -0
- package/dist/primitives/notice/Notice.js +61 -0
- package/dist/primitives/notice/Toast.d.ts +55 -0
- package/dist/primitives/notice/Toast.js +142 -0
- package/dist/primitives/notice/index.d.ts +4 -0
- package/dist/primitives/notice/index.js +6 -0
- package/dist/primitives/overlay/Popover.d.ts +6 -0
- package/dist/primitives/overlay/Popover.js +27 -0
- package/dist/primitives/overlay/PopoverTrigger.d.ts +55 -0
- package/dist/primitives/overlay/PopoverTrigger.js +38 -0
- package/dist/primitives/overlay/Preview.d.ts +19 -0
- package/dist/primitives/overlay/Preview.js +60 -0
- package/dist/primitives/overlay/Tooltip.d.ts +38 -0
- package/dist/primitives/overlay/Tooltip.js +74 -0
- package/dist/primitives/overlay/index.d.ts +4 -0
- package/dist/primitives/overlay/index.js +7 -0
- package/dist/primitives/picker/Picker.d.ts +89 -0
- package/dist/primitives/picker/Picker.js +204 -0
- package/dist/primitives/picker/index.d.ts +1 -0
- package/dist/primitives/picker/index.js +2 -0
- package/dist/primitives/surface/Peek.d.ts +60 -0
- package/dist/primitives/surface/Peek.js +71 -0
- package/dist/primitives/surface/index.d.ts +1 -0
- package/dist/primitives/surface/index.js +2 -0
- package/dist/primitives/table/CellCopy.d.ts +20 -0
- package/dist/primitives/table/CellCopy.js +23 -0
- package/dist/primitives/table/CellEditor.d.ts +14 -0
- package/dist/primitives/table/CellEditor.js +83 -0
- package/dist/primitives/table/DataTable.d.ts +189 -0
- package/dist/primitives/table/DataTable.js +330 -0
- package/dist/primitives/table/EditableTable.d.ts +191 -0
- package/dist/primitives/table/EditableTable.js +617 -0
- package/dist/primitives/table/PivotTable.d.ts +14 -0
- package/dist/primitives/table/PivotTable.js +0 -0
- package/dist/primitives/table/ReportTable.d.ts +38 -0
- package/dist/primitives/table/ReportTable.js +106 -0
- package/dist/primitives/table/RowActions.d.ts +16 -0
- package/dist/primitives/table/RowActions.js +28 -0
- package/dist/primitives/table/SheetTable.d.ts +73 -0
- package/dist/primitives/table/SheetTable.js +230 -0
- package/dist/primitives/table/Table.d.ts +106 -0
- package/dist/primitives/table/Table.js +116 -0
- package/dist/primitives/table/index.d.ts +7 -0
- package/dist/primitives/table/index.js +9 -0
- package/dist/primitives/tabs/Tabs.d.ts +91 -0
- package/dist/primitives/tabs/Tabs.js +56 -0
- package/dist/primitives/tabs/index.d.ts +1 -0
- package/dist/primitives/tabs/index.js +1 -0
- package/dist/primitives/tag/Tag.d.ts +36 -0
- package/dist/primitives/tag/Tag.js +68 -0
- package/dist/primitives/tag/Tags.d.ts +57 -0
- package/dist/primitives/tag/Tags.js +104 -0
- package/dist/primitives/tag/TagsField.d.ts +35 -0
- package/dist/primitives/tag/TagsField.js +186 -0
- package/dist/primitives/tag/choices.d.ts +38 -0
- package/dist/primitives/tag/choices.js +26 -0
- package/dist/primitives/tag/index.d.ts +4 -0
- package/dist/primitives/tag/index.js +8 -0
- package/dist/primitives/toolbar/Toolbar.d.ts +13 -0
- package/dist/primitives/toolbar/Toolbar.js +36 -0
- package/dist/primitives/toolbar/index.d.ts +1 -0
- package/dist/primitives/toolbar/index.js +2 -0
- package/dist/primitives/tree/Tree.d.ts +56 -0
- package/dist/primitives/tree/Tree.js +74 -0
- package/dist/primitives/tree/index.d.ts +1 -0
- package/dist/primitives/tree/index.js +3 -0
- package/dist/primitives/value/Copyable.d.ts +48 -0
- package/dist/primitives/value/Copyable.js +71 -0
- package/dist/primitives/value/Fecha.d.ts +12 -0
- package/dist/primitives/value/Fecha.js +89 -0
- package/dist/primitives/value/Meter.d.ts +29 -0
- package/dist/primitives/value/Meter.js +46 -0
- package/dist/primitives/value/Metric.d.ts +42 -0
- package/dist/primitives/value/Metric.js +38 -0
- package/dist/primitives/value/Person.d.ts +90 -0
- package/dist/primitives/value/Person.js +208 -0
- package/dist/primitives/value/ProgressBar.d.ts +31 -0
- package/dist/primitives/value/ProgressBar.js +53 -0
- package/dist/primitives/value/Relation.d.ts +74 -0
- package/dist/primitives/value/Relation.js +74 -0
- package/dist/primitives/value/Totals.d.ts +121 -0
- package/dist/primitives/value/Totals.js +54 -0
- package/dist/primitives/value/Value.d.ts +40 -0
- package/dist/primitives/value/Value.js +80 -0
- package/dist/primitives/value/index.d.ts +9 -0
- package/dist/primitives/value/index.js +10 -0
- package/dist/tokens.css +1062 -0
- package/dist/views/index.d.ts +2 -0
- package/dist/views/index.js +3 -0
- package/dist/views/list/List.d.ts +119 -0
- package/dist/views/list/List.js +101 -0
- package/dist/views/list/ListView.d.ts +6 -0
- package/dist/views/list/ListView.js +295 -0
- package/dist/views/list/columns.d.ts +18 -0
- package/dist/views/list/columns.js +27 -0
- package/dist/views/list/derive.d.ts +87 -0
- package/dist/views/list/derive.js +207 -0
- package/dist/views/list/index.d.ts +2 -0
- package/dist/views/list/index.js +4 -0
- package/dist/views/list/presentations.d.ts +39 -0
- package/dist/views/list/presentations.js +105 -0
- package/dist/views/list/types.d.ts +284 -0
- package/dist/views/list/types.js +1 -0
- package/dist/views/list/useNarrowList.d.ts +17 -0
- package/dist/views/list/useNarrowList.js +32 -0
- package/dist/views/record/Record.d.ts +145 -0
- package/dist/views/record/Record.js +211 -0
- package/dist/views/record/index.d.ts +1 -0
- package/dist/views/record/index.js +2 -0
- package/package.json +113 -4
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { AriaLocale } from "../../foundations/aria.js";
|
|
4
|
+
import { fillCopy, useUiCopy } from "../../foundations/copy.js";
|
|
5
|
+
import { cn, tv } from "../../foundations/variants.js";
|
|
6
|
+
import { IconButton } from "../button/Button.js";
|
|
7
|
+
import { FieldButton, FieldGroup } from "../field/aria.js";
|
|
8
|
+
import { useFieldIds } from "../field/context.js";
|
|
9
|
+
import { ReadValue } from "../field/Field.js";
|
|
10
|
+
import { useFormMode } from "../field/mode.js";
|
|
11
|
+
import { Popover } from "../overlay/Popover.js";
|
|
12
|
+
import { Check, ChevronDown, SquareArrowOutUpRight } from "lucide-react";
|
|
13
|
+
import { useContext, useMemo, useRef, useState } from "react";
|
|
14
|
+
import { Collection, ComboBox as AriaComboBox, ComboBoxStateContext, Header, Input, ListBox, ListBoxItem, ListBoxLoadMoreItem, ListBoxSection, Text, useFilter, } from "react-aria-components";
|
|
15
|
+
/** The id of the «see all…» row. Not a record id: it cannot collide with one. */
|
|
16
|
+
const BROWSE_ALL = "\u0000browse-all";
|
|
17
|
+
const CAP = 8;
|
|
18
|
+
/**
|
|
19
|
+
* What the list shows and how many are left out of it. Capped at 8 with the footer count,
|
|
20
|
+
* unless the list loads incrementally — then everything that arrived is listed and the
|
|
21
|
+
* scroll asks for more.
|
|
22
|
+
*/
|
|
23
|
+
export function shownRows(filtered, { loading, incremental }) {
|
|
24
|
+
if (loading)
|
|
25
|
+
return { visible: [], remaining: 0 };
|
|
26
|
+
if (incremental)
|
|
27
|
+
return { visible: filtered, remaining: 0 };
|
|
28
|
+
const visible = filtered.slice(0, CAP);
|
|
29
|
+
return { visible, remaining: filtered.length - visible.length };
|
|
30
|
+
}
|
|
31
|
+
/** Folds consecutive items of the same `section` into one run under a header. The ids of
|
|
32
|
+
* the runs cannot collide with a record id, like the «see all…» one. */
|
|
33
|
+
function sectioned(items) {
|
|
34
|
+
const out = [];
|
|
35
|
+
for (const item of items) {
|
|
36
|
+
if (item.section === undefined) {
|
|
37
|
+
out.push(item);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const last = out.at(-1);
|
|
41
|
+
if (last !== undefined && "items" in last && last.section === item.section) {
|
|
42
|
+
last.items.push(item);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
out.push({ id: `\u0000section-${out.length}`, section: item.section, items: [item] });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
/* No focus ring on an option: the focus stays in the input (the list is driven from it),
|
|
51
|
+
and the marked row is said by its tint — a ring inside the list as well as around the
|
|
52
|
+
field is two marks for one place. */
|
|
53
|
+
const option = tv({
|
|
54
|
+
base: "flex w-full min-w-0 cursor-default gap-2 rounded-control px-2 text-left outline-0 select-none forced-color-adjust-none",
|
|
55
|
+
variants: {
|
|
56
|
+
hints: {
|
|
57
|
+
true: "items-start py-1.5",
|
|
58
|
+
false: "items-center py-1",
|
|
59
|
+
},
|
|
60
|
+
isFocused: {
|
|
61
|
+
true: "bg-primary/8 forced-colors:bg-[Highlight] forced-colors:text-[HighlightText]",
|
|
62
|
+
},
|
|
63
|
+
isHovered: {
|
|
64
|
+
true: "bg-ink/4",
|
|
65
|
+
},
|
|
66
|
+
isPressed: {
|
|
67
|
+
true: "bg-primary/12",
|
|
68
|
+
},
|
|
69
|
+
isDisabled: {
|
|
70
|
+
true: "opacity-45 forced-colors:text-[GrayText]",
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
compoundVariants: [{ isFocused: true, isHovered: true, class: "bg-primary/8" }],
|
|
74
|
+
});
|
|
75
|
+
export function Picker({ mode, ...props }) {
|
|
76
|
+
if (useFormMode(mode) === "read") {
|
|
77
|
+
return _jsx(ReadValue, { value: props.items.find((i) => i.id === props.value)?.name ?? null });
|
|
78
|
+
}
|
|
79
|
+
/* The filter reads the locale from React Aria's context, so the box that provides it has
|
|
80
|
+
to be ABOVE the component that asks for it. */
|
|
81
|
+
return (_jsx(AriaLocale, { children: _jsx(EditablePicker, { ...props }) }));
|
|
82
|
+
}
|
|
83
|
+
function EditablePicker({ id: ownId, items, value, onChange, placeholder, onBrowseAll, onOpen, emptyText, searchPlaceholder, loading = false, loadingText, onLoadMore, loadingMore = false, describedBy: ownDescribedBy, invalid: ownInvalid, disabled = false, "aria-label": ariaLabel, }) {
|
|
84
|
+
const { id, labelledBy, describedBy, invalid } = useFieldIds({
|
|
85
|
+
id: ownId,
|
|
86
|
+
describedBy: ownDescribedBy,
|
|
87
|
+
invalid: ownInvalid,
|
|
88
|
+
});
|
|
89
|
+
const copy = useUiCopy();
|
|
90
|
+
const { contains } = useFilter({ sensitivity: "base" });
|
|
91
|
+
const shownPlaceholder = placeholder ?? copy.picker.placeholder;
|
|
92
|
+
const shownEmpty = emptyText ?? copy.picker.empty;
|
|
93
|
+
const shownSearch = searchPlaceholder ?? copy.picker.search;
|
|
94
|
+
const shownLoading = loadingText ?? copy.picker.loading;
|
|
95
|
+
const chosen = items.find((i) => i.id === value);
|
|
96
|
+
const chosenName = chosen?.name ?? "";
|
|
97
|
+
/* Both the text and the choice are controlled, so React Aria never rewrites the field
|
|
98
|
+
from its own collection — which is capped and filtered, and may not hold the chosen
|
|
99
|
+
record at all. `query` is what filters: empty when the list opens (the whole list,
|
|
100
|
+
not just the one already chosen), the typed text after that. */
|
|
101
|
+
const [open, setOpen] = useState(false);
|
|
102
|
+
const [text, setText] = useState(chosenName);
|
|
103
|
+
const [query, setQuery] = useState("");
|
|
104
|
+
const [shownFor, setShownFor] = useState(chosenName);
|
|
105
|
+
if (!open && shownFor !== chosenName) {
|
|
106
|
+
setShownFor(chosenName);
|
|
107
|
+
setText(chosenName);
|
|
108
|
+
}
|
|
109
|
+
const filtered = useMemo(() => {
|
|
110
|
+
const q = query.trim();
|
|
111
|
+
if (q === "")
|
|
112
|
+
return items;
|
|
113
|
+
return items.filter((i) => contains(`${i.name} ${i.meta ?? ""} ${i.haystack ?? ""}`, q));
|
|
114
|
+
}, [items, query, contains]);
|
|
115
|
+
const { visible, remaining } = shownRows(filtered, {
|
|
116
|
+
loading,
|
|
117
|
+
incremental: onLoadMore !== undefined,
|
|
118
|
+
});
|
|
119
|
+
const hasMarks = items.some((i) => i.icon !== undefined);
|
|
120
|
+
const hasHints = visible.some((i) => i.meta !== undefined);
|
|
121
|
+
/* «See all…» is an OPTION at the end of the list and not a button in a footer: the
|
|
122
|
+
arrows reach it like any other row. As a footer button it was out of the keyboard's
|
|
123
|
+
path — Tab leaves the field and closes the list before getting there. */
|
|
124
|
+
const browsable = remaining > 0 && onBrowseAll !== undefined;
|
|
125
|
+
const listed = browsable
|
|
126
|
+
? [...visible, { id: BROWSE_ALL, name: copy.picker.browseAll }]
|
|
127
|
+
: visible;
|
|
128
|
+
const closeList = useRef(null);
|
|
129
|
+
const take = (key) => {
|
|
130
|
+
if (key === null)
|
|
131
|
+
return;
|
|
132
|
+
const next = String(key);
|
|
133
|
+
if (next === BROWSE_ALL) {
|
|
134
|
+
/* The full list opens somewhere else (a dialog, a panel): this one closes and the
|
|
135
|
+
field keeps the chosen name. */
|
|
136
|
+
closeList.current?.();
|
|
137
|
+
setText(chosenName);
|
|
138
|
+
setQuery("");
|
|
139
|
+
onBrowseAll?.();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
setText(items.find((i) => i.id === next)?.name ?? "");
|
|
143
|
+
setQuery("");
|
|
144
|
+
if (next !== value)
|
|
145
|
+
onChange?.(next);
|
|
146
|
+
};
|
|
147
|
+
return (_jsxs(AriaComboBox, { id: id, "aria-labelledby": labelledBy, "aria-label": ariaLabel, "aria-describedby": describedBy, items: listed, value: value ?? null, onChange: take, inputValue: text, onInputChange: (next) => {
|
|
148
|
+
setText(next);
|
|
149
|
+
setQuery(next);
|
|
150
|
+
}, onOpenChange: (isOpen, trigger) => {
|
|
151
|
+
setOpen(isOpen);
|
|
152
|
+
if (isOpen && trigger !== "input")
|
|
153
|
+
setQuery("");
|
|
154
|
+
if (!isOpen) {
|
|
155
|
+
setText(chosenName);
|
|
156
|
+
setQuery("");
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
/* Opening on focus is what a many2one does: you arrive at the field to choose, and
|
|
160
|
+
the options are the answer before the first letter. */
|
|
161
|
+
menuTrigger: "focus",
|
|
162
|
+
/* An empty list stays open to SAY it is empty (or loading) instead of vanishing. */
|
|
163
|
+
allowsEmptyCollection: true, isInvalid: invalid, isDisabled: disabled, className: "w-full min-w-0", children: [_jsx(CloseBridge, { into: closeList }), _jsxs(FieldGroup, { children: [_jsx(Input, { placeholder: loading ? shownLoading : open ? shownSearch : shownPlaceholder, title: shownSearch,
|
|
164
|
+
/* Arriving selects the name, so the first letter typed replaces it. Only when
|
|
165
|
+
ARRIVING: React Aria hands the focus back to the input while the list is open,
|
|
166
|
+
and selecting again then would swallow every second letter. */
|
|
167
|
+
onFocus: (event) => {
|
|
168
|
+
if (!open)
|
|
169
|
+
event.currentTarget.select();
|
|
170
|
+
}, className: "h-full min-w-0 flex-1 bg-transparent px-2.5 text-meta text-ink outline-0 placeholder:text-muted disabled:cursor-not-allowed" }), value !== undefined && value !== "" && onOpen !== undefined ? (_jsx(IconButton, { icon: SquareArrowOutUpRight, label: copy.picker.open, size: "sm", disabled: disabled, onClick: () => onOpen(value) })) : null, _jsx(FieldButton, { className: "mr-1", children: _jsx(ChevronDown, { "aria-hidden": true, className: "size-(--size-icon-md)", strokeWidth: 2 }) })] }), _jsxs(Popover, { placement: "bottom start", className: "flex w-(--trigger-width) flex-col overflow-hidden", children: [_jsx(PickerOptions, { items: listed, marks: hasMarks, hints: hasHints, remaining: remaining, emptyText: loading ? shownLoading : shownEmpty,
|
|
171
|
+
/* Not while the first page loads: the sentinel of an empty list is in view and
|
|
172
|
+
would ask for page two before page one arrived. */
|
|
173
|
+
onLoadMore: loading ? undefined : onLoadMore, loadingMore: loadingMore }), remaining > 0 && !browsable ? (_jsxs("div", { className: "flex shrink-0 items-center gap-2 border-t border-line bg-ground px-2.5 py-1.5", children: [_jsx("span", { className: "text-micro tabular-nums text-muted", children: fillCopy(copy.picker.more, { count: remaining.toLocaleString(copy.locale) }) }), _jsx("span", { className: "text-micro text-muted", children: copy.picker.refine })] })) : null] })] }));
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* The list of a `Picker`: its rows, the section headers, the «see all…» row and the loading
|
|
177
|
+
* row at the end. Internal — not exported from the family — and apart from the combobox so it
|
|
178
|
+
* can be rendered on the server without a popover, which is how the tests see the rows.
|
|
179
|
+
*/
|
|
180
|
+
export function PickerOptions({ items, marks, hints, remaining, emptyText, onLoadMore, loadingMore = false, "aria-label": ariaLabel, }) {
|
|
181
|
+
const copy = useUiCopy();
|
|
182
|
+
const entries = useMemo(() => sectioned(items), [items]);
|
|
183
|
+
const renderItem = (item) => item.id === BROWSE_ALL ? (_jsx(ListBoxItem, { id: BROWSE_ALL, textValue: item.name, className: (state) => `${option({ ...state, hints: false })} mt-1 border-t border-line text-primary`, children: _jsxs("span", { className: "flex w-full items-center justify-between gap-2", children: [_jsx("span", { className: "font-medium", children: item.name }), _jsx("span", { className: "text-micro tabular-nums text-muted", children: fillCopy(copy.picker.more, {
|
|
184
|
+
count: remaining.toLocaleString(copy.locale),
|
|
185
|
+
}) })] }) })) : (_jsx(ListBoxItem, { id: item.id, textValue: item.name, isDisabled: item.disabled, className: (state) => option({ ...state, hints }), children: ({ isSelected }) => (_jsx(Row, { item: item, selected: isSelected, marks: marks, hints: hints })) }));
|
|
186
|
+
return (_jsxs(ListBox, { "aria-label": ariaLabel,
|
|
187
|
+
/* Nothing matched YET while the next page is coming: «nothing matches» above «loading
|
|
188
|
+
more…» says two contradicting things, so the empty list says only the second. */
|
|
189
|
+
renderEmptyState: () => (_jsx("p", { className: "px-3 py-3 text-center text-meta text-muted", children: loadingMore ? copy.picker.loadingMore : emptyText })),
|
|
190
|
+
/* Incremental, the list is not capped: it scrolls inside its own height and the
|
|
191
|
+
sentinel at its end is what asks for more. */
|
|
192
|
+
className: cn("min-h-0 flex-1 overflow-y-auto p-1 outline-0", onLoadMore === undefined ? "" : "max-h-(--size-dropdown)"), children: [_jsx(Collection, { items: entries, children: (entry) => "items" in entry ? (_jsxs(ListBoxSection, { id: entry.id, className: "not-first:mt-1 not-first:border-t not-first:border-line not-first:pt-1", children: [_jsxs(Header, { className: "flex items-center gap-2 px-2 pt-1 pb-0.5 text-micro font-medium text-muted", children: [marks ? (_jsx("span", { className: "size-(--size-icon-sm) shrink-0", "aria-hidden": true })) : null, entry.section] }), _jsx(Collection, { items: entry.items, children: renderItem })] })) : (renderItem(entry)) }), onLoadMore === undefined ? null : (_jsx(ListBoxLoadMoreItem, { onLoadMore: onLoadMore, isLoading: loadingMore && items.length > 0, className: "px-3 py-1.5 text-center text-micro text-muted", children: copy.picker.loadingMore }))] }));
|
|
193
|
+
}
|
|
194
|
+
function Row({ item, selected, marks, hints, }) {
|
|
195
|
+
const Icon = item.icon;
|
|
196
|
+
return (_jsxs(_Fragment, { children: [marks ? (_jsx("span", { className: cn(hints ? "mt-0.5" : "", "flex size-(--size-icon-sm) shrink-0 items-center justify-center"), children: selected ? (_jsx(Check, { "aria-hidden": true, className: "size-(--size-icon-sm) text-primary forced-colors:text-[HighlightText]", strokeWidth: 2.5 })) : Icon !== undefined ? (_jsx(Icon, { "aria-hidden": true, className: "size-(--size-icon-sm) text-muted", strokeWidth: 1.75 })) : null })) : null, _jsxs("span", { className: "flex min-w-0 flex-1 flex-col gap-0.5", children: [_jsxs("span", { className: "flex min-w-0 items-center gap-1.5", children: [_jsx(Text, { slot: "label", className: cn("min-w-0 truncate text-meta text-ink", selected ? "font-medium" : ""), children: item.name }), item.tags] }), item.meta === undefined ? null : (_jsx(Text, { slot: "description", className: "truncate text-micro tabular-nums text-muted", children: item.meta }))] }), !marks && selected ? (_jsx(Check, { "aria-hidden": true, className: "size-(--size-icon-sm) shrink-0 text-primary forced-colors:text-[HighlightText]", strokeWidth: 2.5 })) : null] }));
|
|
197
|
+
}
|
|
198
|
+
/** Hands the combobox's `close` to the code outside it: React Aria owns the open state and
|
|
199
|
+
* offers no controlled `isOpen`, and the «see all…» row has to close the list it lives in. */
|
|
200
|
+
function CloseBridge({ into }) {
|
|
201
|
+
const state = useContext(ComboBoxStateContext);
|
|
202
|
+
into.current = state === null ? null : () => state.close();
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Picker, type PickerItem, type PickerProps, } from "./Picker.js";
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export interface PeekProps {
|
|
3
|
+
open: boolean;
|
|
4
|
+
title: ReactNode;
|
|
5
|
+
subtitle?: ReactNode;
|
|
6
|
+
/** The × button and Escape (while the focus is inside the drawer). */
|
|
7
|
+
onClose: () => void;
|
|
8
|
+
/** Promoting preserves the context: it is the same view changing surface. */
|
|
9
|
+
onPromote?: () => void;
|
|
10
|
+
/** The actions of the record being previewed, next to «open as panel». */
|
|
11
|
+
actions?: ReactNode;
|
|
12
|
+
children: ReactNode;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The drawer that previews a record next to the list it came from.
|
|
16
|
+
*
|
|
17
|
+
* Context: the drawer comes in from the right and does NOT dim what is behind
|
|
18
|
+
* it — that is the property that defines it. The list it came from stays
|
|
19
|
+
* clickable, which is the whole point of a preview; a scrim would turn it into
|
|
20
|
+
* a dialog, and a dialog is for what has to be answered before moving on.
|
|
21
|
+
* Pressing another row replaces what the drawer shows; it never opens a second one.
|
|
22
|
+
*
|
|
23
|
+
* It is anchored to the work area and not to the window: it covers the content
|
|
24
|
+
* without covering the menu or the activity bar, which are still navigation.
|
|
25
|
+
* The left corners are the panel radius. The sheet still spans the work area;
|
|
26
|
+
* only the cut against the list is rounded, so the drawer reads as a panel.
|
|
27
|
+
*
|
|
28
|
+
* ── On a phone it is a bottom sheet ───────────────────────────────────
|
|
29
|
+
*
|
|
30
|
+
* Below `sm` (40rem, `--container-handheld`) there is no «beside the list»: 30rem from
|
|
31
|
+
* the right of a 390 px screen is the whole screen. So the same drawer anchors to the
|
|
32
|
+
* BOTTOM edge of the window — full width, top corners rounded, at most `--size-sheet`
|
|
33
|
+
* (90dvh) tall, its body scrolling inside — and comes in from below. The strip left on
|
|
34
|
+
* top still shows the list, and it is still not dimmed: what defines the drawer survives
|
|
35
|
+
* the change of edge. It is `fixed` there and not `absolute` because on a phone the work
|
|
36
|
+
* area is as tall as its content, and its bottom edge can be three screens down; the
|
|
37
|
+
* screen's bottom edge is where the thumb is. (Like any `fixed`, it is anchored to the
|
|
38
|
+
* window only if no ancestor has a `transform` or containment — mount it next to the
|
|
39
|
+
* list, not inside a `@container`.) CSS only: the focus, Escape and restore below do not
|
|
40
|
+
* change.
|
|
41
|
+
*
|
|
42
|
+
* ── Why it is not React Aria's `ModalOverlay` ─────────────────────────
|
|
43
|
+
*
|
|
44
|
+
* Every overlay React Aria has —`ModalOverlay`, `Sheet`— is modal: it makes the rest of the
|
|
45
|
+
* page `inert`, traps the focus and portals to the window. Each of those breaks a promise
|
|
46
|
+
* above: the list would stop answering, and the drawer would cover the menu. So the drawer
|
|
47
|
+
* is an `<aside>` in the work area (the «preview» landmark) holding a NON-modal React Aria
|
|
48
|
+
* `Dialog`:
|
|
49
|
+
*
|
|
50
|
+
* - `Dialog` — `role="dialog"` without `aria-modal`, named by its title (`Heading
|
|
51
|
+
* slot="title"`), and on open it takes the focus itself — else a keyboard user would have
|
|
52
|
+
* to tab through the whole list to reach it. Focusing the panel and not its first button
|
|
53
|
+
* keeps the close tooltip from popping up on every open.
|
|
54
|
+
* - `FocusScope restoreFocus` — on close the focus goes back to the row that opened it.
|
|
55
|
+
* There is no `contain`: Tab may walk out to the list, which is still live.
|
|
56
|
+
* - `useKeyboard` — Escape closes it while the focus is INSIDE the drawer. A listener on
|
|
57
|
+
* the window closed it from anywhere, competing with every other Escape on the page (a
|
|
58
|
+
* search field clearing, a list dropping its selection).
|
|
59
|
+
*/
|
|
60
|
+
export declare function Peek({ open, title, subtitle, onClose, onPromote, actions, children, }: PeekProps): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useUiCopy } from "../../foundations/copy.js";
|
|
4
|
+
import { DensityProvider } from "../../foundations/density.js";
|
|
5
|
+
import { cn } from "../../foundations/variants.js";
|
|
6
|
+
import { Button, IconButton } from "../button/Button.js";
|
|
7
|
+
import { Tag } from "../tag/Tag.js";
|
|
8
|
+
import { SquareArrowOutUpRight, X } from "lucide-react";
|
|
9
|
+
import { FocusScope, useKeyboard } from "react-aria";
|
|
10
|
+
import { Dialog, Heading } from "react-aria-components";
|
|
11
|
+
/**
|
|
12
|
+
* The drawer that previews a record next to the list it came from.
|
|
13
|
+
*
|
|
14
|
+
* Context: the drawer comes in from the right and does NOT dim what is behind
|
|
15
|
+
* it — that is the property that defines it. The list it came from stays
|
|
16
|
+
* clickable, which is the whole point of a preview; a scrim would turn it into
|
|
17
|
+
* a dialog, and a dialog is for what has to be answered before moving on.
|
|
18
|
+
* Pressing another row replaces what the drawer shows; it never opens a second one.
|
|
19
|
+
*
|
|
20
|
+
* It is anchored to the work area and not to the window: it covers the content
|
|
21
|
+
* without covering the menu or the activity bar, which are still navigation.
|
|
22
|
+
* The left corners are the panel radius. The sheet still spans the work area;
|
|
23
|
+
* only the cut against the list is rounded, so the drawer reads as a panel.
|
|
24
|
+
*
|
|
25
|
+
* ── On a phone it is a bottom sheet ───────────────────────────────────
|
|
26
|
+
*
|
|
27
|
+
* Below `sm` (40rem, `--container-handheld`) there is no «beside the list»: 30rem from
|
|
28
|
+
* the right of a 390 px screen is the whole screen. So the same drawer anchors to the
|
|
29
|
+
* BOTTOM edge of the window — full width, top corners rounded, at most `--size-sheet`
|
|
30
|
+
* (90dvh) tall, its body scrolling inside — and comes in from below. The strip left on
|
|
31
|
+
* top still shows the list, and it is still not dimmed: what defines the drawer survives
|
|
32
|
+
* the change of edge. It is `fixed` there and not `absolute` because on a phone the work
|
|
33
|
+
* area is as tall as its content, and its bottom edge can be three screens down; the
|
|
34
|
+
* screen's bottom edge is where the thumb is. (Like any `fixed`, it is anchored to the
|
|
35
|
+
* window only if no ancestor has a `transform` or containment — mount it next to the
|
|
36
|
+
* list, not inside a `@container`.) CSS only: the focus, Escape and restore below do not
|
|
37
|
+
* change.
|
|
38
|
+
*
|
|
39
|
+
* ── Why it is not React Aria's `ModalOverlay` ─────────────────────────
|
|
40
|
+
*
|
|
41
|
+
* Every overlay React Aria has —`ModalOverlay`, `Sheet`— is modal: it makes the rest of the
|
|
42
|
+
* page `inert`, traps the focus and portals to the window. Each of those breaks a promise
|
|
43
|
+
* above: the list would stop answering, and the drawer would cover the menu. So the drawer
|
|
44
|
+
* is an `<aside>` in the work area (the «preview» landmark) holding a NON-modal React Aria
|
|
45
|
+
* `Dialog`:
|
|
46
|
+
*
|
|
47
|
+
* - `Dialog` — `role="dialog"` without `aria-modal`, named by its title (`Heading
|
|
48
|
+
* slot="title"`), and on open it takes the focus itself — else a keyboard user would have
|
|
49
|
+
* to tab through the whole list to reach it. Focusing the panel and not its first button
|
|
50
|
+
* keeps the close tooltip from popping up on every open.
|
|
51
|
+
* - `FocusScope restoreFocus` — on close the focus goes back to the row that opened it.
|
|
52
|
+
* There is no `contain`: Tab may walk out to the list, which is still live.
|
|
53
|
+
* - `useKeyboard` — Escape closes it while the focus is INSIDE the drawer. A listener on
|
|
54
|
+
* the window closed it from anywhere, competing with every other Escape on the page (a
|
|
55
|
+
* search field clearing, a list dropping its selection).
|
|
56
|
+
*/
|
|
57
|
+
export function Peek({ open, title, subtitle, onClose, onPromote, actions, children, }) {
|
|
58
|
+
const copy = useUiCopy();
|
|
59
|
+
const { keyboardProps } = useKeyboard({
|
|
60
|
+
onKeyDown: (e) => {
|
|
61
|
+
if (e.key === "Escape")
|
|
62
|
+
onClose();
|
|
63
|
+
else
|
|
64
|
+
e.continuePropagation();
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
if (!open)
|
|
68
|
+
return null;
|
|
69
|
+
const hasActions = onPromote !== undefined || actions !== undefined;
|
|
70
|
+
return (_jsx(FocusScope, { restoreFocus: true, children: _jsx("aside", { ...keyboardProps, "aria-label": copy.shell.preview, className: "peek-width absolute top-0 right-0 bottom-0 z-30 overflow-hidden rounded-l-panel border-l border-line-strong bg-surface shadow-window transition-[opacity,translate] duration-(--duration-panel) ease-(--ease-out) starting:translate-x-4 starting:opacity-0 max-sm:fixed max-sm:top-auto max-sm:left-0 max-sm:max-h-(--size-sheet) max-sm:w-full max-sm:rounded-tr-panel max-sm:rounded-bl-none max-sm:border-t max-sm:border-l-0 max-sm:starting:translate-x-0 max-sm:starting:translate-y-4", children: _jsxs(Dialog, { className: "flex h-full max-h-[inherit] flex-col outline-0", children: [_jsxs("header", { className: "shrink-0 border-b border-line", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-2 px-3 py-2", children: [_jsx(Tag, { tone: "info", children: copy.shell.previewTag }), _jsx(Heading, { slot: "title", level: 2, className: "min-w-0 flex-1 truncate text-meta font-medium text-ink", children: title }), _jsx(IconButton, { icon: X, label: copy.shell.closePreview, onClick: onClose })] }), subtitle === undefined ? null : (_jsx("p", { className: "truncate px-3 pb-2 text-micro tabular-nums text-muted", children: subtitle }))] }), hasActions ? (_jsxs("div", { className: "flex min-w-0 shrink-0 flex-wrap items-center gap-1.5 border-b border-line px-3 py-1.5 max-sm:order-last max-sm:border-t max-sm:border-b-0 max-sm:pt-2 max-sm:safe-bottom", children: [onPromote === undefined ? null : (_jsx(Button, { role: "secondary", size: "sm", icon: SquareArrowOutUpRight, onClick: onPromote, children: copy.shell.openAsPanel })), actions] })) : null, _jsx("div", { className: cn("min-h-0 flex-1 overflow-y-auto px-3 py-3", !hasActions && "max-sm:safe-bottom"), children: _jsx(DensityProvider, { value: "compact", children: children }) })] }) }) }));
|
|
71
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Peek, type PeekProps } from "./Peek.js";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* A cell's value with its copy button — the table's version of a field's (see
|
|
4
|
+
* `primitives/field/copy.tsx`): the number of an invoice, a customer's name, an amount, copied
|
|
5
|
+
* without opening the row or selecting by hand.
|
|
6
|
+
*
|
|
7
|
+
* Context: the button appears where the pointer or the focus is (on a touch screen it is
|
|
8
|
+
* always there) and always takes its room, so nothing in the row moves when it shows. In a
|
|
9
|
+
* numeric cell it goes BEFORE the figure, so the figures stay aligned on the right edge —
|
|
10
|
+
* which is what makes a column of amounts comparable. It copies what the cell shows
|
|
11
|
+
* («Bs. 4.132,50», not `4132.5`) unless the column says what to copy.
|
|
12
|
+
*/
|
|
13
|
+
export declare function CellCopy({ children, header, numeric, value, }: {
|
|
14
|
+
children: ReactNode;
|
|
15
|
+
/** The column's name, for the button's: «Copiar «Cliente»». */
|
|
16
|
+
header: string;
|
|
17
|
+
numeric?: boolean;
|
|
18
|
+
/** What to copy instead of what is seen. */
|
|
19
|
+
value?: string;
|
|
20
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { fillCopy, useUiCopy } from "../../foundations/copy.js";
|
|
4
|
+
import { shownText } from "../field/copy.js";
|
|
5
|
+
import { CopyButton } from "../value/Copyable.js";
|
|
6
|
+
import { useRef } from "react";
|
|
7
|
+
/**
|
|
8
|
+
* A cell's value with its copy button — the table's version of a field's (see
|
|
9
|
+
* `primitives/field/copy.tsx`): the number of an invoice, a customer's name, an amount, copied
|
|
10
|
+
* without opening the row or selecting by hand.
|
|
11
|
+
*
|
|
12
|
+
* Context: the button appears where the pointer or the focus is (on a touch screen it is
|
|
13
|
+
* always there) and always takes its room, so nothing in the row moves when it shows. In a
|
|
14
|
+
* numeric cell it goes BEFORE the figure, so the figures stay aligned on the right edge —
|
|
15
|
+
* which is what makes a column of amounts comparable. It copies what the cell shows
|
|
16
|
+
* («Bs. 4.132,50», not `4132.5`) unless the column says what to copy.
|
|
17
|
+
*/
|
|
18
|
+
export function CellCopy({ children, header, numeric = false, value, }) {
|
|
19
|
+
const ui = useUiCopy();
|
|
20
|
+
const shown = useRef(null);
|
|
21
|
+
const button = (_jsx("span", { "data-copy-skip": "", className: "inline-flex shrink-0 opacity-0 transition-opacity duration-(--duration-instant) group-hover/cell:opacity-100 focus-within:opacity-100 pointer-coarse:opacity-100", children: _jsx(CopyButton, { label: fillCopy(ui.copyable.field, { label: header }), value: () => value ?? (shown.current === null ? "" : shownText(shown.current)) }) }));
|
|
22
|
+
return (_jsxs("span", { className: `group/cell flex w-full min-w-0 items-center gap-1 ${numeric ? "justify-end" : ""}`, children: [numeric ? button : null, _jsx("span", { ref: shown, className: "min-w-0", children: children }), numeric ? null : button] }));
|
|
23
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* One cell of a `DataTable` that edits in place: its drawing becomes a button that opens
|
|
4
|
+
* `editor` in a popover anchored to the cell.
|
|
5
|
+
*
|
|
6
|
+
* Internal to the table family: a column asks for it with `DataColumn.edit`.
|
|
7
|
+
*/
|
|
8
|
+
export declare function CellEditor({ label, numeric, editor, children, }: {
|
|
9
|
+
/** The verb and the column: «Editar vencimiento». Names the button and the popover. */
|
|
10
|
+
label: string;
|
|
11
|
+
numeric: boolean;
|
|
12
|
+
editor: (done: () => void) => ReactNode;
|
|
13
|
+
children: ReactNode;
|
|
14
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { cn, focusRing } from "../../foundations/variants.js";
|
|
4
|
+
import { Popover } from "../overlay/Popover.js";
|
|
5
|
+
import { Pencil } from "lucide-react";
|
|
6
|
+
import { useEffect, useId, useRef } from "react";
|
|
7
|
+
import { Button as AriaButton, Dialog, DialogTrigger } from "react-aria-components";
|
|
8
|
+
/* ==================================================================== *
|
|
9
|
+
* CELL EDITOR — change one value of a list without leaving the list.
|
|
10
|
+
*
|
|
11
|
+
* Context: the salesperson of an invoice, its due date, its stage. Opening the record to
|
|
12
|
+
* change one field and coming back is three screens for one value, and the person loses
|
|
13
|
+
* the row they were on. Odoo answers it with the editable list; this is the step before it:
|
|
14
|
+
* the list stays a list, and ONE cell opens a small popover with the right control —
|
|
15
|
+
* a `Picker`, a `Calendar`, a `Select` — anchored to the cell, the way a date picker opens
|
|
16
|
+
* its calendar under its field.
|
|
17
|
+
*
|
|
18
|
+
* ── The cell IS the button ──────────────────────────────────────────
|
|
19
|
+
*
|
|
20
|
+
* The whole drawing of the cell is the trigger, not a pencil on its side: the target is
|
|
21
|
+
* the value the eye is already on (Fitts), and a column of twelve pencils is noise. The
|
|
22
|
+
* pencil only appears where the pointer or the keyboard is — on hover and on visible
|
|
23
|
+
* focus — the same rule `RowActions` follows: an affordance without a subject yet is not
|
|
24
|
+
* shown. That is also why an editable column must not draw links: they would be a button
|
|
25
|
+
* inside a button.
|
|
26
|
+
*
|
|
27
|
+
* ── React Aria's dialog trigger ─────────────────────────────────────
|
|
28
|
+
*
|
|
29
|
+
* `DialogTrigger` + `Popover` + `Dialog`: Escape and a click outside close it, and the
|
|
30
|
+
* focus goes back to the cell it came from, so the keyboard continues down the column.
|
|
31
|
+
* What commits is the editor's own gesture (choosing a person, pressing a day), and then
|
|
32
|
+
* it calls `done`. Closing without that gesture changes nothing — Escape is «cancel».
|
|
33
|
+
*
|
|
34
|
+
* The button's name is the verb plus the value («Editar vencimiento 15/11/2026»): the
|
|
35
|
+
* value alone does not say it can be changed, the verb alone does not say what it is.
|
|
36
|
+
* ==================================================================== */
|
|
37
|
+
/** The first control of the editor gets the focus, unless the editor already placed it
|
|
38
|
+
* (a `Calendar` with `autoFocus`). Arriving in a popover with the focus on its frame is one
|
|
39
|
+
* extra Tab before the first key does anything. */
|
|
40
|
+
function useFocusFirst(scope) {
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
const frame = requestAnimationFrame(() => {
|
|
43
|
+
const root = scope.current;
|
|
44
|
+
if (root === null)
|
|
45
|
+
return;
|
|
46
|
+
const active = document.activeElement;
|
|
47
|
+
if (active !== null && active !== root && root.contains(active))
|
|
48
|
+
return;
|
|
49
|
+
for (const selector of [
|
|
50
|
+
"input:not([disabled])",
|
|
51
|
+
/* A calendar: the day the keyboard is on, not the «previous month» arrow. */
|
|
52
|
+
'[role="grid"] [tabindex="0"]',
|
|
53
|
+
"button:not([disabled])",
|
|
54
|
+
'[tabindex="0"]',
|
|
55
|
+
]) {
|
|
56
|
+
const target = root.querySelector(selector);
|
|
57
|
+
if (target !== null) {
|
|
58
|
+
target.focus();
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
return () => cancelAnimationFrame(frame);
|
|
64
|
+
}, [scope]);
|
|
65
|
+
}
|
|
66
|
+
function EditorDialog({ label, editor, }) {
|
|
67
|
+
const ref = useRef(null);
|
|
68
|
+
useFocusFirst(ref);
|
|
69
|
+
return (_jsx(Dialog, { ref: ref, "aria-label": label, className: "max-h-[inherit] max-w-sm min-w-56 overflow-auto p-2 outline-0", children: ({ close }) => editor(close) }));
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* One cell of a `DataTable` that edits in place: its drawing becomes a button that opens
|
|
73
|
+
* `editor` in a popover anchored to the cell.
|
|
74
|
+
*
|
|
75
|
+
* Internal to the table family: a column asks for it with `DataColumn.edit`.
|
|
76
|
+
*/
|
|
77
|
+
export function CellEditor({ label, numeric, editor, children, }) {
|
|
78
|
+
const verb = useId();
|
|
79
|
+
const shown = useId();
|
|
80
|
+
return (
|
|
81
|
+
/* The row may open its record on click; changing one cell must not also open it. */
|
|
82
|
+
_jsx("div", { onClick: (event) => event.stopPropagation(), className: "-mx-1 min-w-0", children: _jsxs(DialogTrigger, { children: [_jsxs(AriaButton, { "aria-labelledby": `${verb} ${shown}`, className: ({ isFocusVisible }) => cn("group/edit flex w-full min-w-0 cursor-pointer items-center gap-1.5 rounded-mark px-1 text-inherit outline-0 transition-colors duration-(--duration-instant) hover:bg-ink/6 pressed:bg-ink/10 aria-expanded:bg-primary/8", numeric ? "flex-row-reverse text-right" : "text-left", focusRing({ isFocusVisible })), children: [_jsx("span", { id: verb, hidden: true, children: label }), _jsx("span", { id: shown, className: "min-w-0 flex-1 truncate", children: children }), _jsx(Pencil, { "aria-hidden": true, strokeWidth: 2, className: "size-(--size-icon-sm) shrink-0 text-muted opacity-0 transition-opacity duration-(--duration-instant) group-hover/edit:opacity-100 group-focus-visible/edit:opacity-100 group-aria-expanded/edit:opacity-100" })] }), _jsx(Popover, { placement: numeric ? "bottom end" : "bottom start", children: _jsx(EditorDialog, { label: label, editor: editor }) })] }) }));
|
|
83
|
+
}
|