@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,617 @@
|
|
|
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 { byDensity, useDensity } from "../../foundations/density.js";
|
|
5
|
+
import { pathKey, pileIsOpen, pileItems, pilesOf, } from "../../foundations/grouping.js";
|
|
6
|
+
import { HANDHELD_WIDTH } from "../../foundations/layout.js";
|
|
7
|
+
import { stickyTop, useScrollBox } from "../../foundations/sticky.js";
|
|
8
|
+
import { IconButton } from "../button/Button.js";
|
|
9
|
+
import { NoteCard } from "../field/note.js";
|
|
10
|
+
import { Flag } from "../notice/Flag.js";
|
|
11
|
+
import { Plus, Trash2 } from "lucide-react";
|
|
12
|
+
import { Fragment, useEffect, useLayoutEffect, useMemo, useRef, useState, } from "react";
|
|
13
|
+
import { useKeyboard } from "react-aria";
|
|
14
|
+
/** The lines each section heads: the rows after it up to the next section. Notes do not
|
|
15
|
+
* cut a section, and are not lines. */
|
|
16
|
+
export function sectionLines(rows, getRowId, kind) {
|
|
17
|
+
const out = new Map();
|
|
18
|
+
let current = null;
|
|
19
|
+
for (const row of rows) {
|
|
20
|
+
const k = kind(row);
|
|
21
|
+
if (k === "section") {
|
|
22
|
+
current = [];
|
|
23
|
+
out.set(getRowId(row), current);
|
|
24
|
+
}
|
|
25
|
+
else if (k === null)
|
|
26
|
+
current?.push(row);
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Where each column goes in the stacked card, defaults resolved: a declared `stack`
|
|
32
|
+
* wins; otherwise the first undeclared column is the title (unless one was declared),
|
|
33
|
+
* the last undeclared numeric one is the amount (unless one was declared), and the rest
|
|
34
|
+
* are detail.
|
|
35
|
+
*/
|
|
36
|
+
export function stackPlaces(columns) {
|
|
37
|
+
const titleAt = columns.some((c) => c.stack === "title")
|
|
38
|
+
? -1
|
|
39
|
+
: columns.findIndex((c) => c.stack === undefined);
|
|
40
|
+
const amountAt = columns.some((c) => c.stack === "amount")
|
|
41
|
+
? -1
|
|
42
|
+
: columns.findLastIndex((c, i) => c.stack === undefined && c.numeric === true && i !== titleAt);
|
|
43
|
+
return columns.map((c, i) => c.stack ?? (i === titleAt ? "title" : i === amountAt ? "amount" : "detail"));
|
|
44
|
+
}
|
|
45
|
+
/** A brief that says nothing is left out of the line, separator included. */
|
|
46
|
+
function says(node) {
|
|
47
|
+
return node !== null && node !== undefined && node !== false && node !== "";
|
|
48
|
+
}
|
|
49
|
+
/** The label of a group band — its field, its value, how many rows it holds. The same
|
|
50
|
+
* in the table's band and the stacked list's. */
|
|
51
|
+
function pileTag(pile, locale) {
|
|
52
|
+
return (_jsxs("span", { className: "flex min-w-0 items-center gap-1.5", children: [_jsx("span", { className: "shrink-0 text-micro text-muted/70", children: pile.field }), _jsx("span", { className: "truncate text-micro font-medium text-ink", children: pile.label }), _jsx("span", { className: "inline-flex shrink-0 items-center rounded-pill border border-line-strong bg-ink/5 px-2 py-0.5 text-micro tabular-nums text-muted", children: pile.count.toLocaleString(locale) })] }));
|
|
53
|
+
}
|
|
54
|
+
/** The focus ring of an operable row: drawn on the row, inset, so it is not cut by the
|
|
55
|
+
* box and does not move the cells. */
|
|
56
|
+
const RING = "outline-0 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-primary forced-colors:focus-visible:outline-[Highlight]";
|
|
57
|
+
/** What can take the focus in a row. Filtered again in `stopsIn`: React Aria's hidden form
|
|
58
|
+
* mirrors (a `<select>` behind a `Select`, an `<input hidden>` in a date picker) match this
|
|
59
|
+
* and are not stops. */
|
|
60
|
+
const FOCUSABLE = 'input:not([type="hidden"]):not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), a[href], [tabindex]';
|
|
61
|
+
/** What `Tab` stops on inside `scope` of a row: focusable, and not a hidden form mirror.
|
|
62
|
+
*
|
|
63
|
+
* Context: NOT filtered by `aria-hidden` / `inert`. While a list is open (a tags field, a
|
|
64
|
+
* picker) React Aria hides everything outside it that way — the rest of the row included —
|
|
65
|
+
* and the row then looked as if its open control were its last: `Tab` added a new row from
|
|
66
|
+
* the middle of this one. The mirrors carry their own marks instead. */
|
|
67
|
+
function stopsIn(row, scope) {
|
|
68
|
+
return [...row.querySelectorAll(scope)].filter((el) => el.tabIndex >= 0 && el.closest("[hidden], [data-react-aria-prevent-focus]") === null);
|
|
69
|
+
}
|
|
70
|
+
/** The controls of the editable cells of a row, in tab order. */
|
|
71
|
+
function editStops(row) {
|
|
72
|
+
return stopsIn(row, `[data-edit] :is(${FOCUSABLE})`);
|
|
73
|
+
}
|
|
74
|
+
/** The first control of a row being edited: the first one in an editable cell, else
|
|
75
|
+
* anything focusable in the row (a column without `edit` may still hold a button). */
|
|
76
|
+
export function firstControl(row) {
|
|
77
|
+
return editStops(row)[0] ?? stopsIn(row, FOCUSABLE)[0] ?? null;
|
|
78
|
+
}
|
|
79
|
+
/** The last control of a row being edited — where `Shift+Tab` from the next row lands. */
|
|
80
|
+
function lastControl(row) {
|
|
81
|
+
return editStops(row).at(-1) ?? stopsIn(row, FOCUSABLE).at(-1) ?? null;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Where `Tab` past the last control (`next`) or `Shift+Tab` before the first (`prev`) of
|
|
85
|
+
* the edited row `from` goes, over the rows in the order they are drawn.
|
|
86
|
+
*
|
|
87
|
+
* Past the last row it adds one when the table can — unless `from` is itself a row just
|
|
88
|
+
* added and still blank: one more `Tab` there means «I am done», not «another blank line».
|
|
89
|
+
*/
|
|
90
|
+
export function tabTarget(order, from, direction, { canAdd, blank }) {
|
|
91
|
+
const at = order.indexOf(from);
|
|
92
|
+
if (at === -1)
|
|
93
|
+
return { kind: "leave" };
|
|
94
|
+
const to = order[direction === "next" ? at + 1 : at - 1];
|
|
95
|
+
if (to !== undefined)
|
|
96
|
+
return { kind: "row", id: to };
|
|
97
|
+
return direction === "next" && canAdd && !blank ? { kind: "add" } : { kind: "leave" };
|
|
98
|
+
}
|
|
99
|
+
/** Keys a control inside the row already answers with `Enter`: a button presses, a
|
|
100
|
+
* link follows, a text area breaks the line, an open list picks its option. */
|
|
101
|
+
function ownsEnter(target) {
|
|
102
|
+
return (target instanceof Element &&
|
|
103
|
+
target.closest('button, a[href], textarea, [role="button"], [role="combobox"][aria-expanded="true"]') !== null);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* One body row. Its own component so that it can hold React Aria's `useKeyboard`
|
|
107
|
+
* (a hook per row) and the timer that decides whether the focus really left it.
|
|
108
|
+
* The same row is a `<tr>` in the table and an `<li>` in the stacked list — and the row
|
|
109
|
+
* of a new record in `DataTable`: one keyboard model, three shapes. Exported for that
|
|
110
|
+
* family sibling only; it is not in the barrel.
|
|
111
|
+
*/
|
|
112
|
+
export function EditRow({ as, rowId, rowRef, editable, editing, tabbable, className, onStart, onCommit, onCancel, onMove, onFocusRow, onTabOut, children, }) {
|
|
113
|
+
const leaving = useRef(null);
|
|
114
|
+
const stay = () => {
|
|
115
|
+
if (leaving.current !== null)
|
|
116
|
+
clearTimeout(leaving.current);
|
|
117
|
+
leaving.current = null;
|
|
118
|
+
};
|
|
119
|
+
useEffect(() => stay, []);
|
|
120
|
+
/* React Aria's `useKeyboard` stops the propagation of what it handles and lets
|
|
121
|
+
the rest through (`continuePropagation`), so an `Escape` the row did not use
|
|
122
|
+
still reaches the dialog or the drawer around the table. */
|
|
123
|
+
const { keyboardProps } = useKeyboard({
|
|
124
|
+
isDisabled: !editable,
|
|
125
|
+
onKeyDown: (e) => {
|
|
126
|
+
if (!editing) {
|
|
127
|
+
/* Only on the row itself: a link drawn inside a reading cell keeps its
|
|
128
|
+
own `Enter`. */
|
|
129
|
+
if (e.target !== e.currentTarget)
|
|
130
|
+
return e.continuePropagation();
|
|
131
|
+
if (e.key === "Enter" || e.key === "F2") {
|
|
132
|
+
e.preventDefault();
|
|
133
|
+
onStart(true);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (e.key === "ArrowDown" ||
|
|
137
|
+
e.key === "ArrowUp" ||
|
|
138
|
+
e.key === "Home" ||
|
|
139
|
+
e.key === "End") {
|
|
140
|
+
e.preventDefault();
|
|
141
|
+
onMove(e.key);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
return e.continuePropagation();
|
|
145
|
+
}
|
|
146
|
+
if (e.key === "Escape") {
|
|
147
|
+
e.preventDefault();
|
|
148
|
+
onCancel();
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
e.continuePropagation();
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
const props = {
|
|
155
|
+
...keyboardProps,
|
|
156
|
+
/* `Enter` commits in the CAPTURE phase: React Aria's number field answers
|
|
157
|
+
`Enter` itself and stops it, so it would never bubble up to the row. Taking
|
|
158
|
+
it on the way down —and only from the row's own cells, not from a list
|
|
159
|
+
open in a portal— commits from any control. Focusing the row first lets
|
|
160
|
+
that field write the value it was holding (it commits on blur). */
|
|
161
|
+
onKeyDownCapture: editing
|
|
162
|
+
? (e) => {
|
|
163
|
+
if (e.nativeEvent.isComposing)
|
|
164
|
+
return;
|
|
165
|
+
if (!(e.target instanceof Node) || !e.currentTarget.contains(e.target))
|
|
166
|
+
return;
|
|
167
|
+
if (e.key === "Tab") {
|
|
168
|
+
/* Only the default is prevented, never the propagation: a combobox still
|
|
169
|
+
hears its `Tab` and takes the option it has marked before the row moves. */
|
|
170
|
+
const stops = editStops(e.currentTarget);
|
|
171
|
+
const edge = e.shiftKey ? stops[0] : stops.at(-1);
|
|
172
|
+
if (edge === undefined || !edge.contains(e.target))
|
|
173
|
+
return;
|
|
174
|
+
if (onTabOut(e.shiftKey ? "prev" : "next"))
|
|
175
|
+
e.preventDefault();
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (e.key !== "Enter")
|
|
179
|
+
return;
|
|
180
|
+
if (ownsEnter(e.target))
|
|
181
|
+
return;
|
|
182
|
+
e.preventDefault();
|
|
183
|
+
e.stopPropagation();
|
|
184
|
+
onCommit(true);
|
|
185
|
+
}
|
|
186
|
+
: undefined,
|
|
187
|
+
...(editable
|
|
188
|
+
? {
|
|
189
|
+
/* While it is being edited the row itself is not a stop: `Shift+Tab`
|
|
190
|
+
from its first control leaves it (and commits) instead of landing on
|
|
191
|
+
the row it is already in. */
|
|
192
|
+
tabIndex: tabbable && !editing ? 0 : -1,
|
|
193
|
+
"aria-keyshortcuts": editing ? undefined : "Enter F2",
|
|
194
|
+
onFocus: () => {
|
|
195
|
+
stay();
|
|
196
|
+
onFocusRow();
|
|
197
|
+
},
|
|
198
|
+
onBlur: editing
|
|
199
|
+
? () => {
|
|
200
|
+
/* Commit only if the focus left the WHOLE row — and the row is
|
|
201
|
+
not only its DOM. A `Select`'s list opens in a portal, outside
|
|
202
|
+
the `<tr>`, so "the next focused element is not inside" would
|
|
203
|
+
close the row the moment its list opened. The focus event of a
|
|
204
|
+
portal still bubbles through React's tree back to this row, so
|
|
205
|
+
the commit waits one tick: if the focus landed anywhere that
|
|
206
|
+
belongs to the row, `onFocus` above cancels it. */
|
|
207
|
+
stay();
|
|
208
|
+
leaving.current = setTimeout(() => {
|
|
209
|
+
leaving.current = null;
|
|
210
|
+
onCommit(false);
|
|
211
|
+
}, 0);
|
|
212
|
+
}
|
|
213
|
+
: undefined,
|
|
214
|
+
onClick: editing ? undefined : () => onStart(false),
|
|
215
|
+
}
|
|
216
|
+
: {}),
|
|
217
|
+
className,
|
|
218
|
+
"data-row-id": rowId,
|
|
219
|
+
};
|
|
220
|
+
return as === "li" ? (_jsx("li", { ref: rowRef, ...props, children: children })) : (_jsx("tr", { ref: rowRef, ...props, children: children }));
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* A table whose rows are edited in place, one row at a time.
|
|
224
|
+
*
|
|
225
|
+
* Mouse: touching a row opens it. Keyboard: the rows are one tab stop, the arrows walk
|
|
226
|
+
* them, `Enter` / `F2` open one, `Tab` walks its controls, `Enter` commits and `Escape`
|
|
227
|
+
* puts the row back as it was; both return the focus to the row. Leaving the row
|
|
228
|
+
* (click outside, tabbing past its last control) commits.
|
|
229
|
+
*
|
|
230
|
+
* Narrower than `--container-handheld`, each row is a stacked card (see the file's
|
|
231
|
+
* header): the same keys, and `onOpen` —when given— instead of editing in place.
|
|
232
|
+
*/
|
|
233
|
+
export function EditableTable({ rows, columns, getRowId, onChange, onRemove, onAdd, addLabel,
|
|
234
|
+
/** The footer with the aggregates, already formatted. `null` on the columns
|
|
235
|
+
* that do not add up. Whoever composes computes it, because whoever composes
|
|
236
|
+
* knows what adding up means in their domain —a percentage is not summed, it
|
|
237
|
+
* is averaged. */
|
|
238
|
+
totals, empty, grouping,
|
|
239
|
+
/** El total de una columna DENTRO de un montón, ya formateado. Lo calcula quien
|
|
240
|
+
* compone, por el mismo motivo que `totals`: sumar significa cosas distintas
|
|
241
|
+
* según el dominio, y un porcentaje se promedia, no se suma. */
|
|
242
|
+
groupTotal, height, layout = "auto", onOpen, onDiscard, bands, adds, flag, }) {
|
|
243
|
+
const copy = useUiCopy();
|
|
244
|
+
const bandOf = (row) => bands?.kind(row) ?? null;
|
|
245
|
+
/* The row's mark, before its first value — and, in a record's margin, its card. */
|
|
246
|
+
const flagOf = (row, withCard = false) => {
|
|
247
|
+
const said = flag?.(row) ?? null;
|
|
248
|
+
if (said === null)
|
|
249
|
+
return null;
|
|
250
|
+
return (_jsxs(_Fragment, { children: [_jsx(Flag, { tone: said.tone, label: said.label, children: said.detail }), withCard ? (_jsx(NoteCard, { id: `flag-${getRowId(row)}`, ...(said.owner === undefined ? {} : { owner: said.owner }), note: {
|
|
251
|
+
tone: said.tone,
|
|
252
|
+
label: said.label,
|
|
253
|
+
...(said.detail === undefined ? {} : { detail: said.detail }),
|
|
254
|
+
} })) : null] }));
|
|
255
|
+
};
|
|
256
|
+
const underSection = useMemo(() => (bands === undefined ? null : sectionLines(rows, getRowId, bands.kind)), [rows, bands, getRowId]);
|
|
257
|
+
/* Where a band's text stops: the first column with something of its own on that row. */
|
|
258
|
+
const bandFigures = (row) => {
|
|
259
|
+
const lines = underSection?.get(getRowId(row)) ?? null;
|
|
260
|
+
const figures = columns.map((c) => c.onBands === true
|
|
261
|
+
? c.render(row)
|
|
262
|
+
: lines === null
|
|
263
|
+
? null
|
|
264
|
+
: (bands?.total?.(c, lines) ?? null));
|
|
265
|
+
const first = figures.findIndex((f, i) => columns[i]?.onBands === true || (f !== null && f !== undefined));
|
|
266
|
+
return { span: first <= 0 ? columns.length : first, figures };
|
|
267
|
+
};
|
|
268
|
+
const d = useDensity();
|
|
269
|
+
const box = useScrollBox(height === undefined ? {} : { height });
|
|
270
|
+
const [editing, setEditing] = useState(null);
|
|
271
|
+
/* The row that holds the one tab stop of the body: the last one focused. */
|
|
272
|
+
const [active, setActive] = useState(null);
|
|
273
|
+
const rowNodes = useRef(new Map());
|
|
274
|
+
/* The row as it was when editing began, to put it back on `Escape`; `touched` says
|
|
275
|
+
whether any control wrote to it since, so a cancel without changes changes nothing. */
|
|
276
|
+
const before = useRef(null);
|
|
277
|
+
/* Set when the keyboard opened the row: its first control —or its last, coming back
|
|
278
|
+
with `Shift+Tab`— takes the focus once the controls exist. A click opens it without
|
|
279
|
+
moving the focus. */
|
|
280
|
+
const focusOnOpen = useRef(null);
|
|
281
|
+
/* The id `onAdd` returned, until that row shows up in `rows` and can be opened. */
|
|
282
|
+
const [arriving, setArriving] = useState(null);
|
|
283
|
+
const addButton = useRef(null);
|
|
284
|
+
/* La secuencia que se dibuja: montones y filas intercalados. Sin agrupado, las
|
|
285
|
+
filas tal cual — una tabla que no agrupa no paga nada por poder hacerlo. */
|
|
286
|
+
const drawn = useMemo(() => {
|
|
287
|
+
const piles = pilesOf(rows, grouping);
|
|
288
|
+
if (piles === null)
|
|
289
|
+
return rows.map((row) => ({ row }));
|
|
290
|
+
const out = [];
|
|
291
|
+
const walk = (list) => {
|
|
292
|
+
for (const pile of list) {
|
|
293
|
+
out.push({ pile });
|
|
294
|
+
if (!pileIsOpen(pile, grouping))
|
|
295
|
+
continue;
|
|
296
|
+
if (pile.piles.length > 0)
|
|
297
|
+
walk(pile.piles);
|
|
298
|
+
else
|
|
299
|
+
for (const row of pile.items)
|
|
300
|
+
out.push({ row });
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
walk(piles);
|
|
304
|
+
return out;
|
|
305
|
+
}, [rows, grouping]);
|
|
306
|
+
/* The width of the box decides, not the window's: a drawer on a wide screen is as
|
|
307
|
+
narrow as a phone. Measured before paint, so a phone never sees the table first. */
|
|
308
|
+
const frame = useRef(null);
|
|
309
|
+
const [narrow, setNarrow] = useState(false);
|
|
310
|
+
useLayoutEffect(() => {
|
|
311
|
+
const el = frame.current;
|
|
312
|
+
if (layout !== "auto" || el === null)
|
|
313
|
+
return;
|
|
314
|
+
/* A box that measures zero is hidden (a closed tab): it keeps what it had. */
|
|
315
|
+
const measure = (width) => {
|
|
316
|
+
if (width > 0)
|
|
317
|
+
setNarrow(width < HANDHELD_WIDTH);
|
|
318
|
+
};
|
|
319
|
+
measure(el.getBoundingClientRect().width);
|
|
320
|
+
if (typeof ResizeObserver === "undefined")
|
|
321
|
+
return;
|
|
322
|
+
const watch = new ResizeObserver(([entry]) => {
|
|
323
|
+
if (entry !== undefined)
|
|
324
|
+
measure(entry.contentRect.width);
|
|
325
|
+
});
|
|
326
|
+
watch.observe(el);
|
|
327
|
+
return () => watch.disconnect();
|
|
328
|
+
}, [layout]);
|
|
329
|
+
const stacked = layout === "stacked" || (layout === "auto" && narrow);
|
|
330
|
+
const editable = onChange !== undefined;
|
|
331
|
+
/* A card that opens the consumer's form is operable even with nothing to edit in
|
|
332
|
+
place: it has a tab stop, takes Enter and answers a press. */
|
|
333
|
+
const opens = stacked && onOpen !== undefined;
|
|
334
|
+
const operable = editable || opens;
|
|
335
|
+
const cell = byDensity(d, "px-3 py-1.5", "px-2 py-1");
|
|
336
|
+
const addText = addLabel ?? copy.editable.add;
|
|
337
|
+
const emptyText = empty ?? copy.editable.empty;
|
|
338
|
+
/* The rows in the order they are drawn: what the arrows walk. */
|
|
339
|
+
const order = drawn.flatMap((entry) => entry.row === undefined ? [] : [getRowId(entry.row)]);
|
|
340
|
+
const stop = active !== null && order.includes(active) ? active : (order[0] ?? null);
|
|
341
|
+
useEffect(() => {
|
|
342
|
+
const target = focusOnOpen.current;
|
|
343
|
+
if (target === null || target.id !== editing)
|
|
344
|
+
return;
|
|
345
|
+
focusOnOpen.current = null;
|
|
346
|
+
const node = rowNodes.current.get(target.id);
|
|
347
|
+
if (node === undefined)
|
|
348
|
+
return;
|
|
349
|
+
const control = target.at === "last" ? lastControl(node) : firstControl(node);
|
|
350
|
+
(control ?? node).focus();
|
|
351
|
+
}, [editing]);
|
|
352
|
+
const start = (id, row, viaKeyboard, { at = "first", added = false } = {}) => {
|
|
353
|
+
/* A row just added is written where it appeared, card or not: it was added in place
|
|
354
|
+
precisely so that it would not open somewhere else. */
|
|
355
|
+
if (opens && !added && bandOf(row) === null) {
|
|
356
|
+
setActive(id);
|
|
357
|
+
onOpen?.(id);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
before.current = { id, row, touched: false, added };
|
|
361
|
+
focusOnOpen.current = viaKeyboard ? { id, at } : null;
|
|
362
|
+
setActive(id);
|
|
363
|
+
setEditing(id);
|
|
364
|
+
};
|
|
365
|
+
/* The added row is opened the render it appears in — which may be this one, or the one
|
|
366
|
+
after the backend answered. */
|
|
367
|
+
useEffect(() => {
|
|
368
|
+
if (arriving === null)
|
|
369
|
+
return;
|
|
370
|
+
const row = rows.find((r) => getRowId(r) === arriving);
|
|
371
|
+
if (row === undefined)
|
|
372
|
+
return;
|
|
373
|
+
setArriving(null);
|
|
374
|
+
start(arriving, row, true, { added: true });
|
|
375
|
+
/* `start` and `getRowId` are new every render; what decides is the id and the rows. */
|
|
376
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
377
|
+
}, [arriving, rows]);
|
|
378
|
+
const add = (maker = onAdd) => {
|
|
379
|
+
if (maker === undefined)
|
|
380
|
+
return;
|
|
381
|
+
const made = maker();
|
|
382
|
+
if (typeof made === "string")
|
|
383
|
+
setArriving(made);
|
|
384
|
+
else if (made instanceof Promise) {
|
|
385
|
+
void made.then((id) => {
|
|
386
|
+
if (typeof id === "string")
|
|
387
|
+
setArriving(id);
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
/* Keyed by the row: a commit that arrives late (the blur of the row you clicked
|
|
392
|
+
away from) must not close the row the click just opened. */
|
|
393
|
+
const close = (id) => setEditing((current) => (current === id ? null : current));
|
|
394
|
+
/* A row added and abandoned goes away. `true` when it did. */
|
|
395
|
+
const discard = (id, untouchedOnly) => {
|
|
396
|
+
const was = before.current;
|
|
397
|
+
if (was === null || was.id !== id || !was.added || onDiscard === undefined)
|
|
398
|
+
return false;
|
|
399
|
+
if (untouchedOnly && was.touched)
|
|
400
|
+
return false;
|
|
401
|
+
before.current = null;
|
|
402
|
+
close(id);
|
|
403
|
+
onDiscard(id);
|
|
404
|
+
return true;
|
|
405
|
+
};
|
|
406
|
+
const commit = (id, refocus) => {
|
|
407
|
+
if (discard(id, true)) {
|
|
408
|
+
if (refocus)
|
|
409
|
+
addButton.current?.focus();
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
if (refocus)
|
|
413
|
+
rowNodes.current.get(id)?.focus();
|
|
414
|
+
if (before.current?.id === id)
|
|
415
|
+
before.current = null;
|
|
416
|
+
close(id);
|
|
417
|
+
};
|
|
418
|
+
const cancel = (id) => {
|
|
419
|
+
/* The focus goes back to the row FIRST: a control that writes its value on blur
|
|
420
|
+
(a number field) writes it now, and the old row is put back after it, not
|
|
421
|
+
before. */
|
|
422
|
+
rowNodes.current.get(id)?.focus();
|
|
423
|
+
if (discard(id, false)) {
|
|
424
|
+
/* The row is gone with the focus in it: «add» is where it came from. */
|
|
425
|
+
addButton.current?.focus();
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
const was = before.current;
|
|
429
|
+
before.current = null;
|
|
430
|
+
if (was !== null && was.id === id && was.touched)
|
|
431
|
+
onChange?.(id, was.row);
|
|
432
|
+
close(id);
|
|
433
|
+
};
|
|
434
|
+
const tabOut = (from, direction) => {
|
|
435
|
+
const was = before.current;
|
|
436
|
+
const blank = was !== null && was.id === from && was.added && !was.touched;
|
|
437
|
+
const target = tabTarget(order, from, direction, { canAdd: onAdd !== undefined, blank });
|
|
438
|
+
if (target.kind === "leave")
|
|
439
|
+
return false;
|
|
440
|
+
commit(from, false);
|
|
441
|
+
if (target.kind === "add") {
|
|
442
|
+
add();
|
|
443
|
+
return true;
|
|
444
|
+
}
|
|
445
|
+
const row = rows.find((r) => getRowId(r) === target.id);
|
|
446
|
+
if (row === undefined)
|
|
447
|
+
return false;
|
|
448
|
+
start(target.id, row, true, { at: direction === "next" ? "first" : "last" });
|
|
449
|
+
return true;
|
|
450
|
+
};
|
|
451
|
+
/* What a control writes, marking the row as touched for `Escape`. */
|
|
452
|
+
const setter = (id) => (next) => {
|
|
453
|
+
if (before.current?.id === id)
|
|
454
|
+
before.current.touched = true;
|
|
455
|
+
onChange?.(id, next);
|
|
456
|
+
};
|
|
457
|
+
/* The wiring a row needs, the same for the table's `<tr>` and the stacked `<li>`. */
|
|
458
|
+
const rowWiring = (id, row) => ({
|
|
459
|
+
rowId: id,
|
|
460
|
+
rowRef: (node) => {
|
|
461
|
+
if (node === null)
|
|
462
|
+
rowNodes.current.delete(id);
|
|
463
|
+
else
|
|
464
|
+
rowNodes.current.set(id, node);
|
|
465
|
+
},
|
|
466
|
+
editable: operable,
|
|
467
|
+
editing: editing === id,
|
|
468
|
+
tabbable: stop === id,
|
|
469
|
+
onStart: (viaKeyboard) => start(id, row, viaKeyboard),
|
|
470
|
+
onCommit: (refocus) => commit(id, refocus),
|
|
471
|
+
onCancel: () => cancel(id),
|
|
472
|
+
onMove: (key) => move(id, key),
|
|
473
|
+
onFocusRow: () => setActive(id),
|
|
474
|
+
onTabOut: (direction) => tabOut(id, direction),
|
|
475
|
+
});
|
|
476
|
+
const move = (from, key) => {
|
|
477
|
+
const at = order.indexOf(from);
|
|
478
|
+
const to = key === "Home"
|
|
479
|
+
? 0
|
|
480
|
+
: key === "End"
|
|
481
|
+
? order.length - 1
|
|
482
|
+
: key === "ArrowDown"
|
|
483
|
+
? Math.min(at + 1, order.length - 1)
|
|
484
|
+
: Math.max(at - 1, 0);
|
|
485
|
+
const id = order[to];
|
|
486
|
+
if (id === undefined)
|
|
487
|
+
return;
|
|
488
|
+
setActive(id);
|
|
489
|
+
rowNodes.current.get(id)?.focus();
|
|
490
|
+
};
|
|
491
|
+
/* ── the stacked list: one card per row, below `--container-handheld` ── */
|
|
492
|
+
const places = stackPlaces(columns);
|
|
493
|
+
const placed = (place) => columns.filter((_, i) => places[i] === place);
|
|
494
|
+
const titles = placed("title");
|
|
495
|
+
const details = placed("detail");
|
|
496
|
+
const amounts = placed("amount");
|
|
497
|
+
const trails = placed("trail");
|
|
498
|
+
const pad = byDensity(d, "px-3 py-2.5", "px-2 py-1.5");
|
|
499
|
+
const card = (row) => {
|
|
500
|
+
const id = getRowId(row);
|
|
501
|
+
const isEditing = editing === id;
|
|
502
|
+
const set = setter(id);
|
|
503
|
+
const parts = details.flatMap((c) => {
|
|
504
|
+
const said = c.brief === undefined ? null : c.brief(row);
|
|
505
|
+
if (c.brief !== undefined)
|
|
506
|
+
return says(said) ? [{ id: c.id, node: said }] : [];
|
|
507
|
+
return [
|
|
508
|
+
{
|
|
509
|
+
id: c.id,
|
|
510
|
+
node: (_jsxs(_Fragment, { children: [c.header, " ", c.render(row)] })),
|
|
511
|
+
},
|
|
512
|
+
];
|
|
513
|
+
});
|
|
514
|
+
const controls = editable && isEditing ? columns.filter((c) => c.edit !== undefined) : [];
|
|
515
|
+
return (_jsxs(EditRow, { as: "li", ...rowWiring(id, row), className: `group/row flex min-w-0 flex-col gap-1 rounded-control border-b border-line ${pad} ${isEditing ? "bg-primary/5" : "hover:bg-ink/4"} ${operable ? RING : ""} ${operable && !isEditing ? (opens ? "cursor-pointer" : "cursor-text") : ""}`, children: [_jsxs("div", { className: "flex min-w-0 items-start gap-2", children: [flagOf(row), _jsx("div", { className: "flex min-w-0 flex-1 flex-col gap-0.5 text-meta text-ink", children: titles.map((c) => (_jsx("div", { className: "min-w-0", children: c.render(row) }, c.id))) }), trails.map((c) => (_jsx("div", { className: "shrink-0", children: c.render(row) }, c.id))), isEditing && onRemove !== undefined ? (_jsx(IconButton, { icon: Trash2, label: copy.editable.remove, size: "sm", tone: "danger", onClick: () => onRemove(id) })) : null] }), parts.length === 0 && amounts.length === 0 ? null : (_jsxs("div", { className: "flex min-w-0 items-baseline gap-3 text-meta", children: [_jsx("span", { className: "min-w-0 flex-1 text-muted tabular-nums", children: parts.map((p, i) => (_jsxs(Fragment, { children: [i > 0 ? " · " : null, p.node] }, p.id))) }), amounts.map((c) => (
|
|
516
|
+
/* The amount is read by its figure on screen and by its name aloud: a
|
|
517
|
+
lone «Bs. 1.296,00» at the end of a card does not say what it is. */
|
|
518
|
+
_jsxs("span", { className: "shrink-0 text-right font-medium whitespace-nowrap text-ink tabular-nums", children: [_jsxs("span", { className: "sr-only", children: [c.header, " "] }), c.render(row)] }, c.id)))] })), controls.length === 0 ? null : (
|
|
519
|
+
/* Each control under the column's name: a cell had the header above it, a
|
|
520
|
+
card has nothing, and a number box that does not say what it holds is a
|
|
521
|
+
guess. The name is drawn for the eye; the control carries its own label. */
|
|
522
|
+
_jsx("div", { className: "grid grid-cols-2 gap-x-3 gap-y-2 pt-1.5", children: controls.map((c) => (_jsxs("div", { "data-edit": "", className: `flex min-w-0 flex-col gap-1 ${c.numeric === true ? "" : "col-span-2"}`, children: [_jsx("span", { "aria-hidden": "true", className: "text-micro text-muted", children: c.header }), c.edit?.(row, set)] }, c.id))) }))] }, id));
|
|
523
|
+
};
|
|
524
|
+
/* A section or a note on a phone: the band and, for a section, what it adds up to in
|
|
525
|
+
the card's amount; editing, its control under it. */
|
|
526
|
+
const bandCard = (row) => {
|
|
527
|
+
const id = getRowId(row);
|
|
528
|
+
const band = bandOf(row);
|
|
529
|
+
const isEditing = editing === id;
|
|
530
|
+
const lines = underSection?.get(id) ?? null;
|
|
531
|
+
return (_jsxs(EditRow, { as: "li", ...rowWiring(id, row), className: `flex min-w-0 flex-col gap-1.5 rounded-control border-b border-line ${pad} ${isEditing ? "bg-primary/5" : band === "section" ? "bg-ground" : ""} ${operable ? RING : ""}`, children: [_jsxs("div", { className: "flex min-w-0 items-baseline gap-3 text-meta", children: [_jsx("span", { className: `min-w-0 flex-1 ${band === "section" ? "font-semibold text-ink" : "text-muted italic"}`, children: bands?.render(row) }), lines === null
|
|
532
|
+
? null
|
|
533
|
+
: amounts.map((c) => (_jsx("span", { className: "shrink-0 font-semibold text-ink tabular-nums", children: bands?.total?.(c, lines) ?? null }, c.id))), isEditing && onRemove !== undefined ? (_jsx(IconButton, { icon: Trash2, label: copy.editable.remove, size: "sm", tone: "danger", onClick: () => onRemove(id) })) : null] }), isEditing && editable && bands?.edit !== undefined ? (_jsx("div", { "data-edit": "", children: bands.edit(row, setter(id)) })) : null] }, id));
|
|
534
|
+
};
|
|
535
|
+
const list = (_jsxs(_Fragment, { children: [_jsx("ul", { "data-stacked": "", className: "flex min-w-0 flex-col px-1", children: rows.length === 0 ? (_jsx("li", { className: `text-meta text-muted italic ${pad}`, children: emptyText })) : (drawn.map((entry) => {
|
|
536
|
+
if (entry.pile !== undefined) {
|
|
537
|
+
const pile = entry.pile;
|
|
538
|
+
const inside = pile.piles.length === 0 ? pile.items : pileItems(pile.piles);
|
|
539
|
+
return (
|
|
540
|
+
/* The band says the group and, at the right, what its rows add up to in
|
|
541
|
+
the column that is the card's amount: the figure under the figures. */
|
|
542
|
+
_jsxs("li", { className: `flex min-w-0 items-center gap-3 bg-ground text-meta ${pad}`, style: pile.depth > 0
|
|
543
|
+
? {
|
|
544
|
+
paddingLeft: `calc(var(--spacing-indent) * ${pile.depth} + 0.625rem)`,
|
|
545
|
+
}
|
|
546
|
+
: undefined, children: [_jsx("span", { className: "min-w-0 flex-1", children: pileTag(pile, copy.locale) }), amounts.map((c) => (_jsx("span", { className: "shrink-0 text-ink tabular-nums", children: groupTotal?.(c, inside) ?? null }, c.id)))] }, `pile:${pathKey(pile.path)}`));
|
|
547
|
+
}
|
|
548
|
+
const row = entry.row;
|
|
549
|
+
return bandOf(row) === null ? card(row) : bandCard(row);
|
|
550
|
+
})) }), totals === undefined ? null : (
|
|
551
|
+
/* The footer's aggregates, one per line with the column's name: under a card
|
|
552
|
+
there is no column to stand under. */
|
|
553
|
+
_jsx("dl", { className: `mx-1 flex flex-col gap-1 border-t border-line-strong ${pad}`, children: columns.map((c, i) => {
|
|
554
|
+
const total = totals[i];
|
|
555
|
+
if (total === null || total === undefined)
|
|
556
|
+
return null;
|
|
557
|
+
return (_jsxs("div", { className: "flex items-baseline justify-between gap-3 text-meta", children: [_jsx("dt", { className: "text-muted", children: c.header }), _jsx("dd", { className: "font-semibold text-ink tabular-nums", children: total })] }, c.id));
|
|
558
|
+
}) }))] }));
|
|
559
|
+
return (_jsxs("div", { ref: frame, className: "min-w-0", children: [_jsx("div", { ref: box.ref, className: `min-w-0 ${box.className}`, style: box.style, children: stacked ? (list) : (_jsxs("table", { className: "w-full border-separate border-spacing-0 px-1 text-left", children: [_jsx("thead", { className: "sticky z-10 bg-ground", style: stickyTop, children: _jsxs("tr", { children: [columns.map((c) => (_jsx("th", { scope: "col", style: c.width === undefined ? undefined : { width: c.width }, className: `border-b border-line-strong text-meta font-medium whitespace-nowrap text-muted ${cell} ${c.numeric === true ? "text-right" : "text-left"}`, children: c.header }, c.id))), onRemove === undefined ? null : (_jsx("th", { className: "w-0 border-b border-line-strong" }))] }) }), _jsx("tbody", { children: rows.length === 0 ? (_jsx("tr", { children: _jsx("td", { colSpan: columns.length + (onRemove === undefined ? 0 : 1), className: `text-meta text-muted italic ${cell}`, children: emptyText }) })) : (drawn.map((entry) => {
|
|
560
|
+
if (entry.pile !== undefined) {
|
|
561
|
+
const pile = entry.pile;
|
|
562
|
+
const inside = pile.piles.length === 0 ? pile.items : pileItems(pile.piles);
|
|
563
|
+
return (
|
|
564
|
+
/* La banda respeta la cuadrícula: el rótulo en la primera
|
|
565
|
+
columna y cada total bajo la suya. Un total en un rincón
|
|
566
|
+
obliga a buscar de qué columna es; alineado no hay nada que
|
|
567
|
+
buscar. */
|
|
568
|
+
_jsxs("tr", { children: [columns.map((c, ci) => (_jsx("td", { className: `bg-ground text-meta ${cell} ${c.numeric === true ? "text-right tabular-nums" : ""}`, style: ci === 0 && pile.depth > 0
|
|
569
|
+
? {
|
|
570
|
+
paddingLeft: `calc(var(--spacing-indent) * ${pile.depth} + 0.625rem)`,
|
|
571
|
+
}
|
|
572
|
+
: undefined, children: ci === 0
|
|
573
|
+
? pileTag(pile, copy.locale)
|
|
574
|
+
: (groupTotal?.(c, inside) ?? null) }, c.id))), onRemove === undefined ? null : _jsx("td", { className: "w-0 bg-ground" })] }, `pile:${pathKey(pile.path)}`));
|
|
575
|
+
}
|
|
576
|
+
const row = entry.row;
|
|
577
|
+
const id = getRowId(row);
|
|
578
|
+
const isEditing = editing === id;
|
|
579
|
+
const band = bandOf(row);
|
|
580
|
+
if (band !== null) {
|
|
581
|
+
const { span, figures } = bandFigures(row);
|
|
582
|
+
const bandWash = isEditing
|
|
583
|
+
? "bg-primary/5"
|
|
584
|
+
: band === "section"
|
|
585
|
+
? "bg-ground group-hover/row:bg-ink/6"
|
|
586
|
+
: "group-hover/row:bg-ink/4";
|
|
587
|
+
const bandMark = `border-b border-line first:rounded-l-control last:rounded-r-control ${bandWash}`;
|
|
588
|
+
const control = isEditing && editable && bands?.edit !== undefined;
|
|
589
|
+
return (_jsxs(EditRow, { as: "tr", ...rowWiring(id, row), className: `group/row ${editable ? RING : ""} ${editable && !isEditing ? "cursor-text" : ""}`, children: [_jsx("td", { colSpan: span, ...(control ? { "data-edit": "" } : {}), className: `text-meta ${bandMark} ${cell} ${band === "section"
|
|
590
|
+
? "font-semibold text-ink"
|
|
591
|
+
: "text-muted italic"}`, children: control ? bands?.edit?.(row, setter(id)) : bands?.render(row) }), columns.slice(span).map((c, i) => (_jsx("td", { className: `text-meta text-ink ${bandMark} ${cell} ${c.numeric === true
|
|
592
|
+
? "text-right font-semibold tabular-nums"
|
|
593
|
+
: ""}`, children: figures[span + i] ?? null }, c.id))), onRemove === undefined ? null : (_jsx("td", { className: `w-0 py-1 pr-2 pl-1 align-middle ${bandMark}`, children: _jsx("span", { className: "flex opacity-0 transition-opacity duration-(--duration-instant) group-hover/row:opacity-100 group-focus-visible/row:opacity-100 focus-within:opacity-100", onClick: (e) => e.stopPropagation(), children: _jsx(IconButton, { icon: Trash2, label: copy.editable.remove, size: "sm", tone: "danger", ...(editable && stop !== id ? { tabIndex: -1 } : {}), onClick: () => onRemove(id) }) }) }))] }, id));
|
|
594
|
+
}
|
|
595
|
+
/* The wash lives on the cells. A background on the row paints
|
|
596
|
+
edge to edge, and `border-collapse` would ignore a radius. */
|
|
597
|
+
const wash = isEditing ? "bg-primary/5" : "group-hover/row:bg-ink/4";
|
|
598
|
+
const mark = `border-b border-line first:rounded-l-control last:rounded-r-control ${wash}`;
|
|
599
|
+
const set = setter(id);
|
|
600
|
+
return (_jsxs(EditRow, { as: "tr", ...rowWiring(id, row),
|
|
601
|
+
/* The focus ring is drawn on the row, inset, so it is not cut by
|
|
602
|
+
the box and does not move the cells. */
|
|
603
|
+
className: `group/row ${editable ? RING : ""} ${editable && !isEditing ? "cursor-text" : ""}`, children: [columns.map((c, ci) => {
|
|
604
|
+
const control = isEditing && c.edit !== undefined && editable;
|
|
605
|
+
const mark = ci === 0 ? flagOf(row, true) : null;
|
|
606
|
+
return (_jsx("td", { ...(control ? { "data-edit": "" } : {}), className: `text-meta text-ink ${mark} ${cell} ${c.numeric === true ? "text-right tabular-nums" : ""}`, children: mark === null ? (control ? (c.edit?.(row, set)) : (c.render(row))) : (_jsxs("span", { className: "flex min-w-0 items-start gap-1.5", children: [mark, _jsx("span", { className: "min-w-0 flex-1", children: control ? c.edit?.(row, set) : c.render(row) })] })) }, c.id));
|
|
607
|
+
}), onRemove === undefined ? null : (_jsx("td", { className: `w-0 py-1 pr-2 pl-1 align-middle ${mark}`, children: _jsx("span", { className: "flex opacity-0 transition-opacity duration-(--duration-instant) group-hover/row:opacity-100 group-focus-visible/row:opacity-100 focus-within:opacity-100", onClick: (e) => e.stopPropagation(), children: _jsx(IconButton, { icon: Trash2, label: copy.editable.remove, size: "sm", tone: "danger", ...(editable && stop !== id ? { tabIndex: -1 } : {}), onClick: () => onRemove(id) }) }) }))] }, id));
|
|
608
|
+
})) }), totals === undefined ? null : (_jsx("tfoot", { children: _jsxs("tr", { children: [columns.map((c, i) => (_jsx("td", { className: `border-t border-line-strong text-meta font-semibold text-ink ${cell} ${c.numeric === true ? "text-right tabular-nums" : ""}`, children: totals[i] ?? null }, c.id))), onRemove === undefined ? null : (_jsx("td", { className: "border-t border-line-strong" }))] }) }))] })) }), onAdd === undefined && (adds === undefined || adds.length === 0) ? null : (
|
|
609
|
+
/* The «add» goes under the grid, as text, not as a button away from it:
|
|
610
|
+
what is about to happen is that one more row shows up right here.
|
|
611
|
+
The hover is the same inset mark as a row: the shell lines the label
|
|
612
|
+
up with the cells, and the button itself is the rounded wash. It sits
|
|
613
|
+
outside the scroll box, so a table with its own height keeps it in view.
|
|
614
|
+
A section and a note sit beside it, quieter: the line is what is added
|
|
615
|
+
most, so it keeps the lead and the whole free width. */
|
|
616
|
+
_jsxs("div", { className: "flex flex-wrap items-center gap-x-1 px-1 py-0.5", children: [onAdd === undefined ? null : (_jsxs("button", { ref: addButton, type: "button", onClick: () => add(), className: `flex items-center gap-1.5 rounded-control text-left text-meta text-primary transition-colors duration-(--duration-instant) hover:bg-primary/5 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-primary ${adds === undefined || adds.length === 0 ? "w-full" : ""} ${byDensity(d, "px-3 py-1", "px-2 py-0.5")}`, children: [_jsx(Plus, { className: "size-(--size-icon-sm)", strokeWidth: 2.5, "aria-hidden": true }), addText] })), (adds ?? []).map((extra) => (_jsx("button", { type: "button", onClick: () => add(extra.onAdd), className: `rounded-control text-left text-meta text-muted transition-colors duration-(--duration-instant) hover:bg-primary/5 hover:text-primary focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-primary ${byDensity(d, "px-3 py-1", "px-2 py-0.5")}`, children: extra.label }, extra.label)))] }))] }));
|
|
617
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type Pivot, type PivotKey } from "../../foundations/pivot.js";
|
|
2
|
+
export type PivotTableProps = {
|
|
3
|
+
pivot: Pivot;
|
|
4
|
+
/** Qué pasa al tocar un cruce. Sin esto los números no son botones: una celda
|
|
5
|
+
* que no lleva a ningún lado no tiene por qué prometer que sí. */
|
|
6
|
+
onCell?: (row: PivotKey, column: PivotKey) => void;
|
|
7
|
+
/** Con alto, la grilla scrollea sola y el encabezado se pega a su borde; sin
|
|
8
|
+
* alto scrollea la página. Es la misma regla que las otras tres tablas y vive
|
|
9
|
+
* entera en `useScrollBox`. */
|
|
10
|
+
height?: number | string;
|
|
11
|
+
/** Qué es esta grilla. Se lee en voz alta y se dibuja. */
|
|
12
|
+
caption?: string;
|
|
13
|
+
};
|
|
14
|
+
export declare function PivotTable({ pivot, onCell, height, caption }: PivotTableProps): import("react").JSX.Element;
|
|
Binary file
|