@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,121 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useUiCopy } from "../../foundations/copy.js";
|
|
4
|
+
import { cn, tv } from "../../foundations/variants.js";
|
|
5
|
+
import { Button, IconButton } from "../button/Button.js";
|
|
6
|
+
import { Maximize2, X } from "lucide-react";
|
|
7
|
+
import { useId } from "react";
|
|
8
|
+
import { Dialog as AriaDialog, Heading, Modal, ModalOverlay } from "react-aria-components";
|
|
9
|
+
/** The scrim. The page stays visible under it — the dialog is ABOUT something on it.
|
|
10
|
+
* On a phone it drops the padding and pushes the window to the bottom edge. */
|
|
11
|
+
const overlay = tv({
|
|
12
|
+
base: "fixed inset-0 z-50 flex items-center justify-center bg-scrim/40 p-4 transition-opacity duration-(--duration-quick) ease-(--ease-out) max-sm:items-end max-sm:p-0 sm:p-8",
|
|
13
|
+
variants: {
|
|
14
|
+
isEntering: { true: "opacity-0" },
|
|
15
|
+
isExiting: { true: "opacity-0" },
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
/** The window. It never grows past the scrim's padding: when the form is long the BODY
|
|
19
|
+
* scrolls and the header (what it is about) and the footer (what to do) stay in view.
|
|
20
|
+
*
|
|
21
|
+
* On a phone it is a bottom sheet: every width is the full width, only the top corners
|
|
22
|
+
* are rounded (the bottom ones sit on the screen's edge), it stops at `--size-sheet`,
|
|
23
|
+
* and it enters from below — the motion says where it lives. */
|
|
24
|
+
const modal = tv({
|
|
25
|
+
base: "flex max-h-full w-full flex-col overflow-hidden rounded-panel border border-line-strong bg-surface text-ink shadow-window outline-0 transition-[opacity,translate] duration-(--duration-panel) ease-(--ease-out) forced-colors:bg-[Canvas] max-sm:max-h-(--size-sheet) max-sm:max-w-none max-sm:rounded-b-none max-sm:border-x-0 max-sm:border-b-0",
|
|
26
|
+
variants: {
|
|
27
|
+
width: { sm: "max-w-md", md: "max-w-2xl", lg: "max-w-4xl" },
|
|
28
|
+
isEntering: { true: "translate-y-2 opacity-0 max-sm:translate-y-full" },
|
|
29
|
+
isExiting: {
|
|
30
|
+
true: "translate-y-1 opacity-0 duration-(--duration-quick) max-sm:translate-y-full",
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
/** The scrim and the window recipes. Internal — exported only so a test can read the
|
|
35
|
+
* classes that make the bottom sheet: the modal is a portal and draws nothing on the
|
|
36
|
+
* server. */
|
|
37
|
+
export const dialogStyles = { overlay, modal };
|
|
38
|
+
/** The inside of the window: React Aria's `Dialog` with the header, body and footer.
|
|
39
|
+
*
|
|
40
|
+
* Internal — exported only so a test can read its markup, which the modal (a portal)
|
|
41
|
+
* does not produce on the server. */
|
|
42
|
+
export function DialogSurface({ title, subtitle, context, onClose, onExpand, expandLabel, footer, children, }) {
|
|
43
|
+
const copy = useUiCopy();
|
|
44
|
+
const subtitleId = useId();
|
|
45
|
+
const expand = expandLabel ?? copy.dialog.expand;
|
|
46
|
+
return (_jsxs(AriaDialog, { "aria-describedby": subtitle === undefined ? undefined : subtitleId, className: "flex max-h-[inherit] min-h-0 flex-1 flex-col outline-0", children: [_jsxs("header", { className: "flex shrink-0 items-start gap-3 border-b border-line px-4 py-3", children: [_jsxs("div", { className: "min-w-0 flex-1", children: [context === undefined ? null : (
|
|
47
|
+
/* The context, on top and small. It is what keeps the first field
|
|
48
|
+
from being «about what?». Same voice as a field label, not a
|
|
49
|
+
console kicker: mono uppercase next to a form title was a
|
|
50
|
+
second chrome fighting the first. */
|
|
51
|
+
_jsxs("p", { className: "mb-0.5 flex min-w-0 flex-wrap items-baseline gap-x-2 gap-y-0.5 text-micro text-muted", children: [context.kind === undefined ? null : (_jsx("span", { className: "font-medium", children: context.kind })), _jsx("span", { className: "min-w-0 truncate text-ink", children: context.label }), context.detail === undefined ? null : (_jsx("span", { className: "min-w-0 truncate tabular-nums", children: context.detail }))] })), _jsx(Heading, { slot: "title", className: "text-title font-semibold text-ink", children: title }), subtitle === undefined ? null : (_jsx("p", { id: subtitleId, className: "mt-1 max-w-prose text-meta text-muted", children: subtitle }))] }), _jsxs("div", { className: "flex shrink-0 items-center gap-1", children: [onExpand === undefined ? null : (_jsx(IconButton, { icon: Maximize2, label: expand, onClick: onExpand })), _jsx(IconButton, { icon: X, label: copy.shell.close, onClick: onClose })] })] }), _jsx("div", { className: cn("min-h-0 flex-1 overflow-y-auto px-4 py-4", footer === undefined && "max-sm:safe-bottom"), children: children }), footer === undefined ? null : (_jsx("footer", { className: "flex shrink-0 flex-wrap items-center justify-end gap-2 border-t border-line bg-ground px-4 py-3 max-sm:safe-bottom", children: footer }))] }));
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* What has to be answered before moving on: a short form that blocks the page and keeps
|
|
55
|
+
* saying what it is about.
|
|
56
|
+
*
|
|
57
|
+
* Context: controlled by `open` + `onClose` — the screen that opens it owns the decision of
|
|
58
|
+
* when it is open, and every way out (×, Escape, the scrim) reports through `onClose`. It
|
|
59
|
+
* stays mounted for the exit transition after `open` turns `false`, so the window fades
|
|
60
|
+
* out instead of vanishing.
|
|
61
|
+
*/
|
|
62
|
+
export function Dialog({ open, width = "md", onClose, ...surface }) {
|
|
63
|
+
return (_jsx(ModalOverlay, { isOpen: open, onOpenChange: (next) => {
|
|
64
|
+
if (!next)
|
|
65
|
+
onClose();
|
|
66
|
+
}, isDismissable: true, className: overlay, children: _jsx(Modal, { className: (state) => modal({ ...state, width }), children: _jsx(DialogSurface, { onClose: onClose, ...surface }) }) }));
|
|
67
|
+
}
|
|
68
|
+
/** A dialog with steps, for what CANNOT be answered in one go.
|
|
69
|
+
*
|
|
70
|
+
* ── When a form deserves to be a wizard ─────────────────────────────
|
|
71
|
+
*
|
|
72
|
+
* Only when **one answer changes the following questions**. If the fields are
|
|
73
|
+
* independent, splitting them into steps simplifies nothing: it adds clicks and
|
|
74
|
+
* hides half the form, and whoever wants to review what they entered has to
|
|
75
|
+
* navigate backwards. A long but complete form is filled in faster than three
|
|
76
|
+
* short steps.
|
|
77
|
+
*
|
|
78
|
+
* The case that does justify it: a purchase order. Whether it is a local purchase
|
|
79
|
+
* or an import changes which conditions exist — an import asks for currency and
|
|
80
|
+
* incoterm, a local one for payment terms. There, step 2 cannot even be drawn
|
|
81
|
+
* until step 1 has been answered.
|
|
82
|
+
*
|
|
83
|
+
* ── The steps already completed can be clicked ──────────────────────
|
|
84
|
+
*
|
|
85
|
+
* Going back is not a mistake: it is reviewing. What you cannot do is jump
|
|
86
|
+
* ahead, because the next step does not know yet what to ask.
|
|
87
|
+
*
|
|
88
|
+
* ── It is the CONTENT of a `Dialog`, not a second one ────────────────
|
|
89
|
+
*
|
|
90
|
+
* The focus trap, Escape and the return of focus are the dialog's. The wizard
|
|
91
|
+
* only moves between steps, so it has no overlay behaviour of its own. */
|
|
92
|
+
export function Wizard({ steps, current, onStepChange, children, onCancel, onBack, onNext, nextLabel, busy = false, }) {
|
|
93
|
+
/* The words of the wizard's own buttons come from the copy table. */
|
|
94
|
+
const words = useUiCopy().dialog;
|
|
95
|
+
const at = steps.findIndex((s) => s.id === current);
|
|
96
|
+
const step = steps[at];
|
|
97
|
+
const last = at === steps.length - 1;
|
|
98
|
+
const blocked = step?.blocked;
|
|
99
|
+
return (_jsxs("div", { className: "flex min-w-0 flex-col gap-3", children: [_jsx("ol", { className: "flex min-w-0 flex-wrap items-center gap-0.5 rounded-box bg-ink/4 p-1", children: steps.map((s, i) => {
|
|
100
|
+
const done = i < at;
|
|
101
|
+
const here = i === at;
|
|
102
|
+
return (_jsx("li", { className: "flex items-center", children: _jsxs("button", { type: "button", disabled: i > at, onClick: () => {
|
|
103
|
+
if (!here)
|
|
104
|
+
onStepChange?.(s.id);
|
|
105
|
+
}, "aria-current": here ? "step" : undefined, "data-state": here ? "current" : done ? "done" : "pending", className: `inline-flex items-center gap-1.5 rounded-control px-2 py-1 text-micro transition-colors duration-(--duration-instant) disabled:pointer-events-none focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-primary ${here
|
|
106
|
+
? "bg-surface font-medium text-ink"
|
|
107
|
+
: done
|
|
108
|
+
? "text-ink hover:bg-surface/70"
|
|
109
|
+
: "text-muted"}`, children: [_jsx("span", { className: `inline-flex size-5 shrink-0 items-center justify-center rounded-pill text-micro tabular-nums ${here
|
|
110
|
+
? "bg-ink text-surface"
|
|
111
|
+
: done
|
|
112
|
+
? "bg-ink/10 text-ink"
|
|
113
|
+
: "bg-ink/8 text-muted"}`, children: i + 1 }), s.label] }) }, s.id));
|
|
114
|
+
}) }), _jsx("div", { className: "min-w-0", children: children }), _jsxs("div", { className: "flex flex-wrap items-center gap-2 border-t border-line pt-3", children: [blocked === undefined ? null : (
|
|
115
|
+
/* The reason, next to the disabled button. A grey «next» with no
|
|
116
|
+
explanation is a dead end: the user tries, nothing happens, and
|
|
117
|
+
there is no way to know what is missing. */
|
|
118
|
+
_jsx("span", { className: "mr-auto min-w-0 text-micro text-warn", children: blocked })), _jsxs("span", { className: blocked === undefined
|
|
119
|
+
? "ml-auto flex items-center gap-2"
|
|
120
|
+
: "flex items-center gap-2", children: [onCancel === undefined ? null : (_jsx(Button, { role: "ghost", onClick: onCancel, children: words.cancel })), at > 0 ? (_jsx(Button, { role: "secondary", onClick: onBack, children: words.back })) : null, _jsx(Button, { role: "primary", onClick: onNext, disabled: blocked !== undefined || busy, "aria-busy": busy || undefined, children: busy ? words.busy : (nextLabel ?? (last ? words.finish : words.next)) })] })] })] }));
|
|
121
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { type DialogContext, type DialogWidth } from "./Dialog.js";
|
|
2
|
+
import { type ReactNode, type RefObject } from "react";
|
|
3
|
+
/** Where the drill was opened from: the page's record. The first crumb of the trail. */
|
|
4
|
+
export type DrillOrigin = DialogContext;
|
|
5
|
+
/** Which contexts are open, in order, and the ways to move between them. */
|
|
6
|
+
export interface DrillState {
|
|
7
|
+
/** The ids of the open contexts, from the first opened to the one in front. */
|
|
8
|
+
readonly path: readonly string[];
|
|
9
|
+
/** One level deeper. An id already on the path goes back to it instead: a product
|
|
10
|
+
* that leads to its supplier that leads to the same product is a loop, not depth. */
|
|
11
|
+
open: (id: string) => void;
|
|
12
|
+
/** Up one level. On the first one, it closes. */
|
|
13
|
+
back: () => void;
|
|
14
|
+
/** Back to the context with this id, closing the ones in front of it. */
|
|
15
|
+
to: (id: string) => void;
|
|
16
|
+
/** Close every level: back to the page. */
|
|
17
|
+
close: () => void;
|
|
18
|
+
/** Internal: the control that opened each level, where focus returns when the level
|
|
19
|
+
* above it closes. Written by `open`, read by `Drill`. */
|
|
20
|
+
readonly focusMarks: RefObject<(HTMLElement | null)[]>;
|
|
21
|
+
}
|
|
22
|
+
/** One level deeper: push the id — or, when it is already open, go back to it. Internal,
|
|
23
|
+
* exported for the test. */
|
|
24
|
+
export declare function openPath(path: readonly string[], id: string): readonly string[];
|
|
25
|
+
/** Back to the context with this id; an id not on the path changes nothing. */
|
|
26
|
+
export declare function toPath(path: readonly string[], id: string): readonly string[];
|
|
27
|
+
/** The written contexts that are open, in the order of the path. A context on the path
|
|
28
|
+
* that is not written is skipped: there is nothing to draw for it. */
|
|
29
|
+
export declare function framesOf(children: ReactNode, path: readonly string[]): DrillContextProps[];
|
|
30
|
+
/** The state of a `Drill`: the path of open contexts. Pass it to `<Drill state={…}>` and
|
|
31
|
+
* call `open(id)` from wherever the context is opened — a relation's «Abrir», a
|
|
32
|
+
* `Picker`'s open button, a rail action, a line's cell. */
|
|
33
|
+
export declare function useDrill(initial?: readonly string[]): DrillState;
|
|
34
|
+
export interface DrillContextProps {
|
|
35
|
+
/** What `open(id)` names. Unique inside the drill. */
|
|
36
|
+
id: string;
|
|
37
|
+
/** The context's name: the title while it is in front, its crumb when it is behind.
|
|
38
|
+
* Short — «Línea 3», «Impresora térmica 80 mm», «Registrar pago». */
|
|
39
|
+
title: string;
|
|
40
|
+
/** One sentence under the title; also the dialog's description while in front. */
|
|
41
|
+
subtitle?: string;
|
|
42
|
+
/** This context's actions — Cancel and ONE primary. Drawn only while it is in front. */
|
|
43
|
+
footer?: ReactNode;
|
|
44
|
+
/** No padding of its own: for a child that is already a surface with its own margins
|
|
45
|
+
* (a `Record`). A form or a wizard keeps the padding. */
|
|
46
|
+
flush?: boolean;
|
|
47
|
+
children: ReactNode;
|
|
48
|
+
}
|
|
49
|
+
export interface DrillProps {
|
|
50
|
+
state: DrillState;
|
|
51
|
+
/** The page's record, as the first crumb: «pedido · S00142». Pressing it closes. */
|
|
52
|
+
origin?: DrillOrigin;
|
|
53
|
+
/** One width for every level: a window that changes width at each step jumps under the
|
|
54
|
+
* pointer. `lg` by default — a record with pages and lines needs it. */
|
|
55
|
+
width?: DialogWidth;
|
|
56
|
+
/** The `Drill.Context`s. Which ones are open is the state's path. */
|
|
57
|
+
children: ReactNode;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The inside of the window: the trail, the context in front (and the ones behind it,
|
|
61
|
+
* hidden), and the footer of the one in front.
|
|
62
|
+
*
|
|
63
|
+
* Internal — exported only so a test can read its markup, which the modal (a portal)
|
|
64
|
+
* does not produce on the server.
|
|
65
|
+
*/
|
|
66
|
+
export declare function DrillSurface({ frames, origin, onBack, onTo, onClose, focusMarks, }: {
|
|
67
|
+
frames: readonly DrillContextProps[];
|
|
68
|
+
origin?: DrillOrigin;
|
|
69
|
+
onBack: () => void;
|
|
70
|
+
onTo: (id: string) => void;
|
|
71
|
+
onClose: () => void;
|
|
72
|
+
focusMarks?: RefObject<(HTMLElement | null)[]>;
|
|
73
|
+
}): import("react").JSX.Element | null;
|
|
74
|
+
/**
|
|
75
|
+
* ONE dialog that holds a stack of contexts opened from a record — and from each other —
|
|
76
|
+
* with a trail back to the page. See the header of this file.
|
|
77
|
+
*
|
|
78
|
+
* const drill = useDrill();
|
|
79
|
+
* …<RelationValue name={product} onOpen={() => drill.open("product")} />…
|
|
80
|
+
* <Drill state={drill} origin={{ kind: "pedido", label: "S00142" }}>
|
|
81
|
+
* <Drill.Context id="product" title={product} flush>
|
|
82
|
+
* <Record identity={…}>…</Record>
|
|
83
|
+
* </Drill.Context>
|
|
84
|
+
* <Drill.Context id="supplier" title={supplier} footer={…}>…</Drill.Context>
|
|
85
|
+
* </Drill>
|
|
86
|
+
*/
|
|
87
|
+
export declare function Drill({ state, origin, width, children }: DrillProps): import("react").JSX.Element;
|
|
88
|
+
export declare namespace Drill {
|
|
89
|
+
var Context: ((props: DrillContextProps) => ReactNode) & import("../../foundations/slots.js").Slotted<"context">;
|
|
90
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { fillCopy, useUiCopy } from "../../foundations/copy.js";
|
|
4
|
+
import { flatten, propsOf, slot, slotOf } from "../../foundations/slots.js";
|
|
5
|
+
import { cn, focusRing, tv } from "../../foundations/variants.js";
|
|
6
|
+
import { IconButton } from "../button/Button.js";
|
|
7
|
+
import { dialogStyles, } from "./Dialog.js";
|
|
8
|
+
import { ArrowLeft, ChevronRight, X } from "lucide-react";
|
|
9
|
+
import { useEffect, useId, useRef, useState } from "react";
|
|
10
|
+
import { useKeyboard } from "react-aria";
|
|
11
|
+
import { Dialog as AriaDialog, Heading, Link, Modal, ModalOverlay, } from "react-aria-components";
|
|
12
|
+
function focused() {
|
|
13
|
+
if (typeof document === "undefined")
|
|
14
|
+
return null;
|
|
15
|
+
return document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
16
|
+
}
|
|
17
|
+
/** One level deeper: push the id — or, when it is already open, go back to it. Internal,
|
|
18
|
+
* exported for the test. */
|
|
19
|
+
export function openPath(path, id) {
|
|
20
|
+
const at = path.indexOf(id);
|
|
21
|
+
return at === -1 ? [...path, id] : path.slice(0, at + 1);
|
|
22
|
+
}
|
|
23
|
+
/** Back to the context with this id; an id not on the path changes nothing. */
|
|
24
|
+
export function toPath(path, id) {
|
|
25
|
+
const at = path.indexOf(id);
|
|
26
|
+
return at === -1 ? path : path.slice(0, at + 1);
|
|
27
|
+
}
|
|
28
|
+
/** The written contexts that are open, in the order of the path. A context on the path
|
|
29
|
+
* that is not written is skipped: there is nothing to draw for it. */
|
|
30
|
+
export function framesOf(children, path) {
|
|
31
|
+
const written = flatten(children)
|
|
32
|
+
.filter((node) => slotOf(node) === "context")
|
|
33
|
+
.map((node) => propsOf(node))
|
|
34
|
+
.filter((props) => props !== undefined);
|
|
35
|
+
return path
|
|
36
|
+
.map((id) => written.find((context) => context.id === id))
|
|
37
|
+
.filter((frame) => frame !== undefined);
|
|
38
|
+
}
|
|
39
|
+
/** The state of a `Drill`: the path of open contexts. Pass it to `<Drill state={…}>` and
|
|
40
|
+
* call `open(id)` from wherever the context is opened — a relation's «Abrir», a
|
|
41
|
+
* `Picker`'s open button, a rail action, a line's cell. */
|
|
42
|
+
export function useDrill(initial = []) {
|
|
43
|
+
const [path, setPath] = useState(initial);
|
|
44
|
+
const focusMarks = useRef([]);
|
|
45
|
+
return {
|
|
46
|
+
path,
|
|
47
|
+
open: (id) => {
|
|
48
|
+
if (!path.includes(id))
|
|
49
|
+
focusMarks.current[path.length] = focused();
|
|
50
|
+
setPath(openPath(path, id));
|
|
51
|
+
},
|
|
52
|
+
back: () => setPath(path.slice(0, -1)),
|
|
53
|
+
to: (id) => setPath(toPath(path, id)),
|
|
54
|
+
close: () => setPath([]),
|
|
55
|
+
focusMarks,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const crumb = tv({
|
|
59
|
+
extend: focusRing,
|
|
60
|
+
base: "block max-w-(--container-crumb) min-w-0 cursor-pointer truncate rounded-mark text-muted transition-colors duration-(--duration-instant) hover:text-ink hover:underline hover:underline-offset-4",
|
|
61
|
+
});
|
|
62
|
+
/**
|
|
63
|
+
* The inside of the window: the trail, the context in front (and the ones behind it,
|
|
64
|
+
* hidden), and the footer of the one in front.
|
|
65
|
+
*
|
|
66
|
+
* Internal — exported only so a test can read its markup, which the modal (a portal)
|
|
67
|
+
* does not produce on the server.
|
|
68
|
+
*/
|
|
69
|
+
export function DrillSurface({ frames, origin, onBack, onTo, onClose, focusMarks, }) {
|
|
70
|
+
const copy = useUiCopy();
|
|
71
|
+
const depth = frames.length;
|
|
72
|
+
const top = frames[depth - 1];
|
|
73
|
+
const parent = depth > 1 ? frames[depth - 2] : undefined;
|
|
74
|
+
const bodies = useRef(new Map());
|
|
75
|
+
const was = useRef(depth);
|
|
76
|
+
const subtitleId = useId();
|
|
77
|
+
const { keyboardProps } = useKeyboard({
|
|
78
|
+
onKeyDown: (e) => {
|
|
79
|
+
if (e.key !== "Escape") {
|
|
80
|
+
e.continuePropagation();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (depth > 1)
|
|
84
|
+
onBack();
|
|
85
|
+
else
|
|
86
|
+
onClose();
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
/* Focus follows the level. Down: into the new context, so the keyboard does not stay on
|
|
90
|
+
a control that is now hidden. Up: back to what opened the level that closed. */
|
|
91
|
+
const topId = top?.id;
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
const before = was.current;
|
|
94
|
+
was.current = depth;
|
|
95
|
+
if (before === depth || depth === 0 || topId === undefined)
|
|
96
|
+
return;
|
|
97
|
+
const box = bodies.current.get(topId);
|
|
98
|
+
if (depth > before) {
|
|
99
|
+
box?.focus();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const mark = focusMarks?.current[depth];
|
|
103
|
+
(mark?.isConnected === true ? mark : box)?.focus();
|
|
104
|
+
}, [depth, topId, focusMarks]);
|
|
105
|
+
if (top === undefined)
|
|
106
|
+
return null;
|
|
107
|
+
const trail = [
|
|
108
|
+
...(origin === undefined
|
|
109
|
+
? []
|
|
110
|
+
: [
|
|
111
|
+
{
|
|
112
|
+
key: "\u0000origin",
|
|
113
|
+
label: (_jsxs(_Fragment, { children: [origin.kind === undefined ? null : (_jsxs("span", { className: "font-medium", children: [origin.kind, " "] })), _jsx("span", { className: "text-ink", children: origin.label })] })),
|
|
114
|
+
onPress: onClose,
|
|
115
|
+
},
|
|
116
|
+
]),
|
|
117
|
+
...frames.slice(0, -1).map((frame) => ({
|
|
118
|
+
key: frame.id,
|
|
119
|
+
label: frame.title,
|
|
120
|
+
onPress: () => onTo(frame.id),
|
|
121
|
+
})),
|
|
122
|
+
];
|
|
123
|
+
return (_jsx(AriaDialog, { ...(top.subtitle === undefined ? {} : { "aria-describedby": subtitleId }), className: "flex max-h-[inherit] min-h-0 flex-1 flex-col outline-0", children: _jsxs("div", { ...keyboardProps, className: "flex max-h-[inherit] min-h-0 flex-1 flex-col", children: [_jsxs("header", { className: "flex shrink-0 items-start gap-2 border-b border-line px-4 py-3", children: [parent === undefined ? null : (_jsx("span", { className: "-ml-1.5 shrink-0", children: _jsx(IconButton, { icon: ArrowLeft, label: fillCopy(copy.drill.back, { name: parent.title }), onClick: onBack }) })), _jsxs("div", { className: "min-w-0 flex-1", children: [trail.length === 0 ? null : (
|
|
124
|
+
/* Where this was opened from, small and on top — the same place `Dialog`
|
|
125
|
+
says what it is about. Every crumb is a way back; the one in front is
|
|
126
|
+
not a crumb, it is the title. */
|
|
127
|
+
_jsx("nav", { "aria-label": copy.drill.trail, className: "mb-0.5 flex min-w-0", children: _jsx("ol", { className: "flex min-w-0 flex-wrap items-center gap-x-1 gap-y-0.5 text-micro", children: trail.map((one, i) => (_jsxs("li", { className: "flex min-w-0 items-center gap-1", children: [i === 0 ? null : (_jsx(ChevronRight, { className: "size-(--size-icon-sm) shrink-0 text-line-strong", strokeWidth: 2, "aria-hidden": true })), _jsx(Link, { onPress: one.onPress, className: ({ isFocusVisible }) => crumb({ isFocusVisible }), children: one.label })] }, one.key))) }) })), _jsx(Heading, { slot: "title", className: "truncate text-title font-semibold text-ink", children: top.title }), top.subtitle === undefined ? null : (_jsx("p", { id: subtitleId, className: "mt-1 max-w-prose text-meta text-muted", children: top.subtitle }))] }), _jsx(IconButton, { icon: X, label: copy.shell.close, onClick: onClose })] }), frames.map((frame) => {
|
|
128
|
+
const front = frame.id === top.id;
|
|
129
|
+
return (
|
|
130
|
+
/* Each level scrolls on its own and the ones behind are `hidden`, not gone:
|
|
131
|
+
their state — a half-typed payment, the open tab of a product — is still
|
|
132
|
+
there when the level above closes. */
|
|
133
|
+
_jsx("div", { ref: (node) => {
|
|
134
|
+
if (node === null)
|
|
135
|
+
bodies.current.delete(frame.id);
|
|
136
|
+
else
|
|
137
|
+
bodies.current.set(frame.id, node);
|
|
138
|
+
}, hidden: !front, tabIndex: -1, "data-drill-context": frame.id, className: cn("min-h-0 flex-1 overflow-y-auto outline-0", frame.flush !== true && "px-4 py-4", front && frame.footer === undefined && "max-sm:safe-bottom"), children: frame.children }, frame.id));
|
|
139
|
+
}), top.footer === undefined ? null : (_jsx("footer", { className: "flex shrink-0 flex-wrap items-center justify-end gap-2 border-t border-line bg-ground px-4 py-3 max-sm:safe-bottom", children: top.footer }))] }) }));
|
|
140
|
+
}
|
|
141
|
+
/** One context of a drill: a title, what goes inside, and its own footer. It does not
|
|
142
|
+
* draw where it is written — the `Drill` draws it when its id is on the path. */
|
|
143
|
+
const DrillContextSlot = slot("context", (_props) => null);
|
|
144
|
+
/**
|
|
145
|
+
* ONE dialog that holds a stack of contexts opened from a record — and from each other —
|
|
146
|
+
* with a trail back to the page. See the header of this file.
|
|
147
|
+
*
|
|
148
|
+
* const drill = useDrill();
|
|
149
|
+
* …<RelationValue name={product} onOpen={() => drill.open("product")} />…
|
|
150
|
+
* <Drill state={drill} origin={{ kind: "pedido", label: "S00142" }}>
|
|
151
|
+
* <Drill.Context id="product" title={product} flush>
|
|
152
|
+
* <Record identity={…}>…</Record>
|
|
153
|
+
* </Drill.Context>
|
|
154
|
+
* <Drill.Context id="supplier" title={supplier} footer={…}>…</Drill.Context>
|
|
155
|
+
* </Drill>
|
|
156
|
+
*/
|
|
157
|
+
export function Drill({ state, origin, width = "lg", children }) {
|
|
158
|
+
/* While the window fades out the path is already empty: it keeps drawing the last levels
|
|
159
|
+
it had, so the content does not vanish before the window does. */
|
|
160
|
+
const [lastPath, setLastPath] = useState(state.path);
|
|
161
|
+
if (state.path.length > 0 && state.path !== lastPath)
|
|
162
|
+
setLastPath(state.path);
|
|
163
|
+
const path = state.path.length > 0 ? state.path : lastPath;
|
|
164
|
+
const frames = framesOf(children, path);
|
|
165
|
+
return (_jsx(ModalOverlay, { isOpen: state.path.length > 0, onOpenChange: (next) => {
|
|
166
|
+
if (!next)
|
|
167
|
+
state.close();
|
|
168
|
+
}, isDismissable: true,
|
|
169
|
+
/* Escape is ours: up one level, not out of everything. See the header. */
|
|
170
|
+
isKeyboardDismissDisabled: true, className: dialogStyles.overlay, children: _jsx(Modal, { className: (s) => dialogStyles.modal({ ...s, width }), children: _jsx(DrillSurface, { frames: frames, ...(origin === undefined ? {} : { origin }), onBack: state.back, onTo: state.to, onClose: state.close, focusMarks: state.focusMarks }) }) }));
|
|
171
|
+
}
|
|
172
|
+
Drill.Context = DrillContextSlot;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
export type DisclosureProps = {
|
|
3
|
+
/** The name of what folds. */
|
|
4
|
+
title: ReactNode;
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
/** Its key inside an `Accordion`. Needed only when the accordion is controlled or has a
|
|
7
|
+
* `defaultExpanded`. */
|
|
8
|
+
id?: string;
|
|
9
|
+
/** Something about the content, at the right of the row: «3 líneas», «Bs 1.240,00». It is
|
|
10
|
+
* what lets a closed section be read without opening it. */
|
|
11
|
+
detail?: ReactNode;
|
|
12
|
+
defaultExpanded?: boolean;
|
|
13
|
+
expanded?: boolean;
|
|
14
|
+
onExpandedChange?: (expanded: boolean) => void;
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
/** The heading level of the title, so the outline of the page stays true. */
|
|
17
|
+
level?: 2 | 3 | 4 | 5 | 6;
|
|
18
|
+
};
|
|
19
|
+
export declare function Disclosure({ title, children, id, detail, defaultExpanded, expanded, onExpandedChange, disabled, level, }: DisclosureProps): import("react").JSX.Element;
|
|
20
|
+
/**
|
|
21
|
+
* Several `Disclosure`s that work as one: by default opening one closes the other.
|
|
22
|
+
*
|
|
23
|
+
* Context: a FAQ or the groups of a long form read better one at a time — the open section
|
|
24
|
+
* is the one being read and the rest stay as an index. `allowsMultipleExpanded` is for when
|
|
25
|
+
* the sections are compared, not read (the lines of two groups side by side). The value is
|
|
26
|
+
* a list of the sections' `id`s, not React Aria's `Set<Key>`, like every other control.
|
|
27
|
+
*/
|
|
28
|
+
export declare function Accordion({ children, allowsMultipleExpanded, defaultExpanded, expanded, onExpandedChange, disabled, }: {
|
|
29
|
+
/** `Disclosure`s, each with its `id`. */
|
|
30
|
+
children: ReactNode;
|
|
31
|
+
allowsMultipleExpanded?: boolean;
|
|
32
|
+
defaultExpanded?: string[];
|
|
33
|
+
expanded?: string[];
|
|
34
|
+
onExpandedChange?: (ids: string[]) => void;
|
|
35
|
+
disabled?: boolean;
|
|
36
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { cn, focusRing, tv } from "../../foundations/variants.js";
|
|
4
|
+
import { ChevronRight } from "lucide-react";
|
|
5
|
+
import { useContext } from "react";
|
|
6
|
+
import { Button as AriaButton, Disclosure as AriaDisclosure, DisclosureGroup as AriaDisclosureGroup, DisclosureGroupStateContext, DisclosurePanel, DisclosureStateContext, Heading, } from "react-aria-components";
|
|
7
|
+
/**
|
|
8
|
+
* A section that folds: the advanced options of a form, a question of a FAQ, the lines of a
|
|
9
|
+
* group.
|
|
10
|
+
*
|
|
11
|
+
* Context: it is a HEADING ROW, not a card. What folds is part of the page it is on — the
|
|
12
|
+
* rest of the form, the rest of the list — so it is separated by hairlines and not boxed:
|
|
13
|
+
* a box would say «this is another thing», and the advanced options of an invoice are still
|
|
14
|
+
* the invoice. The title is set like a field label (`text-meta`, medium) because that is
|
|
15
|
+
* what it is: the name of what is underneath.
|
|
16
|
+
*
|
|
17
|
+
* The chevron points right when closed and turns down when open — the one mark everybody
|
|
18
|
+
* already reads as «there is more here». React Aria owns the rest: the row is a real button
|
|
19
|
+
* with `aria-expanded`, the panel is `hidden="until-found"` when closed (the browser's
|
|
20
|
+
* find-in-page still opens it), and the height animates from the measure it publishes.
|
|
21
|
+
*/
|
|
22
|
+
const row = tv({
|
|
23
|
+
extend: focusRing,
|
|
24
|
+
base: "flex w-full items-center gap-2 rounded-control px-2 py-2 text-left text-meta font-medium text-ink transition-colors duration-(--duration-instant) hover:bg-ink/6 pressed:bg-ink/10",
|
|
25
|
+
variants: {
|
|
26
|
+
isDisabled: {
|
|
27
|
+
true: "cursor-not-allowed text-muted hover:bg-transparent forced-colors:text-[GrayText]",
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
function Header({ title, detail, level, }) {
|
|
32
|
+
const isExpanded = useContext(DisclosureStateContext)?.isExpanded ?? false;
|
|
33
|
+
return (_jsx(Heading, { level: level, className: "m-0", children: _jsxs(AriaButton, { slot: "trigger", className: (state) => row(state), children: [_jsx(ChevronRight, { "aria-hidden": true, strokeWidth: 2, className: cn("size-(--size-icon-md) shrink-0 text-muted transition-transform duration-(--duration-quick) ease-(--ease-out)", isExpanded && "rotate-90") }), _jsx("span", { className: "min-w-0 flex-1 truncate", children: title }), detail === undefined ? null : (_jsx("span", { className: "shrink-0 text-micro font-normal text-muted tabular-nums", children: detail }))] }) }));
|
|
34
|
+
}
|
|
35
|
+
export function Disclosure({ title, children, id, detail, defaultExpanded, expanded, onExpandedChange, disabled, level = 3, }) {
|
|
36
|
+
// Inside an accordion the group draws the line above; alone, it closes itself on both sides.
|
|
37
|
+
const inGroup = useContext(DisclosureGroupStateContext) !== null;
|
|
38
|
+
return (_jsxs(AriaDisclosure, { id: id, defaultExpanded: defaultExpanded, isExpanded: expanded, onExpandedChange: onExpandedChange, isDisabled: disabled, className: cn("border-line py-1", inGroup ? "border-b" : "border-y"), children: [_jsx(Header, { title: title, detail: detail, level: level }), _jsx(DisclosurePanel, { className: "h-(--disclosure-panel-height) overflow-clip motion-safe:transition-[height] motion-safe:duration-(--duration-panel) motion-safe:ease-(--ease-out)", children: _jsx("div", { className: "px-2 pt-1 pb-3 text-meta text-ink", children: children }) })] }));
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Several `Disclosure`s that work as one: by default opening one closes the other.
|
|
42
|
+
*
|
|
43
|
+
* Context: a FAQ or the groups of a long form read better one at a time — the open section
|
|
44
|
+
* is the one being read and the rest stay as an index. `allowsMultipleExpanded` is for when
|
|
45
|
+
* the sections are compared, not read (the lines of two groups side by side). The value is
|
|
46
|
+
* a list of the sections' `id`s, not React Aria's `Set<Key>`, like every other control.
|
|
47
|
+
*/
|
|
48
|
+
export function Accordion({ children, allowsMultipleExpanded = false, defaultExpanded, expanded, onExpandedChange, disabled, }) {
|
|
49
|
+
return (_jsx(AriaDisclosureGroup, { allowsMultipleExpanded: allowsMultipleExpanded, defaultExpandedKeys: defaultExpanded, expandedKeys: expanded, onExpandedChange: onExpandedChange === undefined
|
|
50
|
+
? undefined
|
|
51
|
+
: (keys) => onExpandedChange([...keys].map(String)), isDisabled: disabled, className: "border-t border-line", children: children }));
|
|
52
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Accordion, Disclosure, type DisclosureProps, } from "./Disclosure.js";
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { type FormMode } from "./mode.js";
|
|
2
|
+
export interface CheckItem {
|
|
3
|
+
value: string;
|
|
4
|
+
label: string;
|
|
5
|
+
/** The second line: what the thing is, not a second name. */
|
|
6
|
+
detail?: string;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* One yes/no answer of a form: «Exento de IVA», «Enviar la factura por correo al
|
|
11
|
+
* publicar».
|
|
12
|
+
*
|
|
13
|
+
* Context: it is React Aria's `Checkbox` — a native checkbox input inside the label, so the
|
|
14
|
+
* whole sentence is the click target, Space toggles it and the form reads it as a checkbox.
|
|
15
|
+
* `mixed` is its indeterminate state: the parent of a group where only some are on, and
|
|
16
|
+
* pressing it turns them ALL on — never all off, because «some» is closer to «all» than to
|
|
17
|
+
* «none» in what the person was doing.
|
|
18
|
+
*
|
|
19
|
+
* Inside a `Field` it is named by the field's label AND its own sentence, and described by
|
|
20
|
+
* the field's hint and error — the same wiring as `Switch`.
|
|
21
|
+
*/
|
|
22
|
+
export declare function Checkbox({ checked, mixed, onChange, label, disabled, mode, id: ownId, describedBy: ownDescribedBy, invalid: ownInvalid, }: {
|
|
23
|
+
checked: boolean;
|
|
24
|
+
/** Some of the children are on. Pressing it turns them all on. */
|
|
25
|
+
mixed?: boolean;
|
|
26
|
+
onChange?: (next: boolean) => void;
|
|
27
|
+
label: string;
|
|
28
|
+
disabled?: boolean;
|
|
29
|
+
mode?: FormMode;
|
|
30
|
+
id?: string;
|
|
31
|
+
describedBy?: string;
|
|
32
|
+
invalid?: boolean;
|
|
33
|
+
}): import("react").JSX.Element;
|
|
34
|
+
/**
|
|
35
|
+
* Several answers, all in sight, more than one allowed: the taxes of a product, the
|
|
36
|
+
* warehouses a user can see, the documents of an export.
|
|
37
|
+
*
|
|
38
|
+
* Context: it is React Aria's `ListBox` with `selectionMode="multiple"` and not a stack of
|
|
39
|
+
* checkboxes, because what it is to a keyboard is ONE stop with a list inside — Tab lands on
|
|
40
|
+
* it, the arrows move, Space toggles, Home and End jump, a letter finds the row — and twelve
|
|
41
|
+
* Tab stops is how a form becomes a chore. A disabled row is drawn and skipped. The value is
|
|
42
|
+
* a plain `string[]`, never React Aria's `Selection`.
|
|
43
|
+
*
|
|
44
|
+
* `mono` is for when the label was written by the machine — an account code, a tax code —
|
|
45
|
+
* and the detail was written by a person. The two faces are the point.
|
|
46
|
+
*/
|
|
47
|
+
export declare function CheckList({ label, items, value, onChange, mono, emptyLabel, describedBy: ownDescribedBy, mode, }: {
|
|
48
|
+
label: string;
|
|
49
|
+
items: readonly CheckItem[];
|
|
50
|
+
value: readonly string[];
|
|
51
|
+
onChange?: (next: string[]) => void;
|
|
52
|
+
mono?: boolean;
|
|
53
|
+
/** What read mode says when nothing is chosen. By default, the copy table's `field.none`. */
|
|
54
|
+
emptyLabel?: string;
|
|
55
|
+
describedBy?: string;
|
|
56
|
+
mode?: FormMode;
|
|
57
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useUiCopy } from "../../foundations/copy.js";
|
|
4
|
+
import { useFieldIds } from "./context.js";
|
|
5
|
+
import { useFormMode } from "./mode.js";
|
|
6
|
+
import { Check, Minus } from "lucide-react";
|
|
7
|
+
import { useId } from "react";
|
|
8
|
+
import { Checkbox as AriaCheckbox, ListBox, ListBoxItem, Text, } from "react-aria-components";
|
|
9
|
+
function Mark({ on, mixed, focus = false, invalid = false, }) {
|
|
10
|
+
return (_jsx("span", { "aria-hidden": true, className: `inline-flex size-4 shrink-0 items-center justify-center rounded-mark border transition-colors duration-(--duration-instant) forced-color-adjust-none ${on
|
|
11
|
+
? "border-primary bg-primary text-surface forced-colors:border-[Highlight] forced-colors:bg-[Highlight] forced-colors:text-[HighlightText]"
|
|
12
|
+
: `bg-surface text-transparent ${invalid ? "border-danger" : "border-line-strong"} forced-colors:border-[ButtonBorder]`} ${focus ? "outline-2 outline-offset-2 outline-primary forced-colors:outline-[Highlight]" : ""}`, children: mixed ? (_jsx(Minus, { className: "size-3", strokeWidth: 2.5 })) : (_jsx(Check, { className: "size-3", strokeWidth: 2.5 })) }));
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* One yes/no answer of a form: «Exento de IVA», «Enviar la factura por correo al
|
|
16
|
+
* publicar».
|
|
17
|
+
*
|
|
18
|
+
* Context: it is React Aria's `Checkbox` — a native checkbox input inside the label, so the
|
|
19
|
+
* whole sentence is the click target, Space toggles it and the form reads it as a checkbox.
|
|
20
|
+
* `mixed` is its indeterminate state: the parent of a group where only some are on, and
|
|
21
|
+
* pressing it turns them ALL on — never all off, because «some» is closer to «all» than to
|
|
22
|
+
* «none» in what the person was doing.
|
|
23
|
+
*
|
|
24
|
+
* Inside a `Field` it is named by the field's label AND its own sentence, and described by
|
|
25
|
+
* the field's hint and error — the same wiring as `Switch`.
|
|
26
|
+
*/
|
|
27
|
+
export function Checkbox({ checked, mixed = false, onChange, label, disabled = false, mode, id: ownId, describedBy: ownDescribedBy, invalid: ownInvalid, }) {
|
|
28
|
+
const { id, labelledBy, describedBy, invalid } = useFieldIds({
|
|
29
|
+
id: ownId,
|
|
30
|
+
describedBy: ownDescribedBy,
|
|
31
|
+
invalid: ownInvalid,
|
|
32
|
+
});
|
|
33
|
+
const textId = useId();
|
|
34
|
+
const on = checked || mixed;
|
|
35
|
+
if (useFormMode(mode) === "read") {
|
|
36
|
+
return (_jsxs("span", { className: "flex min-h-(--size-control-md) items-center gap-2 text-meta text-ink", children: [_jsx(Mark, { on: on, mixed: mixed && !checked }), label] }));
|
|
37
|
+
}
|
|
38
|
+
return (_jsx(AriaCheckbox, { id: id, "aria-labelledby": labelledBy === undefined ? undefined : `${labelledBy} ${textId}`, "aria-describedby": describedBy, isSelected: checked, isIndeterminate: mixed, onChange: (next) => onChange?.(mixed ? true : next), isDisabled: disabled, isInvalid: invalid, className: "group inline-flex min-w-0 cursor-pointer items-center gap-2.5 text-meta text-ink disabled:cursor-not-allowed disabled:opacity-45", children: ({ isSelected, isIndeterminate, isFocusVisible, isInvalid }) => (_jsxs(_Fragment, { children: [_jsx(Mark, { on: isSelected || isIndeterminate, mixed: isIndeterminate, focus: isFocusVisible, invalid: isInvalid }), _jsx("span", { id: textId, className: "min-w-0", children: label })] })) }));
|
|
39
|
+
}
|
|
40
|
+
/** The chosen values, in the order the person picked them — what `onChange` hands back. */
|
|
41
|
+
function valuesOf(keys, items) {
|
|
42
|
+
if (keys === "all")
|
|
43
|
+
return items.filter((item) => !item.disabled).map((item) => item.value);
|
|
44
|
+
return [...keys].map(String);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Several answers, all in sight, more than one allowed: the taxes of a product, the
|
|
48
|
+
* warehouses a user can see, the documents of an export.
|
|
49
|
+
*
|
|
50
|
+
* Context: it is React Aria's `ListBox` with `selectionMode="multiple"` and not a stack of
|
|
51
|
+
* checkboxes, because what it is to a keyboard is ONE stop with a list inside — Tab lands on
|
|
52
|
+
* it, the arrows move, Space toggles, Home and End jump, a letter finds the row — and twelve
|
|
53
|
+
* Tab stops is how a form becomes a chore. A disabled row is drawn and skipped. The value is
|
|
54
|
+
* a plain `string[]`, never React Aria's `Selection`.
|
|
55
|
+
*
|
|
56
|
+
* `mono` is for when the label was written by the machine — an account code, a tax code —
|
|
57
|
+
* and the detail was written by a person. The two faces are the point.
|
|
58
|
+
*/
|
|
59
|
+
export function CheckList({ label, items, value, onChange, mono = false, emptyLabel, describedBy: ownDescribedBy, mode, }) {
|
|
60
|
+
const labelId = useId();
|
|
61
|
+
const { describedBy } = useFieldIds({ describedBy: ownDescribedBy });
|
|
62
|
+
const none = useUiCopy().field.none;
|
|
63
|
+
if (useFormMode(mode) === "read") {
|
|
64
|
+
const picked = items.filter((item) => value.includes(item.value));
|
|
65
|
+
return (_jsxs("div", { className: "flex min-w-0 flex-col gap-1", children: [_jsx("p", { className: "text-micro text-muted", children: label }), _jsx("p", { className: `text-meta text-ink ${mono ? "font-mono" : ""}`, children: picked.length === 0
|
|
66
|
+
? (emptyLabel ?? none)
|
|
67
|
+
: picked.map((item) => item.label).join(", ") })] }));
|
|
68
|
+
}
|
|
69
|
+
return (_jsxs("div", { className: "flex min-w-0 flex-col gap-1.5", children: [_jsx("p", { id: labelId, className: "text-micro text-muted", children: label }), _jsx(ListBox, { "aria-labelledby": labelId, "aria-describedby": describedBy, items: items, selectionMode: "multiple", selectedKeys: value, onSelectionChange: (keys) => onChange?.(valuesOf(keys, items)), disabledKeys: items.filter((item) => item.disabled).map((item) => item.value), shouldFocusWrap: true, className: "flex flex-col overflow-hidden rounded-box border border-line bg-surface outline-0 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary", children: (item) => (_jsx(ListBoxItem, { id: item.value, textValue: item.label, className: ({ isSelected, isFocusVisible, isDisabled }) => `flex min-w-0 cursor-pointer items-center gap-2.5 border-b border-line px-3 py-2 text-left outline-0 last:border-b-0 ${isFocusVisible
|
|
70
|
+
? "outline-2 -outline-offset-2 outline-primary forced-colors:outline-[Highlight]"
|
|
71
|
+
: ""} ${isDisabled ? "cursor-not-allowed opacity-45" : ""} ${isSelected ? "bg-primary/8" : "hover:bg-ink/4"}`, children: ({ isSelected }) => (_jsxs(_Fragment, { children: [_jsx(Mark, { on: isSelected, mixed: false }), _jsxs("span", { className: "flex min-w-0 flex-col", children: [_jsx(Text, { slot: "label", className: `truncate text-meta text-ink ${mono ? "font-mono" : ""}`, children: item.label }), item.detail === undefined ? null : (_jsx(Text, { slot: "description", className: "truncate text-micro text-muted", children: item.detail }))] })] })) })) })] }));
|
|
72
|
+
}
|