@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,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A FRAME is a rectangle of values: columns × rows.
|
|
3
|
+
*
|
|
4
|
+
* It is not a domain record and it is not a set of records. ListView talks
|
|
5
|
+
* about N entities of the same kind (the Facturas list, a queue). Record talks
|
|
6
|
+
* about ONE entity. The frame talks about the PAYLOAD — the cells that a
|
|
7
|
+
* record carries, or that a glance extracted from a file — so they can be
|
|
8
|
+
* shown as a table or as a chart, and so a column can be hidden, without the
|
|
9
|
+
* design system knowing what an invoice is.
|
|
10
|
+
*
|
|
11
|
+
* The value is a function, not a copied matrix: the consumer already has the
|
|
12
|
+
* bytes somewhere and this layer only reads them.
|
|
13
|
+
*/
|
|
14
|
+
export type FrameCell = string | number | null;
|
|
15
|
+
export type FrameColumn = {
|
|
16
|
+
id: string;
|
|
17
|
+
header: string;
|
|
18
|
+
type?: string;
|
|
19
|
+
numeric?: boolean;
|
|
20
|
+
width?: number;
|
|
21
|
+
/** Payload columns default to optional. Set `false` to pin a column. */
|
|
22
|
+
optional?: boolean;
|
|
23
|
+
defaultHidden?: boolean;
|
|
24
|
+
};
|
|
25
|
+
export type Frame = {
|
|
26
|
+
columns: readonly FrameColumn[];
|
|
27
|
+
rows: number;
|
|
28
|
+
value: (row: number, column: FrameColumn, columnIndex: number) => FrameCell;
|
|
29
|
+
};
|
|
30
|
+
export type FramePresent = "table" | "chart";
|
|
31
|
+
export type SeriesBar = {
|
|
32
|
+
label: string;
|
|
33
|
+
count: number;
|
|
34
|
+
};
|
|
35
|
+
export declare function frameFromRows(columns: readonly FrameColumn[], rows: readonly (readonly FrameCell[])[]): Frame;
|
|
36
|
+
/** Keep only some columns. The cells still come from the original frame. */
|
|
37
|
+
export declare function withColumns(frame: Frame, ids: readonly string[]): Frame;
|
|
38
|
+
/**
|
|
39
|
+
* Turn one column into bars: a histogram if the values are numbers, the
|
|
40
|
+
* most frequent labels otherwise. Twelve bars is the ceiling — more than
|
|
41
|
+
* that is a table again.
|
|
42
|
+
*/
|
|
43
|
+
export declare function columnSeries(frame: Frame, columnId: string): SeriesBar[];
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { currentCopy, currentCopyLocale } from "../../foundations/copy.js";
|
|
2
|
+
const BINS = 12;
|
|
3
|
+
const TOP = 12;
|
|
4
|
+
export function frameFromRows(columns, rows) {
|
|
5
|
+
return {
|
|
6
|
+
columns,
|
|
7
|
+
rows: rows.length,
|
|
8
|
+
value: (row, _column, columnIndex) => rows[row]?.[columnIndex] ?? null,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
/** Keep only some columns. The cells still come from the original frame. */
|
|
12
|
+
export function withColumns(frame, ids) {
|
|
13
|
+
const wanted = new Set(ids);
|
|
14
|
+
const columns = frame.columns.filter((column) => wanted.has(column.id));
|
|
15
|
+
const indexOf = new Map(frame.columns.map((column, index) => [column.id, index]));
|
|
16
|
+
return {
|
|
17
|
+
columns,
|
|
18
|
+
rows: frame.rows,
|
|
19
|
+
value: (row, column) => {
|
|
20
|
+
const index = indexOf.get(column.id) ?? 0;
|
|
21
|
+
return frame.value(row, column, index);
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function asNumber(cell) {
|
|
26
|
+
if (typeof cell === "number" && Number.isFinite(cell))
|
|
27
|
+
return cell;
|
|
28
|
+
if (typeof cell === "string" && cell.trim() !== "") {
|
|
29
|
+
const n = Number(cell.replace(",", "."));
|
|
30
|
+
return Number.isFinite(n) ? n : null;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
function present(cell) {
|
|
35
|
+
return cell !== null && cell !== "";
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Turn one column into bars: a histogram if the values are numbers, the
|
|
39
|
+
* most frequent labels otherwise. Twelve bars is the ceiling — more than
|
|
40
|
+
* that is a table again.
|
|
41
|
+
*/
|
|
42
|
+
export function columnSeries(frame, columnId) {
|
|
43
|
+
const columnIndex = frame.columns.findIndex((column) => column.id === columnId);
|
|
44
|
+
const column = frame.columns[columnIndex];
|
|
45
|
+
if (column === undefined || columnIndex < 0)
|
|
46
|
+
return [];
|
|
47
|
+
const cells = [];
|
|
48
|
+
for (let row = 0; row < frame.rows; row += 1) {
|
|
49
|
+
cells.push(frame.value(row, column, columnIndex));
|
|
50
|
+
}
|
|
51
|
+
const numbers = cells.map(asNumber);
|
|
52
|
+
const numeric = numbers.filter((n) => n !== null);
|
|
53
|
+
const filled = cells.filter(present).length;
|
|
54
|
+
if (filled === 0)
|
|
55
|
+
return [];
|
|
56
|
+
if (column.numeric === true || numeric.length / filled >= 0.8) {
|
|
57
|
+
return histogram(numeric);
|
|
58
|
+
}
|
|
59
|
+
return categories(cells);
|
|
60
|
+
}
|
|
61
|
+
function histogram(values) {
|
|
62
|
+
if (values.length === 0)
|
|
63
|
+
return [];
|
|
64
|
+
const min = Math.min(...values);
|
|
65
|
+
const max = Math.max(...values);
|
|
66
|
+
if (min === max)
|
|
67
|
+
return [{ label: String(min), count: values.length }];
|
|
68
|
+
const width = (max - min) / BINS;
|
|
69
|
+
const counts = Array.from({ length: BINS }, () => 0);
|
|
70
|
+
for (const value of values) {
|
|
71
|
+
const slot = Math.min(BINS - 1, Math.floor((value - min) / width));
|
|
72
|
+
counts[slot] = (counts[slot] ?? 0) + 1;
|
|
73
|
+
}
|
|
74
|
+
return counts.map((count, index) => {
|
|
75
|
+
const from = min + index * width;
|
|
76
|
+
const to = index === BINS - 1 ? max : from + width;
|
|
77
|
+
return { label: `${formatTick(from)}–${formatTick(to)}`, count };
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
function categories(cells) {
|
|
81
|
+
const tally = new Map();
|
|
82
|
+
for (const cell of cells) {
|
|
83
|
+
if (!present(cell))
|
|
84
|
+
continue;
|
|
85
|
+
const label = String(cell);
|
|
86
|
+
tally.set(label, (tally.get(label) ?? 0) + 1);
|
|
87
|
+
}
|
|
88
|
+
return [...tally.entries()]
|
|
89
|
+
.sort((a, b) => b[1] - a[1])
|
|
90
|
+
.slice(0, TOP)
|
|
91
|
+
.map(([label, count]) => ({
|
|
92
|
+
label: label === "" ? currentCopy().cell.emptyBin : label,
|
|
93
|
+
count,
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
function formatTick(n) {
|
|
97
|
+
const abs = Math.abs(n);
|
|
98
|
+
if (abs >= 100 || Number.isInteger(n))
|
|
99
|
+
return String(Math.round(n));
|
|
100
|
+
return n.toLocaleString(currentCopyLocale(), { maximumFractionDigits: 1 });
|
|
101
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from "./activity/index.js";
|
|
2
|
+
export * from "./assist/index.js";
|
|
3
|
+
export * from "./form/index.js";
|
|
4
|
+
export * from "./frame/index.js";
|
|
5
|
+
export * from "./lines/index.js";
|
|
6
|
+
export * from "./listing/index.js";
|
|
7
|
+
export * from "./navigation/index.js";
|
|
8
|
+
export * from "./record/index.js";
|
|
9
|
+
export * from "./search/index.js";
|
|
10
|
+
export * from "./shell/index.js";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/* COMPONENTS — primitives composed to answer ONE question of the product, by family. */
|
|
2
|
+
export * from "./activity/index.js";
|
|
3
|
+
export * from "./assist/index.js";
|
|
4
|
+
export * from "./form/index.js";
|
|
5
|
+
export * from "./frame/index.js";
|
|
6
|
+
export * from "./lines/index.js";
|
|
7
|
+
export * from "./listing/index.js";
|
|
8
|
+
export * from "./navigation/index.js";
|
|
9
|
+
export * from "./record/index.js";
|
|
10
|
+
export * from "./search/index.js";
|
|
11
|
+
export * from "./shell/index.js";
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { type DialogContext } from "../../primitives/dialog/Dialog.js";
|
|
2
|
+
import { type FormMode } from "../../primitives/field/mode.js";
|
|
3
|
+
import type { FlagTone } from "../../primitives/notice/Flag.js";
|
|
4
|
+
import type { DataColumn } from "../../primitives/table/DataTable.js";
|
|
5
|
+
import { type EditColumn, type EditLayout } from "../../primitives/table/EditableTable.js";
|
|
6
|
+
import { type TotalsProps, type TotalsRow } from "../../primitives/value/Totals.js";
|
|
7
|
+
import { type ReactNode } from "react";
|
|
8
|
+
/**
|
|
9
|
+
* A column of the lines. The same contract as `DataTable`'s columns —`value` is
|
|
10
|
+
* the raw value (what the search reads), `cell` how it is drawn, `text` how it
|
|
11
|
+
* reads as plain text— plus `edit`, the control that replaces the cell when the
|
|
12
|
+
* row is being edited in place.
|
|
13
|
+
*/
|
|
14
|
+
export type LineItemColumn<T> = Pick<DataColumn<T>, "id" | "header" | "value" | "cell" | "text" | "numeric" | "width" | "optional" | "defaultHidden"> & Pick<EditColumn<T>, "stack" | "brief"> & {
|
|
15
|
+
/** The control while the row is edited in place. Without it the cell stays read
|
|
16
|
+
* only — a computed amount, a code. Only simple values belong here; the whole
|
|
17
|
+
* line is edited in the `editor`. */
|
|
18
|
+
edit?: (line: T, set: (next: T) => void) => ReactNode;
|
|
19
|
+
/** What the lines of a section add up to in this column, already formatted. Only
|
|
20
|
+
* with `structure`; without it the section says nothing in this column. */
|
|
21
|
+
subtotal?: (lines: readonly T[]) => ReactNode;
|
|
22
|
+
};
|
|
23
|
+
/** Which lines are sections or notes, and how their text is read and written. See
|
|
24
|
+
* «Sections and notes» in the file's header. */
|
|
25
|
+
export type LineStructure<T> = {
|
|
26
|
+
kind: (line: T) => "line" | "section" | "note";
|
|
27
|
+
text: (line: T) => string;
|
|
28
|
+
withText: (line: T, text: string) => T;
|
|
29
|
+
/** A blank section; enables «Agregar una sección». */
|
|
30
|
+
newSection?: () => T;
|
|
31
|
+
/** A blank note; enables «Agregar una nota». */
|
|
32
|
+
newNote?: () => T;
|
|
33
|
+
};
|
|
34
|
+
/** What the popup editor receives besides the draft. */
|
|
35
|
+
export type LineEditorState = {
|
|
36
|
+
/** «Listo» was pressed and the draft was not valid: show the errors now, not before. */
|
|
37
|
+
tried: boolean;
|
|
38
|
+
/** A line that does not exist yet. */
|
|
39
|
+
isNew: boolean;
|
|
40
|
+
};
|
|
41
|
+
export type LineItemsProps<T> = {
|
|
42
|
+
lines: readonly T[];
|
|
43
|
+
columns: readonly LineItemColumn<T>[];
|
|
44
|
+
getLineId: (line: T) => string;
|
|
45
|
+
/** How a line is named — in the removal notice, the editor's header and for the
|
|
46
|
+
* screen reader next to each row's buttons. Defaults to the first column's text. */
|
|
47
|
+
lineLabel?: (line: T) => string;
|
|
48
|
+
/** `read` for a document that can no longer change (a posted invoice): no add, no
|
|
49
|
+
* edit, no remove — just the lines. By default it inherits the form's mode. */
|
|
50
|
+
mode?: FormMode;
|
|
51
|
+
/** A line changed, in place or in the editor. Without it nothing is editable. */
|
|
52
|
+
onChange?: (id: string, next: T) => void;
|
|
53
|
+
/** A new line, already filled in the editor (or blank, without an editor). */
|
|
54
|
+
onAdd?: (line: T) => void;
|
|
55
|
+
/** The blank line «Agregar una línea» starts from. Required for adding. */
|
|
56
|
+
newLine?: () => T;
|
|
57
|
+
/** A line was removed. It is gone at once; the notice offers to undo it. */
|
|
58
|
+
onRemove?: (id: string) => void;
|
|
59
|
+
/** Put a removed line back where it was. Without it the notice has no «Deshacer». */
|
|
60
|
+
onRestore?: (line: T, index: number) => void;
|
|
61
|
+
/** The whole line as a form: the product, the quantity, the price, the discount.
|
|
62
|
+
* Written with the system's `Field` controls; `set` replaces the draft. */
|
|
63
|
+
editor?: (draft: T, set: (next: T) => void, state: LineEditorState) => ReactNode;
|
|
64
|
+
/** Whether the draft can be saved. When it cannot, «Listo» keeps the editor open
|
|
65
|
+
* and `state.tried` turns on so the editor can say what is missing. */
|
|
66
|
+
isValid?: (draft: T) => boolean;
|
|
67
|
+
/** What the editor is about, in its header: «factura · FAC/2026/0118». */
|
|
68
|
+
context?: DialogContext;
|
|
69
|
+
/** What the search reads. Defaults to every column's `text` or `value`. */
|
|
70
|
+
searchText?: (line: T) => string;
|
|
71
|
+
/** The search shows up only with MORE lines than this. Four lines are read, not
|
|
72
|
+
* searched; a search box over them is a control that answers nothing. */
|
|
73
|
+
searchFrom?: number;
|
|
74
|
+
/** The money summary under the lines, on the right: the system's `Totals`. Its rows,
|
|
75
|
+
* or all of it — payments deducted, the balance due, the credits that can be applied. */
|
|
76
|
+
totals?: readonly TotalsRow[] | TotalsProps;
|
|
77
|
+
/** `auto` (the default) draws the lines as stacked cards below
|
|
78
|
+
* `--container-handheld`; `table` and `stacked` force one. */
|
|
79
|
+
layout?: EditLayout;
|
|
80
|
+
/** How «Agregar una línea» adds: `editor` opens the blank line in the dialog;
|
|
81
|
+
* `inline` puts it in the table, open, under the others (see the file's header).
|
|
82
|
+
* By default the dialog when there is an `editor`, the table when there is not. */
|
|
83
|
+
adding?: "inline" | "editor";
|
|
84
|
+
/** Open the whole line somewhere else than this block's dialog — the app's `Drill`, so
|
|
85
|
+
* the line opens as «S00142 › Línea 3» and can go deeper (its product, its lot) without
|
|
86
|
+
* a modal over a modal. When given, it replaces `editor` for existing lines. */
|
|
87
|
+
onOpenLine?: (id: string) => void;
|
|
88
|
+
/** A warning about one line, on the line: «Sin existencias», «Precio bajo el costo».
|
|
89
|
+
* `EditableTable`'s `flag`, passed through. */
|
|
90
|
+
flag?: (line: T) => {
|
|
91
|
+
tone: FlagTone;
|
|
92
|
+
label: string;
|
|
93
|
+
detail?: ReactNode;
|
|
94
|
+
owner?: string;
|
|
95
|
+
} | null;
|
|
96
|
+
/** Sections and notes among the lines. See the file's header. */
|
|
97
|
+
structure?: LineStructure<T>;
|
|
98
|
+
/** Which columns are on screen, when the owner remembers it (a preference). */
|
|
99
|
+
visibleColumnIds?: readonly string[];
|
|
100
|
+
onVisibleColumnIdsChange?: (ids: readonly string[]) => void;
|
|
101
|
+
};
|
|
102
|
+
/**
|
|
103
|
+
* The lines of a document inside its record: a count and a search above, the lines
|
|
104
|
+
* (edited in place or in a popup editor), and the totals under them on the right.
|
|
105
|
+
*
|
|
106
|
+
* Context: the record used to compose this by hand — a `Table`, its rows, a
|
|
107
|
+
* `Totals` — and every module did it a little differently: where «add» went, whether
|
|
108
|
+
* removing asked first, what an empty document said. One component, built on
|
|
109
|
+
* `EditableTable` and `Dialog`, makes the one2many the same block everywhere.
|
|
110
|
+
*/
|
|
111
|
+
export declare function LineItems<T>({ lines, columns, getLineId, lineLabel, mode, onChange, onAdd, newLine, onRemove, onRestore, editor, isValid, context, searchText, searchFrom, totals, layout, adding, onOpenLine, flag, structure, visibleColumnIds, onVisibleColumnIdsChange, }: LineItemsProps<T>): import("react").JSX.Element;
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { commitDraft, filterLines, planRemoval, } from "./lines.js";
|
|
4
|
+
import { useColumnControl, } from "../listing/columns.js";
|
|
5
|
+
import { ColumnsMenu } from "../listing/ColumnsMenu.js";
|
|
6
|
+
import { fillCopy, useUiCopy } from "../../foundations/copy.js";
|
|
7
|
+
import { Button, IconButton } from "../../primitives/button/Button.js";
|
|
8
|
+
import { SearchField } from "../../primitives/choice/SearchField.js";
|
|
9
|
+
import { Dialog } from "../../primitives/dialog/Dialog.js";
|
|
10
|
+
import { FieldIdsContext } from "../../primitives/field/context.js";
|
|
11
|
+
import { TextInput } from "../../primitives/field/Field.js";
|
|
12
|
+
import { useFormMode } from "../../primitives/field/mode.js";
|
|
13
|
+
import { Empty } from "../../primitives/notice/Notice.js";
|
|
14
|
+
import { useToast } from "../../primitives/notice/Toast.js";
|
|
15
|
+
import { EditableTable, } from "../../primitives/table/EditableTable.js";
|
|
16
|
+
import { Totals, } from "../../primitives/value/Totals.js";
|
|
17
|
+
import { Pencil, Plus, Trash2 } from "lucide-react";
|
|
18
|
+
import { useEffect, useId, useMemo, useRef, useState } from "react";
|
|
19
|
+
/** The notice is optional: without a `ToastProvider` the line is still removed, just
|
|
20
|
+
* without the way back — a missing provider must not break a form. */
|
|
21
|
+
function useOptionalToast() {
|
|
22
|
+
try {
|
|
23
|
+
return useToast();
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function plain(v) {
|
|
30
|
+
return v === null ? "" : String(v);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The lines of a document inside its record: a count and a search above, the lines
|
|
34
|
+
* (edited in place or in a popup editor), and the totals under them on the right.
|
|
35
|
+
*
|
|
36
|
+
* Context: the record used to compose this by hand — a `Table`, its rows, a
|
|
37
|
+
* `Totals` — and every module did it a little differently: where «add» went, whether
|
|
38
|
+
* removing asked first, what an empty document said. One component, built on
|
|
39
|
+
* `EditableTable` and `Dialog`, makes the one2many the same block everywhere.
|
|
40
|
+
*/
|
|
41
|
+
export function LineItems({ lines, columns, getLineId, lineLabel, mode, onChange, onAdd, newLine, onRemove, onRestore, editor, isValid, context, searchText, searchFrom = 8, totals, layout = "auto", adding, onOpenLine, flag, structure, visibleColumnIds, onVisibleColumnIdsChange, }) {
|
|
42
|
+
const copy = useUiCopy();
|
|
43
|
+
const toast = useOptionalToast();
|
|
44
|
+
const read = useFormMode(mode) === "read";
|
|
45
|
+
const root = useRef(null);
|
|
46
|
+
const searchLabelId = useId();
|
|
47
|
+
const formId = useId();
|
|
48
|
+
const [query, setQuery] = useState("");
|
|
49
|
+
const [session, setSession] = useState(null);
|
|
50
|
+
const [open, setOpen] = useState(false);
|
|
51
|
+
/* The draft the form submits, read from a ref: Enter in a number field commits
|
|
52
|
+
the typed text and submits in the same keystroke, before a re-render. */
|
|
53
|
+
const draftRef = useRef(null);
|
|
54
|
+
/* Where focus goes after a removal: the button that was pressed is gone with its
|
|
55
|
+
row, and focus falling to the page is losing the place. */
|
|
56
|
+
const focusNext = useRef(null);
|
|
57
|
+
const canChange = !read && onChange !== undefined;
|
|
58
|
+
const canAdd = !read && onAdd !== undefined && newLine !== undefined;
|
|
59
|
+
const canRemove = !read && onRemove !== undefined;
|
|
60
|
+
/* The whole line opens either in this block's own dialog (`editor`) or wherever the app
|
|
61
|
+
opens records (`onOpenLine`, a `Drill` context) — never both, so a line's form never
|
|
62
|
+
sits on top of another modal. */
|
|
63
|
+
const canEdit = (canChange && editor !== undefined) || onOpenLine !== undefined;
|
|
64
|
+
const openLine = (id, line) => {
|
|
65
|
+
if (onOpenLine !== undefined)
|
|
66
|
+
onOpenLine(id);
|
|
67
|
+
else
|
|
68
|
+
openEditor(id, line);
|
|
69
|
+
};
|
|
70
|
+
const inline = canAdd && (adding ?? (editor === undefined ? "inline" : "editor")) === "inline";
|
|
71
|
+
const labelOf = (line) => {
|
|
72
|
+
if (lineLabel !== undefined)
|
|
73
|
+
return lineLabel(line);
|
|
74
|
+
const first = columns[0];
|
|
75
|
+
if (first === undefined)
|
|
76
|
+
return getLineId(line);
|
|
77
|
+
return first.text?.(line) ?? plain(first.value(line));
|
|
78
|
+
};
|
|
79
|
+
const kindOf = (line) => structure?.kind(line) ?? "line";
|
|
80
|
+
/* Sections and notes are not lines: they are not counted, and a search leaves only lines. */
|
|
81
|
+
const lineCount = structure === undefined ? lines.length : lines.filter((l) => kindOf(l) === "line").length;
|
|
82
|
+
const searchable = lineCount > searchFrom;
|
|
83
|
+
const q = searchable ? query.trim() : "";
|
|
84
|
+
const shown = useMemo(() => {
|
|
85
|
+
const found = filterLines(lines, q, (line) => searchText !== undefined
|
|
86
|
+
? searchText(line)
|
|
87
|
+
: columns.map((c) => c.text?.(line) ?? plain(c.value(line))).join(" "), copy.locale);
|
|
88
|
+
return q === "" || structure === undefined
|
|
89
|
+
? found
|
|
90
|
+
: found.filter((l) => structure.kind(l) === "line");
|
|
91
|
+
}, [lines, columns, searchText, q, copy.locale, structure]);
|
|
92
|
+
/* ── which columns are on screen ── */
|
|
93
|
+
const specs = useMemo(() => columns.map((c) => ({
|
|
94
|
+
id: c.id,
|
|
95
|
+
label: c.header,
|
|
96
|
+
optional: c.optional === true,
|
|
97
|
+
...(c.defaultHidden === true ? { defaultHidden: true } : {}),
|
|
98
|
+
})), [columns]);
|
|
99
|
+
/* `visibleColumnIds` alone is a fixed choice, the way a controlled input without
|
|
100
|
+
`onChange` is: the owner said which columns, and the menu cannot change them. */
|
|
101
|
+
const columnControl = useColumnControl(specs, visibleColumnIds === undefined && onVisibleColumnIdsChange === undefined
|
|
102
|
+
? {}
|
|
103
|
+
: {
|
|
104
|
+
...(visibleColumnIds === undefined ? {} : { visibleIds: visibleColumnIds }),
|
|
105
|
+
onVisibleIdsChange: onVisibleColumnIdsChange ?? (() => { }),
|
|
106
|
+
});
|
|
107
|
+
const onScreen = columns.filter((c) => columnControl.isVisible(c.id));
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
const target = focusNext.current;
|
|
110
|
+
if (target === null)
|
|
111
|
+
return;
|
|
112
|
+
focusNext.current = null;
|
|
113
|
+
const scope = root.current;
|
|
114
|
+
if (scope === null)
|
|
115
|
+
return;
|
|
116
|
+
/* The next line's first button on a desk; on a phone its card, which has none. */
|
|
117
|
+
const next = target.id === null
|
|
118
|
+
? null
|
|
119
|
+
: (scope.querySelector(`[data-line="${CSS.escape(target.id)}"] button`) ?? scope.querySelector(`[data-row-id="${CSS.escape(target.id)}"]`));
|
|
120
|
+
(next ?? scope.querySelector("[data-line-add]"))?.focus();
|
|
121
|
+
}, [lines]);
|
|
122
|
+
/* ── the editor ─────────────────────────────────────────────────── */
|
|
123
|
+
const openEditor = (id, draft) => {
|
|
124
|
+
draftRef.current = draft;
|
|
125
|
+
setSession({ id, draft, tried: false });
|
|
126
|
+
setOpen(true);
|
|
127
|
+
};
|
|
128
|
+
const setDraft = (next) => {
|
|
129
|
+
draftRef.current = next;
|
|
130
|
+
setSession((s) => (s === null ? s : { ...s, draft: next }));
|
|
131
|
+
};
|
|
132
|
+
const save = () => {
|
|
133
|
+
const draft = draftRef.current;
|
|
134
|
+
if (session === null || draft === null)
|
|
135
|
+
return;
|
|
136
|
+
const commit = commitDraft(session.id, draft, isValid);
|
|
137
|
+
if (commit.kind === "invalid") {
|
|
138
|
+
setSession({ ...session, draft, tried: true });
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (commit.kind === "add") {
|
|
142
|
+
/* A new line is shown even if it does not match the search: adding one and
|
|
143
|
+
not seeing it is the worst answer. */
|
|
144
|
+
setQuery("");
|
|
145
|
+
onAdd?.(commit.line);
|
|
146
|
+
}
|
|
147
|
+
else
|
|
148
|
+
onChange?.(commit.id, commit.line);
|
|
149
|
+
setOpen(false);
|
|
150
|
+
};
|
|
151
|
+
const add = () => {
|
|
152
|
+
if (newLine === undefined)
|
|
153
|
+
return;
|
|
154
|
+
const blank = newLine();
|
|
155
|
+
if (editor === undefined) {
|
|
156
|
+
setQuery("");
|
|
157
|
+
onAdd?.(blank);
|
|
158
|
+
}
|
|
159
|
+
else
|
|
160
|
+
openEditor(null, blank);
|
|
161
|
+
};
|
|
162
|
+
/* In place: the table opens the row whose id this returns. */
|
|
163
|
+
const addInline = (make = newLine) => {
|
|
164
|
+
if (make === undefined)
|
|
165
|
+
return undefined;
|
|
166
|
+
const blank = make();
|
|
167
|
+
setQuery("");
|
|
168
|
+
onAdd?.(blank);
|
|
169
|
+
return getLineId(blank);
|
|
170
|
+
};
|
|
171
|
+
/* A section and a note are always written in place, editor or not. */
|
|
172
|
+
const bandAdds = read || onAdd === undefined || structure === undefined
|
|
173
|
+
? []
|
|
174
|
+
: [
|
|
175
|
+
...(structure.newSection === undefined
|
|
176
|
+
? []
|
|
177
|
+
: [
|
|
178
|
+
{
|
|
179
|
+
label: copy.lines.addSection,
|
|
180
|
+
onAdd: () => addInline(structure.newSection),
|
|
181
|
+
},
|
|
182
|
+
]),
|
|
183
|
+
...(structure.newNote === undefined
|
|
184
|
+
? []
|
|
185
|
+
: [{ label: copy.lines.addNote, onAdd: () => addInline(structure.newNote) }]),
|
|
186
|
+
];
|
|
187
|
+
const inPlace = inline || bandAdds.length > 0;
|
|
188
|
+
const bands = structure === undefined
|
|
189
|
+
? undefined
|
|
190
|
+
: {
|
|
191
|
+
kind: (line) => {
|
|
192
|
+
const k = structure.kind(line);
|
|
193
|
+
return k === "line" ? null : k;
|
|
194
|
+
},
|
|
195
|
+
render: (line) => structure.text(line),
|
|
196
|
+
...(canChange
|
|
197
|
+
? {
|
|
198
|
+
edit: (line, set) => (_jsx(TextInput, { "aria-label": structure.kind(line) === "section"
|
|
199
|
+
? copy.lines.section
|
|
200
|
+
: copy.lines.note, value: structure.text(line), onChange: (text) => set(structure.withText(line, text)) })),
|
|
201
|
+
}
|
|
202
|
+
: {}),
|
|
203
|
+
total: (column, under) => columns.find((c) => c.id === column.id)?.subtotal?.(under) ?? null,
|
|
204
|
+
};
|
|
205
|
+
const remove = (line) => {
|
|
206
|
+
const id = getLineId(line);
|
|
207
|
+
const { index, focus } = planRemoval(lines, shown, id, getLineId);
|
|
208
|
+
focusNext.current = { id: focus };
|
|
209
|
+
onRemove?.(id);
|
|
210
|
+
toast?.show({
|
|
211
|
+
message: fillCopy(copy.lineItems.removed, { name: labelOf(line) }),
|
|
212
|
+
...(onRestore === undefined
|
|
213
|
+
? {}
|
|
214
|
+
: {
|
|
215
|
+
action: {
|
|
216
|
+
label: copy.lineItems.undo,
|
|
217
|
+
onSelect: () => onRestore(line, index),
|
|
218
|
+
},
|
|
219
|
+
}),
|
|
220
|
+
});
|
|
221
|
+
};
|
|
222
|
+
/* ── the table ──────────────────────────────────────────────────── */
|
|
223
|
+
const tableColumns = onScreen.map((c) => ({
|
|
224
|
+
id: c.id,
|
|
225
|
+
header: c.header,
|
|
226
|
+
...(c.width === undefined ? {} : { width: c.width }),
|
|
227
|
+
...(c.numeric === undefined ? {} : { numeric: c.numeric }),
|
|
228
|
+
...(c.stack === undefined ? {} : { stack: c.stack }),
|
|
229
|
+
...(c.brief === undefined ? {} : { brief: c.brief }),
|
|
230
|
+
render: (line) => {
|
|
231
|
+
const drawn = c.cell === undefined ? plain(c.value(line)) : c.cell(line);
|
|
232
|
+
/* An amount never breaks in two: «Bs.» over «12.500,00» reads as two values. */
|
|
233
|
+
return c.numeric === true ? _jsx("span", { className: "whitespace-nowrap", children: drawn }) : drawn;
|
|
234
|
+
},
|
|
235
|
+
...(c.edit === undefined || !canChange
|
|
236
|
+
? {}
|
|
237
|
+
: {
|
|
238
|
+
/* The control takes the column's width and does not impose its own: an input's
|
|
239
|
+
natural width is twenty characters, and in an auto-sized table that pushes
|
|
240
|
+
the amounts off the card the moment a row goes into editing. */
|
|
241
|
+
edit: (line, set) => (_jsx("div", { className: "contain-inline-size", children: c.edit?.(line, set) })),
|
|
242
|
+
}),
|
|
243
|
+
}));
|
|
244
|
+
if (canEdit || canRemove) {
|
|
245
|
+
tableColumns.push({
|
|
246
|
+
id: "\u0000actions",
|
|
247
|
+
header: "",
|
|
248
|
+
width: 1,
|
|
249
|
+
/* On a card the press opens the editor, and the editor removes: the buttons
|
|
250
|
+
would be two more targets on a small screen. Without an editor the trash
|
|
251
|
+
is the only way to remove, so it stays — at the card's top right, lit. */
|
|
252
|
+
stack: canEdit ? "hidden" : "trail",
|
|
253
|
+
onBands: true,
|
|
254
|
+
render: (line) => {
|
|
255
|
+
const id = getLineId(line);
|
|
256
|
+
const nameId = `${formId}-line-${id}`;
|
|
257
|
+
return (
|
|
258
|
+
/* Hidden until the row is hovered or holds focus, like `RowActions`: a pencil
|
|
259
|
+
and a trash can lit on each of twenty lines are forty invitations. They
|
|
260
|
+
fade instead of unmounting, so Tab still reaches them. Pressing them does
|
|
261
|
+
not put the row into in-place editing. */
|
|
262
|
+
_jsxs("span", { "data-line": id, className: "flex items-center justify-end gap-0.5 whitespace-nowrap opacity-0 transition-opacity duration-(--duration-instant) group-hover/row:opacity-100 focus-within:opacity-100 in-data-stacked:opacity-100", onClick: (e) => e.stopPropagation(), children: [_jsx("span", { id: nameId, className: "sr-only", children: labelOf(line) }), canEdit && kindOf(line) === "line" ? (_jsx(IconButton, { icon: Pencil, label: copy.lines.edit, "aria-describedby": nameId, haspopup: "dialog", onClick: () => openLine(id, line) })) : null, canRemove ? (_jsx(IconButton, { icon: Trash2, label: copy.lines.remove, tone: "danger", "aria-describedby": nameId, onClick: () => remove(line) })) : null] }));
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
/* In place, «add» is the table's own row under the grid: where the line will appear. */
|
|
267
|
+
const addButton = canAdd && !inline ? (_jsx(Button, { role: "secondary", size: "sm", icon: Plus, onClick: add, "data-line-add": "", children: copy.lines.add })) : null;
|
|
268
|
+
const count = q === ""
|
|
269
|
+
? fillCopy(lineCount === 1 ? copy.lineItems.one : copy.lineItems.many, {
|
|
270
|
+
count: lineCount.toLocaleString(copy.locale),
|
|
271
|
+
})
|
|
272
|
+
: fillCopy(copy.lineItems.shown, {
|
|
273
|
+
shown: shown.length.toLocaleString(copy.locale),
|
|
274
|
+
count: lineCount.toLocaleString(copy.locale),
|
|
275
|
+
});
|
|
276
|
+
return (_jsxs("section", { ref: root, "aria-label": copy.lineItems.table, className: "flex min-w-0 flex-col gap-4", children: [lines.length === 0 && !canAdd ? (
|
|
277
|
+
/* Said, with its way out: an empty document is a question («what is sold
|
|
278
|
+
here?»), not a blank box. In read mode there is no way out to offer. */
|
|
279
|
+
_jsx(Empty, { align: "start", sentence: copy.lines.empty, action: addButton ?? undefined })) : (_jsxs("div", { className: "flex min-w-0 flex-col gap-2", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-x-3 gap-y-2 px-1", children: [searchable ? (_jsxs("div", { className: "w-full max-w-(--container-search) min-w-0 flex-1", children: [_jsx("span", { id: searchLabelId, className: "sr-only", children: copy.lines.search }), _jsx(FieldIdsContext.Provider, { value: {
|
|
280
|
+
id: `${searchLabelId}-input`,
|
|
281
|
+
labelId: searchLabelId,
|
|
282
|
+
describedBy: undefined,
|
|
283
|
+
invalid: false,
|
|
284
|
+
}, children: _jsx(SearchField, { value: query, onChange: setQuery, placeholder: copy.lines.search }) })] })) : null, _jsx("span", { className: "text-meta text-muted tabular-nums", "aria-live": "polite", children: count }), columnControl.hasOptional || addButton !== null ? (_jsxs("span", { className: "ml-auto flex items-center gap-2", children: [_jsx(ColumnsMenu, { control: columnControl }), addButton] })) : null] }), _jsx(EditableTable, { rows: shown, columns: tableColumns, getRowId: getLineId, empty: lines.length === 0
|
|
285
|
+
? copy.lines.empty
|
|
286
|
+
: fillCopy(copy.lineItems.noMatch, { query: query.trim() }), layout: layout, ...(inline ? { onAdd: () => addInline(), addLabel: copy.lines.add } : {}), ...(bandAdds.length === 0 ? {} : { adds: bandAdds }), ...(bands === undefined ? {} : { bands }), ...(flag === undefined ? {} : { flag }), ...(inPlace && onRemove !== undefined
|
|
287
|
+
? /* A blank row nobody wrote in is taken back without a notice: nobody
|
|
288
|
+
removed anything, so there is nothing to undo. */
|
|
289
|
+
{ onDiscard: onRemove }
|
|
290
|
+
: {}), ...(canEdit
|
|
291
|
+
? {
|
|
292
|
+
onOpen: (id) => {
|
|
293
|
+
const line = lines.find((l) => getLineId(l) === id);
|
|
294
|
+
if (line !== undefined)
|
|
295
|
+
openLine(id, line);
|
|
296
|
+
},
|
|
297
|
+
}
|
|
298
|
+
: {}), ...(canChange && columns.some((c) => c.edit !== undefined)
|
|
299
|
+
? { onChange: (id, next) => onChange?.(id, next) }
|
|
300
|
+
: {}) })] })), totals === undefined || ("length" in totals && totals.length === 0) ? null : (
|
|
301
|
+
/* Inset like the table's cells, so the last figure lines up with the amounts. */
|
|
302
|
+
_jsx("div", { className: "px-4", children: "length" in totals ? _jsx(Totals, { rows: totals }) : _jsx(Totals, { ...totals }) })), editor === undefined || session === null ? null : (_jsx(Dialog, { open: open, width: "sm", title: session.id === null ? copy.lineItems.newLine : copy.lines.edit, ...(context === undefined ? {} : { context }), ...(session.id === null ? {} : { subtitle: labelOf(session.draft) }), onClose: () => setOpen(false), footer: _jsxs(_Fragment, { children: [session.id === null || !canRemove ? null : (
|
|
303
|
+
/* Apart, at the other end from «Listo»: removing is not the way the
|
|
304
|
+
form is meant to close. */
|
|
305
|
+
_jsx("span", { className: "mr-auto", children: _jsx(IconButton, { icon: Trash2, label: copy.lines.remove, tone: "danger", onClick: () => {
|
|
306
|
+
const line = lines.find((l) => getLineId(l) === session.id);
|
|
307
|
+
setOpen(false);
|
|
308
|
+
if (line !== undefined)
|
|
309
|
+
remove(line);
|
|
310
|
+
} }) })), _jsx(Button, { role: "ghost", size: "sm", onClick: () => setOpen(false), children: copy.dialog.cancel }), _jsx(Button, { role: "primary", size: "sm", type: "submit", form: formId, children: copy.lines.done })] }), children: _jsx("form", { id: formId, noValidate: true, onSubmit: (e) => {
|
|
311
|
+
e.preventDefault();
|
|
312
|
+
save();
|
|
313
|
+
}, className: "flex flex-col gap-3", children: editor(session.draft, setDraft, {
|
|
314
|
+
tried: session.tried,
|
|
315
|
+
isNew: session.id === null,
|
|
316
|
+
}) }) }))] }));
|
|
317
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { type LineEditorState, type LineItemColumn, LineItems, type LineItemsProps, } from "./LineItems.js";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** The lines whose text contains the query, case and locale aware. An empty query is all. */
|
|
2
|
+
export declare function filterLines<T>(lines: readonly T[], query: string, textOf: (line: T) => string, locale: string): readonly T[];
|
|
3
|
+
/**
|
|
4
|
+
* What removing a line needs to know BEFORE it is gone: its position among all the lines (so
|
|
5
|
+
* «Deshacer» puts it back exactly there) and the visible line that inherits the focus — the
|
|
6
|
+
* next one, or the previous one when it was the last. `null` when none is left.
|
|
7
|
+
*/
|
|
8
|
+
export declare function planRemoval<T>(lines: readonly T[], shown: readonly T[], id: string, getId: (line: T) => string): {
|
|
9
|
+
index: number;
|
|
10
|
+
focus: string | null;
|
|
11
|
+
};
|
|
12
|
+
/** What «Listo» does with the draft: nothing yet (it is not valid), add it, or replace a line. */
|
|
13
|
+
export type DraftCommit<T> = {
|
|
14
|
+
kind: "invalid";
|
|
15
|
+
} | {
|
|
16
|
+
kind: "add";
|
|
17
|
+
line: T;
|
|
18
|
+
} | {
|
|
19
|
+
kind: "change";
|
|
20
|
+
id: string;
|
|
21
|
+
line: T;
|
|
22
|
+
};
|
|
23
|
+
export declare function commitDraft<T>(id: string | null, draft: T, isValid?: (draft: T) => boolean): DraftCommit<T>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/* The decisions of `LineItems` that do not need a screen: which lines a search leaves, where a
|
|
2
|
+
removed line was and who takes the focus after it, and what «Listo» hands to the owner of
|
|
3
|
+
the lines. Pure, so a test reads them without a browser. */
|
|
4
|
+
/** The lines whose text contains the query, case and locale aware. An empty query is all. */
|
|
5
|
+
export function filterLines(lines, query, textOf, locale) {
|
|
6
|
+
const q = query.trim().toLocaleLowerCase(locale);
|
|
7
|
+
if (q === "")
|
|
8
|
+
return lines;
|
|
9
|
+
return lines.filter((line) => textOf(line).toLocaleLowerCase(locale).includes(q));
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* What removing a line needs to know BEFORE it is gone: its position among all the lines (so
|
|
13
|
+
* «Deshacer» puts it back exactly there) and the visible line that inherits the focus — the
|
|
14
|
+
* next one, or the previous one when it was the last. `null` when none is left.
|
|
15
|
+
*/
|
|
16
|
+
export function planRemoval(lines, shown, id, getId) {
|
|
17
|
+
const index = lines.findIndex((l) => getId(l) === id);
|
|
18
|
+
const at = shown.findIndex((l) => getId(l) === id);
|
|
19
|
+
const neighbour = shown[at + 1] ?? (at > 0 ? shown[at - 1] : undefined);
|
|
20
|
+
return { index, focus: neighbour === undefined ? null : getId(neighbour) };
|
|
21
|
+
}
|
|
22
|
+
export function commitDraft(id, draft, isValid) {
|
|
23
|
+
if (isValid !== undefined && !isValid(draft))
|
|
24
|
+
return { kind: "invalid" };
|
|
25
|
+
return id === null ? { kind: "add", line: draft } : { kind: "change", id, line: draft };
|
|
26
|
+
}
|