@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,38 @@
|
|
|
1
|
+
export type ReportRowKind = "section" | "group" | "line" | "total";
|
|
2
|
+
export interface ReportRow {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
/** The indent. It comes from the consumer and not from the nesting, because a
|
|
6
|
+
* second-level total lives next to its siblings and not inside them. */
|
|
7
|
+
level?: number;
|
|
8
|
+
kind?: ReportRowKind;
|
|
9
|
+
/** One value per column, in the same order. `null` is «not applicable» and is
|
|
10
|
+
* drawn empty — different from `0`, which is an amount. */
|
|
11
|
+
values: readonly (number | null)[];
|
|
12
|
+
/** What hangs underneath when it opens. Its presence is what puts the chevron
|
|
13
|
+
* there. */
|
|
14
|
+
children?: readonly ReportRow[];
|
|
15
|
+
/** The note on the line: what goes into this account, where the number comes
|
|
16
|
+
* from. */
|
|
17
|
+
note?: string;
|
|
18
|
+
onClick?: () => void;
|
|
19
|
+
}
|
|
20
|
+
export interface ReportColumn {
|
|
21
|
+
id: string;
|
|
22
|
+
header: string;
|
|
23
|
+
/** For a percentage or a variation column, which is not formatted the same
|
|
24
|
+
* way. */
|
|
25
|
+
format?: (value: number) => string;
|
|
26
|
+
}
|
|
27
|
+
export declare function ReportTable({ columns, rows,
|
|
28
|
+
/** Which groups start open. By default, none: a balance sheet with everything
|
|
29
|
+
* expanded is four hundred lines, and what gets looked at first are the
|
|
30
|
+
* totals. */
|
|
31
|
+
defaultOpen, caption, height, }: {
|
|
32
|
+
columns: readonly ReportColumn[];
|
|
33
|
+
rows: readonly ReportRow[];
|
|
34
|
+
defaultOpen?: readonly string[];
|
|
35
|
+
/** What says what this report is about. It is read out loud and it is drawn. */
|
|
36
|
+
caption?: string;
|
|
37
|
+
height?: number;
|
|
38
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { currentCopyLocale, fillCopy, useUiCopy } from "../../foundations/copy.js";
|
|
4
|
+
import { byDensity, useDensity } from "../../foundations/density.js";
|
|
5
|
+
import { stickyTop, useScrollBox } from "../../foundations/sticky.js";
|
|
6
|
+
import { ChevronRight, Info } from "lucide-react";
|
|
7
|
+
import { useState } from "react";
|
|
8
|
+
/* `useGrouping: "always"` and not the default: without it, four-digit numbers
|
|
9
|
+
lose the thousands separator —`2023.04` next to `258,015.31` in en-US, or
|
|
10
|
+
`2023,04` next to `258.015,31` in es-ES— and in a column of amounts that
|
|
11
|
+
inconsistency reads as a data error. */
|
|
12
|
+
function amountFormat(locale) {
|
|
13
|
+
return new Intl.NumberFormat(locale, {
|
|
14
|
+
minimumFractionDigits: 2,
|
|
15
|
+
maximumFractionDigits: 2,
|
|
16
|
+
/* The assertion is because of the type definitions, not because of the
|
|
17
|
+
engine: `"always"` is ES2023 and TypeScript's `lib` still declares
|
|
18
|
+
`useGrouping` as a boolean. The runtime understands it, and a boolean here
|
|
19
|
+
is not enough — `true` still leaves four-digit numbers without a
|
|
20
|
+
separator. */
|
|
21
|
+
useGrouping: "always",
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
const INDENT = 1.25;
|
|
25
|
+
/** The look of each kind of row. It is up here and not spread across the JSX
|
|
26
|
+
* because it is THE decision table of the component: it reads in one go and it
|
|
27
|
+
* changes in one place.
|
|
28
|
+
*
|
|
29
|
+
* `cell` goes on EVERY cell and not on the `<tr>`: the first column is `sticky`
|
|
30
|
+
* and needs a background of its own to cover what passes underneath, so a
|
|
31
|
+
* background on the row plus another one on the pinned cell gives two layers of
|
|
32
|
+
* the same alpha and the band comes out of two greys. The hover of a clickable
|
|
33
|
+
* row is the same: a layer inside each cell, rounded at the two ends, so the
|
|
34
|
+
* pinned name stays opaque and the mark does not run off the sheet. */
|
|
35
|
+
const SKIN = {
|
|
36
|
+
section: { row: "", cell: "bg-ink/8", text: "font-semibold text-ink" },
|
|
37
|
+
group: { row: "", cell: "bg-surface", text: "font-medium text-ink" },
|
|
38
|
+
line: { row: "", cell: "bg-surface", text: "text-ink" },
|
|
39
|
+
total: {
|
|
40
|
+
row: "border-t border-line-strong",
|
|
41
|
+
cell: "bg-surface",
|
|
42
|
+
text: "font-semibold text-ink",
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
function Amount({ value, format }) {
|
|
46
|
+
if (value === null)
|
|
47
|
+
return _jsx("span", { className: "text-line-strong", children: "\u00B7" });
|
|
48
|
+
const zero = value === 0;
|
|
49
|
+
const negative = value < 0;
|
|
50
|
+
return (_jsx("span", { className: `tabular-nums ${zero ? "text-muted" : negative ? "text-bad" : "text-ink"}`, children: format === undefined ? amountFormat(currentCopyLocale()).format(value) : format(value) }));
|
|
51
|
+
}
|
|
52
|
+
function ReportLine({ row, columns, open, onToggle, compact, }) {
|
|
53
|
+
const copy = useUiCopy();
|
|
54
|
+
const kind = row.kind ?? "line";
|
|
55
|
+
const skin = SKIN[kind];
|
|
56
|
+
const level = row.level ?? 0;
|
|
57
|
+
const collapsible = row.children !== undefined && row.children.length > 0;
|
|
58
|
+
const isOpen = open.has(row.id);
|
|
59
|
+
const clickable = row.onClick !== undefined;
|
|
60
|
+
return (_jsxs(_Fragment, { children: [_jsxs("tr", { className: `group/row ${skin.row} ${clickable ? "cursor-pointer" : ""}`, onClick: row.onClick, children: [_jsxs("th", { scope: "row",
|
|
61
|
+
/* `sticky left-0` so that the account name does not go away when you
|
|
62
|
+
scroll twelve comparison columns to the right. An amount without its
|
|
63
|
+
line is a loose number. */
|
|
64
|
+
className: `sticky left-0 z-10 text-left font-normal ${skin.cell} ${compact ? "py-1 pr-4 pl-2" : "py-1.5 pr-6 pl-3"}`, style: { paddingLeft: `${(compact ? 0.5 : 0.75) + level * INDENT}rem` }, children: [clickable ? (_jsx("span", { "aria-hidden": true, className: "pointer-events-none absolute inset-0 rounded-l-control group-hover/row:bg-primary/5" })) : null, _jsxs("span", { className: `relative flex min-w-0 items-center gap-1.5 text-meta ${skin.text}`, children: [collapsible ? (_jsx("button", { type: "button", onClick: (e) => {
|
|
65
|
+
e.stopPropagation();
|
|
66
|
+
onToggle(row.id);
|
|
67
|
+
}, "aria-expanded": isOpen, "aria-label": fillCopy(isOpen ? copy.report.close : copy.report.open, {
|
|
68
|
+
name: row.label,
|
|
69
|
+
}), className: "-ml-1 inline-flex size-4 shrink-0 items-center justify-center rounded-pill text-muted transition-colors duration-(--duration-instant) hover:bg-ink/8 hover:text-ink focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-primary", children: _jsx(ChevronRight, { className: `size-(--size-icon-sm) transition-transform duration-(--duration-instant) ${isOpen ? "rotate-90" : ""}`, strokeWidth: 2 }) })) : (
|
|
70
|
+
/* The slot for the chevron is reserved anyway: without it, a leaf
|
|
71
|
+
account and a collapsible one would start on different
|
|
72
|
+
verticals and the hierarchy would stop reading. */
|
|
73
|
+
_jsx("span", { className: "-ml-1 size-4 shrink-0", "aria-hidden": true })), _jsx("span", { className: "truncate", children: row.label })] })] }), columns.map((c, i) => (_jsxs("td", { className: `text-right text-meta whitespace-nowrap ${clickable ? "relative" : ""} ${skin.cell} ${compact ? "px-2 py-1" : "px-3 py-1.5"}`, children: [clickable ? (_jsx("span", { "aria-hidden": true, className: "pointer-events-none absolute inset-0 group-hover/row:bg-primary/5" })) : null, kind === "section" ? null : (_jsx("span", { className: "relative", children: _jsx(Amount, { value: row.values[i] ?? null, ...(c.format === undefined ? {} : { format: c.format }) }) }))] }, c.id))), _jsxs("td", { className: `w-0 ${clickable ? "relative" : ""} ${skin.cell} ${compact ? "pr-2" : "pr-3"}`, children: [clickable ? (_jsx("span", { "aria-hidden": true, className: "pointer-events-none absolute inset-0 rounded-r-control group-hover/row:bg-primary/5" })) : null, row.note === undefined ? null : (_jsx("span", { title: row.note, className: "relative inline-flex text-muted transition-colors duration-(--duration-instant) hover:text-ink", children: _jsx(Info, { className: "size-(--size-icon-sm)", strokeWidth: 2, "aria-label": row.note }) }))] })] }), !collapsible || !isOpen
|
|
74
|
+
? null
|
|
75
|
+
: row.children?.map((child) => (_jsx(ReportLine, { row: child, columns: columns, open: open, onToggle: onToggle, compact: compact }, child.id)))] }));
|
|
76
|
+
}
|
|
77
|
+
export function ReportTable({ columns, rows,
|
|
78
|
+
/** Which groups start open. By default, none: a balance sheet with everything
|
|
79
|
+
* expanded is four hundred lines, and what gets looked at first are the
|
|
80
|
+
* totals. */
|
|
81
|
+
defaultOpen, caption, height, }) {
|
|
82
|
+
const copy = useUiCopy();
|
|
83
|
+
const d = useDensity();
|
|
84
|
+
const compact = d === "compact";
|
|
85
|
+
const [open, setOpen] = useState(() => new Set(defaultOpen ?? []));
|
|
86
|
+
const caja = useScrollBox(height === undefined ? {} : { height });
|
|
87
|
+
function toggle(id) {
|
|
88
|
+
setOpen((prev) => {
|
|
89
|
+
const n = new Set(prev);
|
|
90
|
+
if (n.has(id))
|
|
91
|
+
n.delete(id);
|
|
92
|
+
else
|
|
93
|
+
n.add(id);
|
|
94
|
+
return n;
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return (
|
|
98
|
+
/* Misma decisión que las otras dos tablas, en el mismo lugar: con alto la
|
|
99
|
+
caja scrollea y la pila arranca de cero adentro; sin alto y entrando, no
|
|
100
|
+
hay caja y el encabezado se pega debajo de la barra de la lista. Antes
|
|
101
|
+
esto era `overflow-auto` siempre, así que el encabezado heredaba el
|
|
102
|
+
desplazamiento de la barra y quedaba empujado 68 px hacia adentro de su
|
|
103
|
+
propia caja — banda en blanco arriba y títulos de columna cayendo entre
|
|
104
|
+
las filas. */
|
|
105
|
+
_jsx("div", { ref: caja.ref, className: `min-w-0 ${caja.className}`, style: caja.style, children: _jsxs("table", { className: "w-full border-collapse px-1", children: [caption === undefined ? null : (_jsx("caption", { className: `text-left text-meta text-muted ${byDensity(d, "px-3 pb-2", "px-2 pb-1.5")}`, children: caption })), _jsx("thead", { className: "sticky z-20 bg-ground", style: stickyTop, children: _jsxs("tr", { className: "border-b border-line-strong", children: [_jsx("th", { className: `sticky left-0 z-10 bg-ground text-left ${compact ? "px-2 py-1.5" : "px-3 py-2"}`, children: _jsx("span", { className: "sr-only", children: copy.report.concept }) }), columns.map((c) => (_jsx("th", { scope: "col", className: `text-right text-meta font-medium whitespace-nowrap text-muted ${compact ? "px-2 py-1.5" : "px-3 py-2"}`, children: c.header }, c.id))), _jsx("th", { className: "w-0" })] }) }), _jsx("tbody", { children: rows.map((r) => (_jsx(ReportLine, { row: r, columns: columns, open: open, onToggle: toggle, compact: compact }, r.id))) })] }) }));
|
|
106
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { LucideIcon } from "lucide-react";
|
|
2
|
+
export interface RowAction {
|
|
3
|
+
id: string;
|
|
4
|
+
/** What is read out loud, in the `title` and in the menu. */
|
|
5
|
+
label: string;
|
|
6
|
+
icon: LucideIcon;
|
|
7
|
+
tone?: "normal" | "action" | "danger";
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
onSelect: () => void;
|
|
10
|
+
}
|
|
11
|
+
export declare function RowActions({ actions,
|
|
12
|
+
/** How many stay visible before the rest fall into the `⋯`. */
|
|
13
|
+
inline, }: {
|
|
14
|
+
actions: readonly RowAction[];
|
|
15
|
+
inline?: number;
|
|
16
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useUiCopy } from "../../foundations/copy.js";
|
|
4
|
+
import { IconButton } from "../button/Button.js";
|
|
5
|
+
import { Menu } from "../menu/Menu.js";
|
|
6
|
+
import { MoreHorizontal } from "lucide-react";
|
|
7
|
+
export function RowActions({ actions,
|
|
8
|
+
/** How many stay visible before the rest fall into the `⋯`. */
|
|
9
|
+
inline = 2, }) {
|
|
10
|
+
const copy = useUiCopy();
|
|
11
|
+
if (actions.length === 0)
|
|
12
|
+
return _jsx("td", { className: "w-0" });
|
|
13
|
+
/* With only one left over there is no `⋯`: a menu with a single item is one
|
|
14
|
+
extra click to reach the same button. */
|
|
15
|
+
const inlineActions = actions.length <= inline + 1 ? actions : actions.slice(0, inline);
|
|
16
|
+
const overflow = actions.slice(inlineActions.length);
|
|
17
|
+
const byId = new Map(actions.map((a) => [a.id, a]));
|
|
18
|
+
return (_jsx("td", { className: "w-0 py-1 pr-2 pl-1 align-middle whitespace-nowrap", children: _jsxs("div", { onClick: (e) => e.stopPropagation(), className: "flex items-center justify-end gap-0.5 opacity-0 transition-opacity duration-(--duration-instant) group-hover/row:opacity-100 focus-within:opacity-100", children: [inlineActions.map((a) => (_jsx(IconButton, { icon: a.icon, label: a.label, size: "sm", ...(a.tone === undefined ? {} : { tone: a.tone }), ...(a.disabled === undefined ? {} : { disabled: a.disabled }), onClick: a.onSelect }, a.id))), overflow.length === 0 ? null : (_jsx(Menu, { label: copy.selection.more, icon: MoreHorizontal, iconOnly: true, chevron: false, align: "end", groups: [
|
|
19
|
+
{
|
|
20
|
+
options: overflow.map((a) => ({
|
|
21
|
+
id: a.id,
|
|
22
|
+
label: a.label,
|
|
23
|
+
icon: a.icon,
|
|
24
|
+
...(a.disabled === undefined ? {} : { disabled: a.disabled }),
|
|
25
|
+
})),
|
|
26
|
+
},
|
|
27
|
+
], onSelect: (id) => byId.get(id)?.onSelect() }))] }) }));
|
|
28
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { Tone } from "../../foundations/types.js";
|
|
2
|
+
import { type ReactNode } from "react";
|
|
3
|
+
export interface SheetColumn {
|
|
4
|
+
id: string;
|
|
5
|
+
/** The name of the column, just as it came from the file. */
|
|
6
|
+
header: string;
|
|
7
|
+
/** The detected type, underneath the name. It is half the value of a glance:
|
|
8
|
+
* seeing that `importe` was read as text explains the rest of the screen. */
|
|
9
|
+
type?: string;
|
|
10
|
+
numeric?: boolean;
|
|
11
|
+
width?: number;
|
|
12
|
+
}
|
|
13
|
+
export declare function SheetTable({ columns, rows,
|
|
14
|
+
/** The value of a cell, by index. It is a function and not a prebuilt matrix
|
|
15
|
+
* so that the consumer can read from wherever they want without copying a
|
|
16
|
+
* million rows into another structure. */
|
|
17
|
+
value,
|
|
18
|
+
/** Which cells are painted and with what. `undefined` is unpainted, which is
|
|
19
|
+
* the normal case — a spreadsheet with everything colored highlights
|
|
20
|
+
* nothing. */
|
|
21
|
+
tone,
|
|
22
|
+
/** What the cell says on hover: «imputado por la mediana». */
|
|
23
|
+
note, height, rowLabel, band, onBandToggle, caption, onCommit, editable, onAddRow, }: {
|
|
24
|
+
columns: readonly SheetColumn[];
|
|
25
|
+
rows: number;
|
|
26
|
+
value: (row: number, column: SheetColumn, columnIndex: number) => string | number | null;
|
|
27
|
+
tone?: (row: number, column: SheetColumn, columnIndex: number) => Tone | undefined;
|
|
28
|
+
note?: (row: number, column: SheetColumn, columnIndex: number) => string | undefined;
|
|
29
|
+
/** Pixel height of the scroll box. `"auto"` grows with the rows so a
|
|
30
|
+
* twenty-row glance is not a window with its own scrollbar. */
|
|
31
|
+
height?: number | "auto";
|
|
32
|
+
rowLabel?: (row: number) => string;
|
|
33
|
+
/**
|
|
34
|
+
* Esta fila no es una fila: es la cabecera del montón que empieza acá.
|
|
35
|
+
*
|
|
36
|
+
* Agrupar sin perder la virtualización es esto — **una banda es una fila
|
|
37
|
+
* más, de otra clase**. La alternativa era una planilla por montón, y eso
|
|
38
|
+
* son N cuadrículas con N encabezados y N scrolls para un solo conjunto.
|
|
39
|
+
*
|
|
40
|
+
* `depth` es cuán adentro está el montón. La planilla no puede recibir un
|
|
41
|
+
* árbol —está virtualizada por índice, y un índice es una posición en una
|
|
42
|
+
* secuencia—, así que la jerarquía la dice la sangría de cada banda y no el
|
|
43
|
+
* anidamiento del marcado.
|
|
44
|
+
*/
|
|
45
|
+
band?: (row: number) => {
|
|
46
|
+
field?: string;
|
|
47
|
+
label: string;
|
|
48
|
+
count: number;
|
|
49
|
+
open: boolean;
|
|
50
|
+
depth?: number;
|
|
51
|
+
/**
|
|
52
|
+
* Lo que el montón suma EN CADA COLUMNA, en la posición de esa columna.
|
|
53
|
+
*
|
|
54
|
+
* Una banda que pone su total en un rincón obliga a buscar a qué columna
|
|
55
|
+
* corresponde el número. Alineado bajo su columna no hay nada que buscar:
|
|
56
|
+
* la cuadrícula ya dice qué significa, que es para lo que existe una
|
|
57
|
+
* cuadrícula. `null` en una columna que no suma — que es la mayoría.
|
|
58
|
+
*/
|
|
59
|
+
total?: (column: SheetColumn, columnIndex: number) => string | number | null;
|
|
60
|
+
} | undefined;
|
|
61
|
+
onBandToggle?: (row: number) => void;
|
|
62
|
+
caption?: ReactNode;
|
|
63
|
+
/** Present = the spreadsheet is editable. It receives the raw text just as it
|
|
64
|
+
* was typed; converting it to the column's type belongs to the domain, not
|
|
65
|
+
* here. */
|
|
66
|
+
onCommit?: (row: number, column: SheetColumn, columnIndex: number, next: string) => void;
|
|
67
|
+
/** Which cells are editable. Without this, all of them — but a computed column
|
|
68
|
+
* or an identifier is not touched. */
|
|
69
|
+
editable?: (row: number, column: SheetColumn, columnIndex: number) => boolean;
|
|
70
|
+
/** Present = rows can be added (see the file's header). Only with `onCommit`: a row
|
|
71
|
+
* that cannot be written in is not worth adding. */
|
|
72
|
+
onAddRow?: () => void;
|
|
73
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useUiCopy } from "../../foundations/copy.js";
|
|
4
|
+
import { stickyTop, useScrollBox } from "../../foundations/sticky.js";
|
|
5
|
+
import { Cell } from "../value/Value.js";
|
|
6
|
+
import { useVirtualizer } from "@tanstack/react-virtual";
|
|
7
|
+
import { ChevronRight, Plus } from "lucide-react";
|
|
8
|
+
import { useEffect, useState } from "react";
|
|
9
|
+
const TONE = {
|
|
10
|
+
neutral: "bg-ink/6",
|
|
11
|
+
info: "bg-info/12",
|
|
12
|
+
good: "bg-good/12",
|
|
13
|
+
warn: "bg-warn/15",
|
|
14
|
+
bad: "bg-bad/12",
|
|
15
|
+
accent: "bg-accent/20",
|
|
16
|
+
};
|
|
17
|
+
const WIDTH = 168;
|
|
18
|
+
const HEIGHT = 28;
|
|
19
|
+
export function SheetTable({ columns, rows,
|
|
20
|
+
/** The value of a cell, by index. It is a function and not a prebuilt matrix
|
|
21
|
+
* so that the consumer can read from wherever they want without copying a
|
|
22
|
+
* million rows into another structure. */
|
|
23
|
+
value,
|
|
24
|
+
/** Which cells are painted and with what. `undefined` is unpainted, which is
|
|
25
|
+
* the normal case — a spreadsheet with everything colored highlights
|
|
26
|
+
* nothing. */
|
|
27
|
+
tone,
|
|
28
|
+
/** What the cell says on hover: «imputado por la mediana». */
|
|
29
|
+
note, height = 420, rowLabel, band, onBandToggle, caption, onCommit, editable, onAddRow, }) {
|
|
30
|
+
const copy = useUiCopy();
|
|
31
|
+
const labelOf = rowLabel ?? ((i) => (i + 1).toLocaleString(copy.locale));
|
|
32
|
+
/* `"auto"` es para un conjunto que ya vino CORTADO —una página de ochenta— y
|
|
33
|
+
deja de virtualizar de hecho: sin scroll propio, la ventana visible es la
|
|
34
|
+
caja entera y se dibujan todas las filas. Para los datos crudos —donde son
|
|
35
|
+
un millón— hay que declarar un alto, y ahí sí se dibujan las ~30 que se
|
|
36
|
+
ven. Cuándo la caja es un contenedor de scroll lo decide `useScrollBox`. */
|
|
37
|
+
const caja = useScrollBox(height === "auto" ? {} : { height });
|
|
38
|
+
const v = useVirtualizer({
|
|
39
|
+
count: rows,
|
|
40
|
+
getScrollElement: () => caja.ref.current,
|
|
41
|
+
estimateSize: () => HEIGHT,
|
|
42
|
+
overscan: 12,
|
|
43
|
+
});
|
|
44
|
+
const items = v.getVirtualItems();
|
|
45
|
+
const [active, setActive] = useState(null);
|
|
46
|
+
const [draft, setDraft] = useState(null);
|
|
47
|
+
const editing = draft !== null;
|
|
48
|
+
const isEditable = onCommit !== undefined;
|
|
49
|
+
function canEdit(r, c) {
|
|
50
|
+
if (!isEditable)
|
|
51
|
+
return false;
|
|
52
|
+
const col = columns[c];
|
|
53
|
+
if (col === undefined)
|
|
54
|
+
return false;
|
|
55
|
+
return editable === undefined || editable(r, col, c);
|
|
56
|
+
}
|
|
57
|
+
function goTo(r, c) {
|
|
58
|
+
const rr = Math.max(0, Math.min(rows - 1, r));
|
|
59
|
+
const cc = Math.max(0, Math.min(columns.length - 1, c));
|
|
60
|
+
setActive({ r: rr, c: cc });
|
|
61
|
+
setDraft(null);
|
|
62
|
+
v.scrollToIndex(rr);
|
|
63
|
+
}
|
|
64
|
+
function openCell(r, c, initial) {
|
|
65
|
+
if (!canEdit(r, c))
|
|
66
|
+
return;
|
|
67
|
+
const col = columns[c];
|
|
68
|
+
setActive({ r, c });
|
|
69
|
+
setDraft(initial ?? String(value(r, col, c) ?? ""));
|
|
70
|
+
}
|
|
71
|
+
function commit(dr, dc) {
|
|
72
|
+
if (active === null || draft === null)
|
|
73
|
+
return;
|
|
74
|
+
const col = columns[active.c];
|
|
75
|
+
if (col !== undefined)
|
|
76
|
+
onCommit?.(active.r, col, active.c, draft);
|
|
77
|
+
setDraft(null);
|
|
78
|
+
if (dr !== 0 || dc !== 0)
|
|
79
|
+
goTo(active.r + dr, active.c + dc);
|
|
80
|
+
}
|
|
81
|
+
/* The column the active cell waits in until the added row arrives, and how many rows
|
|
82
|
+
there were when it was asked for. */
|
|
83
|
+
const canAdd = isEditable && onAddRow !== undefined;
|
|
84
|
+
const [arriving, setArriving] = useState(null);
|
|
85
|
+
function addRow(c) {
|
|
86
|
+
if (!canAdd)
|
|
87
|
+
return;
|
|
88
|
+
setArriving({ c, from: rows });
|
|
89
|
+
onAddRow?.();
|
|
90
|
+
}
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
if (arriving === null || rows <= arriving.from)
|
|
93
|
+
return;
|
|
94
|
+
setArriving(null);
|
|
95
|
+
setActive({ r: rows - 1, c: arriving.c });
|
|
96
|
+
setDraft(null);
|
|
97
|
+
v.scrollToIndex(rows - 1);
|
|
98
|
+
caja.ref.current?.focus();
|
|
99
|
+
/* `v` and `caja` are new objects every render; the row count is what decides. */
|
|
100
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
101
|
+
}, [rows, arriving]);
|
|
102
|
+
/* All the keyboard handling in one place. Splitting it between the box and
|
|
103
|
+
each cell is how you end up with spreadsheets whose arrows sometimes move
|
|
104
|
+
and sometimes scroll. */
|
|
105
|
+
function onKey(e) {
|
|
106
|
+
if (active === null)
|
|
107
|
+
return;
|
|
108
|
+
const { r, c } = active;
|
|
109
|
+
if (editing) {
|
|
110
|
+
if (e.key === "Escape") {
|
|
111
|
+
e.preventDefault();
|
|
112
|
+
setDraft(null);
|
|
113
|
+
}
|
|
114
|
+
else if (e.key === "Enter") {
|
|
115
|
+
e.preventDefault();
|
|
116
|
+
if (!e.shiftKey && canAdd && r === rows - 1) {
|
|
117
|
+
commit(0, 0);
|
|
118
|
+
addRow(c);
|
|
119
|
+
}
|
|
120
|
+
else
|
|
121
|
+
commit(e.shiftKey ? -1 : 1, 0);
|
|
122
|
+
}
|
|
123
|
+
else if (e.key === "Tab") {
|
|
124
|
+
e.preventDefault();
|
|
125
|
+
commit(0, e.shiftKey ? -1 : 1);
|
|
126
|
+
}
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
switch (e.key) {
|
|
130
|
+
case "ArrowDown":
|
|
131
|
+
e.preventDefault();
|
|
132
|
+
goTo(r + 1, c);
|
|
133
|
+
return;
|
|
134
|
+
case "ArrowUp":
|
|
135
|
+
e.preventDefault();
|
|
136
|
+
goTo(r - 1, c);
|
|
137
|
+
return;
|
|
138
|
+
case "ArrowRight":
|
|
139
|
+
case "Tab":
|
|
140
|
+
e.preventDefault();
|
|
141
|
+
goTo(r, c + (e.shiftKey && e.key === "Tab" ? -1 : 1));
|
|
142
|
+
return;
|
|
143
|
+
case "ArrowLeft":
|
|
144
|
+
e.preventDefault();
|
|
145
|
+
goTo(r, c - 1);
|
|
146
|
+
return;
|
|
147
|
+
case "Enter":
|
|
148
|
+
case "F2":
|
|
149
|
+
e.preventDefault();
|
|
150
|
+
openCell(r, c);
|
|
151
|
+
return;
|
|
152
|
+
case "Delete":
|
|
153
|
+
case "Backspace":
|
|
154
|
+
e.preventDefault();
|
|
155
|
+
if (canEdit(r, c)) {
|
|
156
|
+
const col = columns[c];
|
|
157
|
+
if (col !== undefined)
|
|
158
|
+
onCommit?.(r, col, c, "");
|
|
159
|
+
}
|
|
160
|
+
return;
|
|
161
|
+
default:
|
|
162
|
+
/* Typing with the cell active starts editing and keeps the keystroke:
|
|
163
|
+
it is what anyone coming from a spreadsheet does, and losing the first
|
|
164
|
+
character feels like the app is not responding. */
|
|
165
|
+
if (e.key.length === 1 && !e.metaKey && !e.ctrlKey && !e.altKey) {
|
|
166
|
+
e.preventDefault();
|
|
167
|
+
openCell(r, c, e.key);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return (_jsxs("div", { className: "flex min-w-0 flex-col", children: [caption === undefined ? null : (_jsx("div", { className: "px-3 pb-2 text-meta text-muted", children: caption })), _jsx("div", { ref: caja.ref, style: caja.style, onKeyDown: isEditable ? onKey : undefined, tabIndex: isEditable ? 0 : undefined, className: `relative min-w-0 rounded-box border border-line bg-surface focus:outline-none ${caja.className}`, role: "grid", "aria-rowcount": rows, "aria-colcount": columns.length + 1, children: _jsxs("div", { style: {
|
|
172
|
+
minWidth: `${ROW_LABEL_WIDTH + columns.reduce((a, c) => a + (c.width ?? WIDTH), 0)}px`,
|
|
173
|
+
}, children: [_jsxs("div", { className: "sticky z-20 flex border-b border-line-strong bg-ground", style: stickyTop, role: "row", children: [_jsx("div", { className: "sticky left-0 z-30 shrink-0 border-r border-line bg-ground", style: { width: ROW_LABEL_WIDTH } }), columns.map((c) => (_jsxs("div", { role: "columnheader", style: { width: c.width ?? WIDTH }, className: `flex shrink-0 flex-col justify-center gap-0.5 border-r border-line px-2.5 py-1.5 ${c.numeric === true ? "items-end" : "items-start"}`, children: [_jsx("span", { className: "max-w-full truncate text-meta font-medium text-ink", title: c.header, children: c.header }), c.type === undefined ? null : (_jsx("span", { className: "max-w-full truncate font-mono text-micro text-muted", children: c.type }))] }, c.id))), _jsx("div", { className: "flex-1" })] }), _jsx("div", { style: { height: v.getTotalSize(), position: "relative" }, children: items.map((item) => {
|
|
174
|
+
const montón = band?.(item.index);
|
|
175
|
+
if (montón !== undefined) {
|
|
176
|
+
/* La banda queda pegada a la IZQUIERDA como el número de fila:
|
|
177
|
+
con cuarenta columnas, un rótulo que se va con el scroll
|
|
178
|
+
horizontal deja de decir en qué montón estás justo cuando
|
|
179
|
+
más lejos estás de él. */
|
|
180
|
+
return (
|
|
181
|
+
/* La banda RESPETA la cuadrícula: rótulo en la columna fija de
|
|
182
|
+
la izquierda, y después una celda por columna —vacía, salvo
|
|
183
|
+
que esa columna sume algo—. Antes era un rótulo suelto que
|
|
184
|
+
ignoraba las columnas, y entonces un total quedaba en un
|
|
185
|
+
rincón sin decir de qué columna era. */
|
|
186
|
+
_jsxs("div", { role: "row", "aria-rowindex": item.index + 1, className: "absolute top-0 left-0 flex w-full border-b border-line-strong bg-ground", style: { height: item.size, transform: `translateY(${item.start}px)` }, children: [_jsx("button", { type: "button", onClick: onBandToggle === undefined
|
|
187
|
+
? undefined
|
|
188
|
+
: () => onBandToggle(item.index), "aria-expanded": montón.open, className: "sticky left-0 z-10 flex min-w-0 shrink-0 items-center gap-1.5 border-r border-line bg-ground px-2.5 text-left", style: {
|
|
189
|
+
height: "100%",
|
|
190
|
+
/* El mismo ancho que la columna del número de fila, para
|
|
191
|
+
que el rótulo no corra la cuadrícula. */
|
|
192
|
+
width: ROW_LABEL_WIDTH,
|
|
193
|
+
...(montón.depth === undefined || montón.depth === 0
|
|
194
|
+
? {}
|
|
195
|
+
: {
|
|
196
|
+
paddingLeft: `calc(var(--spacing-indent) * ${montón.depth} + 0.625rem)`,
|
|
197
|
+
}),
|
|
198
|
+
}, children: onBandToggle === undefined ? null : (_jsx(ChevronRight, { className: `size-(--size-icon-sm) shrink-0 text-muted transition-transform duration-(--duration-instant) ${montón.open ? "rotate-90" : ""}`, strokeWidth: 2, "aria-hidden": true })) }), columns.map((c, ci) => {
|
|
199
|
+
const total = montón.total?.(c, ci) ?? null;
|
|
200
|
+
return (_jsx("div", { role: "gridcell", style: { width: c.width ?? WIDTH }, className: `flex shrink-0 items-center overflow-hidden border-r border-line px-2.5 whitespace-nowrap ${c.numeric === true ? "justify-end" : "justify-start"}`, children: ci === 0 ? (_jsxs("span", { className: "flex min-w-0 items-center gap-1.5", children: [montón.field === undefined ? null : (_jsx("span", { className: "shrink-0 text-micro text-muted/70", children: montón.field })), _jsx("span", { className: "truncate text-micro font-medium text-ink", children: montón.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: montón.count.toLocaleString(copy.locale) })] })) : total === null ? null : (_jsx("span", { className: "truncate text-meta font-medium tabular-nums text-ink", children: typeof total === "number"
|
|
201
|
+
? total.toLocaleString(copy.locale)
|
|
202
|
+
: total })) }, c.id));
|
|
203
|
+
})] }, item.key));
|
|
204
|
+
}
|
|
205
|
+
return (_jsxs("div", { role: "row", "aria-rowindex": item.index + 1, className: "absolute top-0 left-0 flex w-full border-b border-line", style: { height: item.size, transform: `translateY(${item.start}px)` }, children: [_jsx("div", { className: "sticky left-0 z-10 flex shrink-0 items-center justify-end border-r border-line bg-surface px-2 text-micro tabular-nums text-muted", style: { width: ROW_LABEL_WIDTH }, children: labelOf(item.index) }), columns.map((c, ci) => {
|
|
206
|
+
const t = tone?.(item.index, c, ci);
|
|
207
|
+
const n = note?.(item.index, c, ci);
|
|
208
|
+
const isActive = active !== null && active.r === item.index && active.c === ci;
|
|
209
|
+
const isEditingHere = isActive && editing;
|
|
210
|
+
return (_jsx("div", { role: "gridcell", title: n, "aria-selected": isActive, onClick: isEditable ? () => goTo(item.index, ci) : undefined, onDoubleClick: isEditable ? () => openCell(item.index, ci) : undefined, style: { width: c.width ?? WIDTH }, className: `relative flex shrink-0 items-center overflow-hidden border-r border-line px-2.5 text-meta tabular-nums whitespace-nowrap text-ink ${c.numeric === true ? "justify-end" : "justify-start"} ${t === undefined ? "" : TONE[t]} ${
|
|
211
|
+
/* The outline of the active cell goes on the inside
|
|
212
|
+
(`inset-ring`) and not on the outside: a 2 px border
|
|
213
|
+
would push the content and the whole row would dance
|
|
214
|
+
as you move around with the arrows. */
|
|
215
|
+
isActive
|
|
216
|
+
? "inset-ring-2 inset-ring-primary"
|
|
217
|
+
: isEditable
|
|
218
|
+
? "cursor-cell"
|
|
219
|
+
: ""}`, children: isEditingHere ? (_jsx("input", { autoFocus: true, value: draft, onChange: (ev) => setDraft(ev.target.value), onBlur: () => commit(0, 0), className: `absolute inset-0 w-full bg-surface px-2.5 text-meta tabular-nums text-ink outline-none ${c.numeric === true ? "text-right" : "text-left"}` })) : (_jsx(Cell, { value: value(item.index, c, ci), max: 24 })) }, c.id));
|
|
220
|
+
}), _jsx("div", { className: "flex-1" })] }, item.key));
|
|
221
|
+
}) })] }) }), canAdd ? (
|
|
222
|
+
/* Under the grid and outside its scroll box, like the editable table's «add»: in
|
|
223
|
+
view however far down a long sheet is scrolled. The new row's first cell that
|
|
224
|
+
can be written takes the place. */
|
|
225
|
+
_jsx("div", { className: "px-1 py-0.5", children: _jsxs("button", { type: "button", onClick: () => {
|
|
226
|
+
const first = columns.findIndex((_, ci) => canEdit(rows, ci));
|
|
227
|
+
addRow(first === -1 ? 0 : first);
|
|
228
|
+
}, className: "flex w-full items-center gap-1.5 rounded-control px-2 py-0.5 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", children: [_jsx(Plus, { className: "size-(--size-icon-sm)", strokeWidth: 2.5, "aria-hidden": true }), copy.editable.addRow] }) })) : null] }));
|
|
229
|
+
}
|
|
230
|
+
const ROW_LABEL_WIDTH = 64;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { Tone } from "../../foundations/types.js";
|
|
2
|
+
import { type ReactNode } from "react";
|
|
3
|
+
export type SortDirection = "asc" | "desc";
|
|
4
|
+
export interface SortState {
|
|
5
|
+
key: string;
|
|
6
|
+
direction: SortDirection;
|
|
7
|
+
}
|
|
8
|
+
export declare function Table({ children,
|
|
9
|
+
/** With a height, the table scrolls on its own and the header sticks.
|
|
10
|
+
* Without a height, the page scrolls and the header goes away. */
|
|
11
|
+
height, }: {
|
|
12
|
+
children: ReactNode;
|
|
13
|
+
/** A number means pixels; a string goes through as is, so `26rem` follows the
|
|
14
|
+
* system scale instead of pinning a size in pixels. */
|
|
15
|
+
height?: number | string;
|
|
16
|
+
}): import("react").JSX.Element;
|
|
17
|
+
export declare function THead({ children }: {
|
|
18
|
+
children: ReactNode;
|
|
19
|
+
}): import("react").JSX.Element;
|
|
20
|
+
/** A column. If it gets a `sortKey`, it can be sorted.
|
|
21
|
+
*
|
|
22
|
+
* The icon is ALWAYS there, dimmed when the column is not the sorted one: an
|
|
23
|
+
* indicator that only shows up once you sort does not say which columns are
|
|
24
|
+
* sortable, and then you have to try them one by one. */
|
|
25
|
+
export declare function Th({ children, sortKey, sort, onSort, numeric, align, width, }: {
|
|
26
|
+
children: ReactNode;
|
|
27
|
+
sortKey?: string;
|
|
28
|
+
sort?: SortState | null;
|
|
29
|
+
onSort?: (next: SortState | null) => void;
|
|
30
|
+
/** Numbers align to the right: that is what lets you compare orders of
|
|
31
|
+
* magnitude without reading the figures. */
|
|
32
|
+
numeric?: boolean;
|
|
33
|
+
/** The alignment, when it is NOT because it is a number. They are two
|
|
34
|
+
* different things: `numeric` says «this is a figure» and drags along the
|
|
35
|
+
* right alignment and the tabular figures; `align` only pushes. A status
|
|
36
|
+
* label on the right is not a number. */
|
|
37
|
+
align?: "left" | "right";
|
|
38
|
+
/** A number means pixels; a string goes through as is, so `6rem` follows the
|
|
39
|
+
* system scale instead of pinning a size in pixels. */
|
|
40
|
+
width?: number | string;
|
|
41
|
+
}): import("react").JSX.Element;
|
|
42
|
+
export declare function Tr({ children, onClick, selected, }: {
|
|
43
|
+
children: ReactNode;
|
|
44
|
+
onClick?: () => void;
|
|
45
|
+
selected?: boolean;
|
|
46
|
+
}): import("react").JSX.Element;
|
|
47
|
+
/**
|
|
48
|
+
* A row that opens its own detail underneath.
|
|
49
|
+
*
|
|
50
|
+
* Context: `DataTable` expands GROUPS —TanStack takes care of that— and that is
|
|
51
|
+
* not enough for the most common case in this product: a column that opens up
|
|
52
|
+
* to show its distribution. That is not a group, it is the detail of ONE row,
|
|
53
|
+
* and every view was writing it by hand with its own `colSpan` and its own
|
|
54
|
+
* chevron.
|
|
55
|
+
*
|
|
56
|
+
* The trigger is the first cell and not a column of chevrons: a whole column
|
|
57
|
+
* for a 12 px control is width that a twelve-column table needs.
|
|
58
|
+
*/
|
|
59
|
+
export declare function TrDetail({ children, detail, open, onToggle, columns, label, selected, }: {
|
|
60
|
+
children: ReactNode;
|
|
61
|
+
detail: ReactNode;
|
|
62
|
+
open: boolean;
|
|
63
|
+
onToggle: () => void;
|
|
64
|
+
/** How many columns the table has, so the detail spans all of them. */
|
|
65
|
+
columns: number;
|
|
66
|
+
/** What is being opened, for the screen reader: «distribución de importe». */
|
|
67
|
+
label: string;
|
|
68
|
+
selected?: boolean;
|
|
69
|
+
}): import("react").JSX.Element;
|
|
70
|
+
/** The background of a cell, by tone. It replaces the row wash on that cell:
|
|
71
|
+
* a hovered row and a painted cell are two signals, and stacking them reads
|
|
72
|
+
* as a third color. */
|
|
73
|
+
export declare const CELL_TONE: Record<Tone, string>;
|
|
74
|
+
export declare function Td({ children, numeric, align,
|
|
75
|
+
/** A CODE — an id, a hash, a key — set in the monospaced face. Off by default: a
|
|
76
|
+
* number does not need it (a numeric cell gets tabular figures on its own), and a
|
|
77
|
+
* name in mono reads like a log, not like a report. */
|
|
78
|
+
mono,
|
|
79
|
+
/** Paints the whole cell. For what the eye has to find without reading: an
|
|
80
|
+
* imputed value, one out of range, a row that changed.
|
|
81
|
+
*
|
|
82
|
+
* Context: it is the CELL and not the text. Tinting the text says «this
|
|
83
|
+
* number is of another kind»; tinting the cell says «look here», which is
|
|
84
|
+
* something else — and in a table of forty rows by six columns, colored text
|
|
85
|
+
* gets lost. */
|
|
86
|
+
tone, title, }: {
|
|
87
|
+
children: ReactNode;
|
|
88
|
+
numeric?: boolean;
|
|
89
|
+
/** Pushes without saying it is a number. See the note on `Th`. */
|
|
90
|
+
align?: "left" | "right";
|
|
91
|
+
mono?: boolean;
|
|
92
|
+
tone?: Tone;
|
|
93
|
+
/** Why it is painted. A colored cell with no explanation is a question. */
|
|
94
|
+
title?: string;
|
|
95
|
+
}): import("react").JSX.Element;
|
|
96
|
+
/** The selection checkbox, with its third state.
|
|
97
|
+
*
|
|
98
|
+
* «some» is not an ornament: without it, the header checkbox of a table with
|
|
99
|
+
* three of forty rows selected lies in both directions — empty says «none»
|
|
100
|
+
* and filled says «all». */
|
|
101
|
+
export declare function SelectBox({ checked, indeterminate, onChange, label, }: {
|
|
102
|
+
checked: boolean;
|
|
103
|
+
indeterminate?: boolean;
|
|
104
|
+
onChange?: (next: boolean) => void;
|
|
105
|
+
label: string;
|
|
106
|
+
}): import("react").JSX.Element;
|