@sincpro/web-ui 0.0.0-stage → 0.1.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 +157 -2
- 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/chatter/Chatter.d.ts +161 -0
- package/dist/components/chatter/Chatter.js +232 -0
- package/dist/components/chatter/Messages.d.ts +66 -0
- package/dist/components/chatter/Messages.js +43 -0
- package/dist/components/chatter/Timeline.d.ts +21 -0
- package/dist/components/chatter/Timeline.js +30 -0
- package/dist/components/chatter/index.d.ts +3 -0
- package/dist/components/chatter/index.js +4 -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 +43 -0
- package/dist/components/form/Sheet.js +83 -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 +74 -0
- package/dist/components/lines/LineItems.js +229 -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 +73 -0
- package/dist/components/listing/ListingBar.js +105 -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 +106 -0
- package/dist/components/listing/Row.js +122 -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 +71 -0
- package/dist/components/record/RecordHeader.js +72 -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 +619 -0
- package/dist/foundations/copy.es.d.ts +2 -0
- package/dist/foundations/copy.es.js +377 -0
- package/dist/foundations/copy.js +472 -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 +16 -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 +24 -0
- package/dist/primitives/choice/SearchField.js +27 -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 +110 -0
- package/dist/primitives/date/DatePicker.d.ts +34 -0
- package/dist/primitives/date/DatePicker.js +99 -0
- package/dist/primitives/date/index.d.ts +2 -0
- package/dist/primitives/date/index.js +4 -0
- package/dist/primitives/dialog/Dialog.d.ts +118 -0
- package/dist/primitives/dialog/Dialog.js +121 -0
- package/dist/primitives/dialog/index.d.ts +1 -0
- package/dist/primitives/dialog/index.js +2 -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 +166 -0
- package/dist/primitives/field/Field.js +240 -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/index.d.ts +7 -0
- package/dist/primitives/field/index.js +11 -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/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 +19 -0
- package/dist/primitives/index.js +20 -0
- package/dist/primitives/menu/Menu.d.ts +100 -0
- package/dist/primitives/menu/Menu.js +59 -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/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 +3 -0
- package/dist/primitives/notice/index.js +5 -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/Tooltip.d.ts +38 -0
- package/dist/primitives/overlay/Tooltip.js +74 -0
- package/dist/primitives/overlay/index.d.ts +3 -0
- package/dist/primitives/overlay/index.js +5 -0
- package/dist/primitives/picker/Picker.d.ts +48 -0
- package/dist/primitives/picker/Picker.js +150 -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/CellEditor.d.ts +14 -0
- package/dist/primitives/table/CellEditor.js +83 -0
- package/dist/primitives/table/DataTable.d.ts +135 -0
- package/dist/primitives/table/DataTable.js +244 -0
- package/dist/primitives/table/EditableTable.d.ts +96 -0
- package/dist/primitives/table/EditableTable.js +423 -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 +70 -0
- package/dist/primitives/table/SheetTable.js +197 -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 +32 -0
- package/dist/primitives/tag/TagsField.js +182 -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/value/Copyable.d.ts +37 -0
- package/dist/primitives/value/Copyable.js +50 -0
- package/dist/primitives/value/Fecha.d.ts +12 -0
- package/dist/primitives/value/Fecha.js +80 -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/Relation.d.ts +69 -0
- package/dist/primitives/value/Relation.js +57 -0
- package/dist/primitives/value/Totals.d.ts +26 -0
- package/dist/primitives/value/Totals.js +4 -0
- package/dist/primitives/value/Value.d.ts +77 -0
- package/dist/primitives/value/Value.js +119 -0
- package/dist/primitives/value/index.d.ts +8 -0
- package/dist/primitives/value/index.js +9 -0
- package/dist/tokens.css +1025 -0
- package/dist/views/index.d.ts +2 -0
- package/dist/views/index.js +3 -0
- package/dist/views/list/List.d.ts +113 -0
- package/dist/views/list/List.js +97 -0
- package/dist/views/list/ListView.d.ts +6 -0
- package/dist/views/list/ListView.js +269 -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 +76 -0
- package/dist/views/list/derive.js +181 -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 +103 -0
- package/dist/views/list/types.d.ts +272 -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 +126 -0
- package/dist/views/record/Record.js +141 -0
- package/dist/views/record/index.d.ts +1 -0
- package/dist/views/record/index.js +2 -0
- package/package.json +109 -4
|
@@ -0,0 +1,229 @@
|
|
|
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 { fillCopy, useUiCopy } from "../../foundations/copy.js";
|
|
5
|
+
import { Button, IconButton } from "../../primitives/button/Button.js";
|
|
6
|
+
import { SearchField } from "../../primitives/choice/SearchField.js";
|
|
7
|
+
import { Dialog } from "../../primitives/dialog/Dialog.js";
|
|
8
|
+
import { FieldIdsContext } from "../../primitives/field/context.js";
|
|
9
|
+
import { useFormMode } from "../../primitives/field/mode.js";
|
|
10
|
+
import { Empty } from "../../primitives/notice/Notice.js";
|
|
11
|
+
import { useToast } from "../../primitives/notice/Toast.js";
|
|
12
|
+
import { EditableTable, } from "../../primitives/table/EditableTable.js";
|
|
13
|
+
import { Totals } from "../../primitives/value/Totals.js";
|
|
14
|
+
import { Pencil, Plus, Trash2 } from "lucide-react";
|
|
15
|
+
import { useEffect, useId, useMemo, useRef, useState } from "react";
|
|
16
|
+
/** The notice is optional: without a `ToastProvider` the line is still removed, just
|
|
17
|
+
* without the way back — a missing provider must not break a form. */
|
|
18
|
+
function useOptionalToast() {
|
|
19
|
+
try {
|
|
20
|
+
return useToast();
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function plain(v) {
|
|
27
|
+
return v === null ? "" : String(v);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The lines of a document inside its record: a count and a search above, the lines
|
|
31
|
+
* (edited in place or in a popup editor), and the totals under them on the right.
|
|
32
|
+
*
|
|
33
|
+
* Context: the record used to compose this by hand — a `Table`, its rows, a
|
|
34
|
+
* `Totals` — and every module did it a little differently: where «add» went, whether
|
|
35
|
+
* removing asked first, what an empty document said. One component, built on
|
|
36
|
+
* `EditableTable` and `Dialog`, makes the one2many the same block everywhere.
|
|
37
|
+
*/
|
|
38
|
+
export function LineItems({ lines, columns, getLineId, lineLabel, mode, onChange, onAdd, newLine, onRemove, onRestore, editor, isValid, context, searchText, searchFrom = 8, totals, layout = "auto", }) {
|
|
39
|
+
const copy = useUiCopy();
|
|
40
|
+
const toast = useOptionalToast();
|
|
41
|
+
const read = useFormMode(mode) === "read";
|
|
42
|
+
const root = useRef(null);
|
|
43
|
+
const searchLabelId = useId();
|
|
44
|
+
const formId = useId();
|
|
45
|
+
const [query, setQuery] = useState("");
|
|
46
|
+
const [session, setSession] = useState(null);
|
|
47
|
+
const [open, setOpen] = useState(false);
|
|
48
|
+
/* The draft the form submits, read from a ref: Enter in a number field commits
|
|
49
|
+
the typed text and submits in the same keystroke, before a re-render. */
|
|
50
|
+
const draftRef = useRef(null);
|
|
51
|
+
/* Where focus goes after a removal: the button that was pressed is gone with its
|
|
52
|
+
row, and focus falling to the page is losing the place. */
|
|
53
|
+
const focusNext = useRef(null);
|
|
54
|
+
const canChange = !read && onChange !== undefined;
|
|
55
|
+
const canAdd = !read && onAdd !== undefined && newLine !== undefined;
|
|
56
|
+
const canRemove = !read && onRemove !== undefined;
|
|
57
|
+
const canEdit = canChange && editor !== undefined;
|
|
58
|
+
const labelOf = (line) => {
|
|
59
|
+
if (lineLabel !== undefined)
|
|
60
|
+
return lineLabel(line);
|
|
61
|
+
const first = columns[0];
|
|
62
|
+
if (first === undefined)
|
|
63
|
+
return getLineId(line);
|
|
64
|
+
return first.text?.(line) ?? plain(first.value(line));
|
|
65
|
+
};
|
|
66
|
+
const searchable = lines.length > searchFrom;
|
|
67
|
+
const q = searchable ? query.trim() : "";
|
|
68
|
+
const shown = useMemo(() => filterLines(lines, q, (line) => searchText !== undefined
|
|
69
|
+
? searchText(line)
|
|
70
|
+
: columns.map((c) => c.text?.(line) ?? plain(c.value(line))).join(" "), copy.locale), [lines, columns, searchText, q, copy.locale]);
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
const target = focusNext.current;
|
|
73
|
+
if (target === null)
|
|
74
|
+
return;
|
|
75
|
+
focusNext.current = null;
|
|
76
|
+
const scope = root.current;
|
|
77
|
+
if (scope === null)
|
|
78
|
+
return;
|
|
79
|
+
/* The next line's first button on a desk; on a phone its card, which has none. */
|
|
80
|
+
const next = target.id === null
|
|
81
|
+
? null
|
|
82
|
+
: (scope.querySelector(`[data-line="${CSS.escape(target.id)}"] button`) ?? scope.querySelector(`[data-row-id="${CSS.escape(target.id)}"]`));
|
|
83
|
+
(next ?? scope.querySelector("[data-line-add]"))?.focus();
|
|
84
|
+
}, [lines]);
|
|
85
|
+
/* ── the editor ─────────────────────────────────────────────────── */
|
|
86
|
+
const openEditor = (id, draft) => {
|
|
87
|
+
draftRef.current = draft;
|
|
88
|
+
setSession({ id, draft, tried: false });
|
|
89
|
+
setOpen(true);
|
|
90
|
+
};
|
|
91
|
+
const setDraft = (next) => {
|
|
92
|
+
draftRef.current = next;
|
|
93
|
+
setSession((s) => (s === null ? s : { ...s, draft: next }));
|
|
94
|
+
};
|
|
95
|
+
const save = () => {
|
|
96
|
+
const draft = draftRef.current;
|
|
97
|
+
if (session === null || draft === null)
|
|
98
|
+
return;
|
|
99
|
+
const commit = commitDraft(session.id, draft, isValid);
|
|
100
|
+
if (commit.kind === "invalid") {
|
|
101
|
+
setSession({ ...session, draft, tried: true });
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (commit.kind === "add") {
|
|
105
|
+
/* A new line is shown even if it does not match the search: adding one and
|
|
106
|
+
not seeing it is the worst answer. */
|
|
107
|
+
setQuery("");
|
|
108
|
+
onAdd?.(commit.line);
|
|
109
|
+
}
|
|
110
|
+
else
|
|
111
|
+
onChange?.(commit.id, commit.line);
|
|
112
|
+
setOpen(false);
|
|
113
|
+
};
|
|
114
|
+
const add = () => {
|
|
115
|
+
if (newLine === undefined)
|
|
116
|
+
return;
|
|
117
|
+
const blank = newLine();
|
|
118
|
+
if (editor === undefined) {
|
|
119
|
+
setQuery("");
|
|
120
|
+
onAdd?.(blank);
|
|
121
|
+
}
|
|
122
|
+
else
|
|
123
|
+
openEditor(null, blank);
|
|
124
|
+
};
|
|
125
|
+
const remove = (line) => {
|
|
126
|
+
const id = getLineId(line);
|
|
127
|
+
const { index, focus } = planRemoval(lines, shown, id, getLineId);
|
|
128
|
+
focusNext.current = { id: focus };
|
|
129
|
+
onRemove?.(id);
|
|
130
|
+
toast?.show({
|
|
131
|
+
message: fillCopy(copy.lineItems.removed, { name: labelOf(line) }),
|
|
132
|
+
...(onRestore === undefined
|
|
133
|
+
? {}
|
|
134
|
+
: {
|
|
135
|
+
action: {
|
|
136
|
+
label: copy.lineItems.undo,
|
|
137
|
+
onSelect: () => onRestore(line, index),
|
|
138
|
+
},
|
|
139
|
+
}),
|
|
140
|
+
});
|
|
141
|
+
};
|
|
142
|
+
/* ── the table ──────────────────────────────────────────────────── */
|
|
143
|
+
const tableColumns = columns.map((c) => ({
|
|
144
|
+
id: c.id,
|
|
145
|
+
header: c.header,
|
|
146
|
+
...(c.width === undefined ? {} : { width: c.width }),
|
|
147
|
+
...(c.numeric === undefined ? {} : { numeric: c.numeric }),
|
|
148
|
+
...(c.stack === undefined ? {} : { stack: c.stack }),
|
|
149
|
+
...(c.brief === undefined ? {} : { brief: c.brief }),
|
|
150
|
+
render: (line) => {
|
|
151
|
+
const drawn = c.cell === undefined ? plain(c.value(line)) : c.cell(line);
|
|
152
|
+
/* An amount never breaks in two: «Bs.» over «12.500,00» reads as two values. */
|
|
153
|
+
return c.numeric === true ? _jsx("span", { className: "whitespace-nowrap", children: drawn }) : drawn;
|
|
154
|
+
},
|
|
155
|
+
...(c.edit === undefined || !canChange
|
|
156
|
+
? {}
|
|
157
|
+
: {
|
|
158
|
+
/* The control takes the column's width and does not impose its own: an input's
|
|
159
|
+
natural width is twenty characters, and in an auto-sized table that pushes
|
|
160
|
+
the amounts off the card the moment a row goes into editing. */
|
|
161
|
+
edit: (line, set) => (_jsx("div", { className: "contain-inline-size", children: c.edit?.(line, set) })),
|
|
162
|
+
}),
|
|
163
|
+
}));
|
|
164
|
+
if (canEdit || canRemove) {
|
|
165
|
+
tableColumns.push({
|
|
166
|
+
id: "\u0000actions",
|
|
167
|
+
header: "",
|
|
168
|
+
width: 1,
|
|
169
|
+
/* On a card the press opens the editor, and the editor removes: the buttons
|
|
170
|
+
would be two more targets on a small screen. Without an editor the trash
|
|
171
|
+
is the only way to remove, so it stays — at the card's top right, lit. */
|
|
172
|
+
stack: canEdit ? "hidden" : "trail",
|
|
173
|
+
render: (line) => {
|
|
174
|
+
const id = getLineId(line);
|
|
175
|
+
const nameId = `${formId}-line-${id}`;
|
|
176
|
+
return (
|
|
177
|
+
/* Hidden until the row is hovered or holds focus, like `RowActions`: a pencil
|
|
178
|
+
and a trash can lit on each of twenty lines are forty invitations. They
|
|
179
|
+
fade instead of unmounting, so Tab still reaches them. Pressing them does
|
|
180
|
+
not put the row into in-place editing. */
|
|
181
|
+
_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 ? (_jsx(IconButton, { icon: Pencil, label: copy.lines.edit, "aria-describedby": nameId, haspopup: "dialog", onClick: () => openEditor(id, line) })) : null, canRemove ? (_jsx(IconButton, { icon: Trash2, label: copy.lines.remove, tone: "danger", "aria-describedby": nameId, onClick: () => remove(line) })) : null] }));
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
const addButton = canAdd ? (_jsx(Button, { role: "secondary", size: "sm", icon: Plus, onClick: add, "data-line-add": "", children: copy.lines.add })) : null;
|
|
186
|
+
const count = q === ""
|
|
187
|
+
? fillCopy(lines.length === 1 ? copy.lineItems.one : copy.lineItems.many, {
|
|
188
|
+
count: lines.length.toLocaleString(copy.locale),
|
|
189
|
+
})
|
|
190
|
+
: fillCopy(copy.lineItems.shown, {
|
|
191
|
+
shown: shown.length.toLocaleString(copy.locale),
|
|
192
|
+
count: lines.length.toLocaleString(copy.locale),
|
|
193
|
+
});
|
|
194
|
+
return (_jsxs("section", { ref: root, "aria-label": copy.lineItems.table, className: "flex min-w-0 flex-col gap-4", children: [lines.length === 0 ? (
|
|
195
|
+
/* Said, with its way out: an empty document is a question («what is sold
|
|
196
|
+
here?»), not a blank box. In read mode there is no way out to offer. */
|
|
197
|
+
_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: {
|
|
198
|
+
id: `${searchLabelId}-input`,
|
|
199
|
+
labelId: searchLabelId,
|
|
200
|
+
describedBy: undefined,
|
|
201
|
+
invalid: false,
|
|
202
|
+
}, 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 }), addButton === null ? null : _jsx("span", { className: "ml-auto", children: addButton })] }), _jsx(EditableTable, { rows: shown, columns: tableColumns, getRowId: getLineId, empty: fillCopy(copy.lineItems.noMatch, { query: query.trim() }), layout: layout, ...(canEdit
|
|
203
|
+
? {
|
|
204
|
+
onOpen: (id) => {
|
|
205
|
+
const line = lines.find((l) => getLineId(l) === id);
|
|
206
|
+
if (line !== undefined)
|
|
207
|
+
openEditor(id, line);
|
|
208
|
+
},
|
|
209
|
+
}
|
|
210
|
+
: {}), ...(canChange && columns.some((c) => c.edit !== undefined)
|
|
211
|
+
? { onChange: (id, next) => onChange?.(id, next) }
|
|
212
|
+
: {}) })] })), totals === undefined || totals.length === 0 ? null : (
|
|
213
|
+
/* Inset like the table's cells, so the last figure lines up with the amounts. */
|
|
214
|
+
_jsx("div", { className: "px-4", children: _jsx(Totals, { rows: 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 : (
|
|
215
|
+
/* Apart, at the other end from «Listo»: removing is not the way the
|
|
216
|
+
form is meant to close. */
|
|
217
|
+
_jsx("span", { className: "mr-auto", children: _jsx(IconButton, { icon: Trash2, label: copy.lines.remove, tone: "danger", onClick: () => {
|
|
218
|
+
const line = lines.find((l) => getLineId(l) === session.id);
|
|
219
|
+
setOpen(false);
|
|
220
|
+
if (line !== undefined)
|
|
221
|
+
remove(line);
|
|
222
|
+
} }) })), _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) => {
|
|
223
|
+
e.preventDefault();
|
|
224
|
+
save();
|
|
225
|
+
}, className: "flex flex-col gap-3", children: editor(session.draft, setDraft, {
|
|
226
|
+
tried: session.tried,
|
|
227
|
+
isNew: session.id === null,
|
|
228
|
+
}) }) }))] }));
|
|
229
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ColumnControl, ColumnSpec } from "./columns.js";
|
|
2
|
+
import type { Surface } from "../../foundations/types.js";
|
|
3
|
+
/**
|
|
4
|
+
* The Odoo columns submenu: required fields stay checked and cannot be
|
|
5
|
+
* unchecked; optional fields toggle. It stays open while toggling so hiding
|
|
6
|
+
* three columns is three clicks, not three open-close round trips.
|
|
7
|
+
*
|
|
8
|
+
* Drawn only when the set declared optional columns. A menu of required-only
|
|
9
|
+
* fields would be a list of things you cannot do.
|
|
10
|
+
*/
|
|
11
|
+
export declare function ColumnsMenu({ control, surface, }: {
|
|
12
|
+
control: ColumnControl;
|
|
13
|
+
surface?: Surface;
|
|
14
|
+
}): import("react").JSX.Element | null;
|
|
15
|
+
export declare function specsFromColumns(columns: readonly {
|
|
16
|
+
id: string;
|
|
17
|
+
header: string;
|
|
18
|
+
optional?: boolean;
|
|
19
|
+
defaultHidden?: boolean;
|
|
20
|
+
}[], fallbackOptional: boolean): ColumnSpec[];
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useUiCopy } from "../../foundations/copy.js";
|
|
4
|
+
import { Menu } from "../../primitives/menu/Menu.js";
|
|
5
|
+
import { Columns3 } from "lucide-react";
|
|
6
|
+
/**
|
|
7
|
+
* The Odoo columns submenu: required fields stay checked and cannot be
|
|
8
|
+
* unchecked; optional fields toggle. It stays open while toggling so hiding
|
|
9
|
+
* three columns is three clicks, not three open-close round trips.
|
|
10
|
+
*
|
|
11
|
+
* Drawn only when the set declared optional columns. A menu of required-only
|
|
12
|
+
* fields would be a list of things you cannot do.
|
|
13
|
+
*/
|
|
14
|
+
export function ColumnsMenu({ control, surface, }) {
|
|
15
|
+
const copy = useUiCopy();
|
|
16
|
+
if (!control.hasOptional)
|
|
17
|
+
return null;
|
|
18
|
+
const requiredOptions = control.required.map((column) => ({
|
|
19
|
+
id: column.id,
|
|
20
|
+
label: column.label,
|
|
21
|
+
checked: true,
|
|
22
|
+
disabled: true,
|
|
23
|
+
hint: copy.frame.required,
|
|
24
|
+
}));
|
|
25
|
+
const optionalOptions = control.optional.map((column) => ({
|
|
26
|
+
id: column.id,
|
|
27
|
+
label: column.label,
|
|
28
|
+
checked: control.isVisible(column.id),
|
|
29
|
+
}));
|
|
30
|
+
const groups = requiredOptions.length === 0
|
|
31
|
+
? [{ label: copy.frame.show, options: optionalOptions }]
|
|
32
|
+
: [
|
|
33
|
+
{ label: copy.frame.required, options: requiredOptions },
|
|
34
|
+
{ label: copy.frame.optional, options: optionalOptions },
|
|
35
|
+
];
|
|
36
|
+
const hiddenOptional = control.optional.some((column) => !control.isVisible(column.id));
|
|
37
|
+
return (_jsx(Menu, { label: copy.frame.columns, icon: Columns3, groups: groups, onSelect: control.toggle, closeOnSelect: false, active: hiddenOptional, ...(surface === undefined ? {} : { surface }), size: "sm" }));
|
|
38
|
+
}
|
|
39
|
+
export function specsFromColumns(columns, fallbackOptional) {
|
|
40
|
+
return columns.map((column) => ({
|
|
41
|
+
id: column.id,
|
|
42
|
+
label: column.header,
|
|
43
|
+
optional: column.optional ?? fallbackOptional,
|
|
44
|
+
...(column.defaultHidden === true ? { defaultHidden: true } : {}),
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { GroupingControl } from "./grouping.js";
|
|
2
|
+
import { type GroupableField, type GroupGrain } from "../../foundations/grouping.js";
|
|
3
|
+
/**
|
|
4
|
+
* «AGRUPAR POR», adentro del panel de recortes.
|
|
5
|
+
*
|
|
6
|
+
* Context: va en la MISMA columna donde están los filtros y los favoritos, y no
|
|
7
|
+
* en un control aparte. Filtrar y agrupar son la misma pregunta —«qué parte de
|
|
8
|
+
* esto estoy mirando»— y separarlos obliga a abrir dos cosas para armar un
|
|
9
|
+
* recorte, sin poder ver los dos estados a la vez.
|
|
10
|
+
*
|
|
11
|
+
* ── Una fecha se DESPLIEGA, no se elige ─────────────────────────────
|
|
12
|
+
*
|
|
13
|
+
* Un campo común es una opción: se tilda y listo. Un campo de fecha no, porque
|
|
14
|
+
* «agrupar por fecha» no quiere decir nada hasta decir con qué corte — y los
|
|
15
|
+
* cortes no son excluyentes: «por mes y adentro por día» es una pregunta
|
|
16
|
+
* legítima y frecuente. Así que una fecha se abre y ofrece sus granos como
|
|
17
|
+
* CASILLAS, que se tildan de a varias.
|
|
18
|
+
*
|
|
19
|
+
* ── Y se ordenan solas ──────────────────────────────────────────────
|
|
20
|
+
*
|
|
21
|
+
* Tildar «día» y después «mes» da «mes › día», nunca al revés. No es una
|
|
22
|
+
* preferencia: un día entero vive adentro de un solo mes, así que partir primero
|
|
23
|
+
* por día deja un nivel de mes que no parte nada. La dirección la pone el
|
|
24
|
+
* calendario y por eso la pone el sistema — ver `normalizeLevels`.
|
|
25
|
+
*/
|
|
26
|
+
export type GroupingLevelsProps = {
|
|
27
|
+
/** Por qué campos se puede partir. Sin ninguno no se dibuja nada: una columna
|
|
28
|
+
* vacía en el panel es peor que una columna que no está. */
|
|
29
|
+
fields: readonly GroupableField[];
|
|
30
|
+
control: GroupingControl;
|
|
31
|
+
/** Cómo se llama cada grano, cuando el producto lo dice con otras palabras.
|
|
32
|
+
* Sin esto, las de la copia en el idioma activo. */
|
|
33
|
+
grainLabel?: (grain: GroupGrain) => string;
|
|
34
|
+
};
|
|
35
|
+
export declare function GroupingLevels({ fields, control, grainLabel }: GroupingLevelsProps): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { grainName } from "../../foundations/calendar.js";
|
|
4
|
+
import { useUiCopy } from "../../foundations/copy.js";
|
|
5
|
+
import { GRAIN_ORDER, levelKey, } from "../../foundations/grouping.js";
|
|
6
|
+
import { ChevronDown, ChevronRight, ChevronUp, X } from "lucide-react";
|
|
7
|
+
import { useState } from "react";
|
|
8
|
+
function fieldLabel(fields, field) {
|
|
9
|
+
return fields.find((one) => one.field === field)?.label ?? field;
|
|
10
|
+
}
|
|
11
|
+
export function GroupingLevels({ fields, control, grainLabel }) {
|
|
12
|
+
const copy = useUiCopy();
|
|
13
|
+
/* Qué fechas están desplegadas. Arranca desplegada la que ya tenga algún grano
|
|
14
|
+
puesto: si no, lo puesto queda escondido detrás de un chevron y hay que
|
|
15
|
+
adivinar dónde. */
|
|
16
|
+
const [opened, setOpened] = useState(() => control.levels.filter((one) => one.grain !== undefined).map((one) => one.field));
|
|
17
|
+
if (fields.length === 0)
|
|
18
|
+
return null;
|
|
19
|
+
const placed = control.levels;
|
|
20
|
+
const grain = (one) => grainLabel?.(one) ?? grainName(one);
|
|
21
|
+
const isOpen = (field) => opened.includes(field);
|
|
22
|
+
const toggleOpen = (field) => setOpened((current) => current.includes(field) ? current.filter((one) => one !== field) : [...current, field]);
|
|
23
|
+
return (_jsxs("div", { className: "flex flex-col gap-0.5", children: [placed.length === 0 ? null : (_jsxs(_Fragment, { children: [_jsx("ol", { className: "flex flex-wrap gap-1 px-1 pb-1.5", children: placed.map((level, index) => (_jsx("li", { children: _jsxs("span", { className: "inline-flex items-center gap-1 rounded-pill border border-accent/40 bg-accent/18 py-0.5 pr-0.5 pl-1.5 text-micro text-ink", children: [_jsx("span", { className: "tabular-nums opacity-70", children: index + 1 }), _jsxs("span", { className: "max-w-32 truncate", children: [fieldLabel(fields, level.field), level.grain === undefined ? "" : ` · ${grain(level.grain)}`] }), _jsx("button", { type: "button", onClick: () => control.move(level.field, -1), disabled: index === 0, "aria-label": copy.grouping.up, title: copy.grouping.up, className: "rounded-pill p-0.5 hover:bg-primary/15 disabled:opacity-30 disabled:hover:bg-transparent", children: _jsx(ChevronUp, { className: "size-3", strokeWidth: 2.5 }) }), _jsx("button", { type: "button", onClick: () => control.move(level.field, 1), disabled: index === placed.length - 1, "aria-label": copy.grouping.down, title: copy.grouping.down, className: "rounded-pill p-0.5 hover:bg-primary/15 disabled:opacity-30 disabled:hover:bg-transparent", children: _jsx(ChevronDown, { className: "size-3", strokeWidth: 2.5 }) }), _jsx("button", { type: "button", onClick: () => control.remove(level.field, level.grain), "aria-label": copy.grouping.remove, title: copy.grouping.remove, className: "rounded-pill p-0.5 hover:bg-primary/15", children: _jsx(X, { className: "size-3", strokeWidth: 2.5 }) })] }) }, levelKey(level)))) }), _jsx("div", { className: "mb-1 h-px bg-line", role: "separator" })] })), fields.map((one) => {
|
|
24
|
+
const grains = one.grains;
|
|
25
|
+
/* Un campo común: una casilla y nada más. */
|
|
26
|
+
if (grains === undefined || grains.length === 0) {
|
|
27
|
+
const on = control.has(one.field);
|
|
28
|
+
return (_jsxs("button", { type: "button", onClick: () => control.toggle(one.field), "aria-pressed": on, className: `flex items-center gap-2 rounded-control px-1.5 py-1 text-left transition-colors duration-(--duration-instant) hover:bg-primary/8 ${on ? "bg-primary/6" : ""}`, children: [_jsx(Mark, { on: on }), _jsx("span", { className: `min-w-0 truncate text-meta ${on ? "font-medium text-ink" : "text-ink"}`, children: one.label })] }, one.field));
|
|
29
|
+
}
|
|
30
|
+
/* Una FECHA: se despliega y ofrece sus cortes como casillas. */
|
|
31
|
+
const open = isOpen(one.field);
|
|
32
|
+
const puestos = placed.filter((level) => level.field === one.field);
|
|
33
|
+
return (_jsxs("div", { className: "flex flex-col", children: [_jsxs("button", { type: "button", onClick: () => toggleOpen(one.field), "aria-expanded": open, className: "flex items-center gap-1.5 rounded-control px-1.5 py-1 text-left transition-colors duration-(--duration-instant) hover:bg-primary/8", children: [_jsx(ChevronRight, { className: `size-(--size-icon-sm) shrink-0 text-muted transition-transform duration-(--duration-instant) ${open ? "rotate-90" : ""}`, strokeWidth: 2, "aria-hidden": true }), _jsx("span", { className: `min-w-0 flex-1 truncate text-meta ${puestos.length > 0 ? "font-medium text-ink" : "text-ink"}`, children: one.label }), puestos.length === 0 ? null : (_jsx("span", { className: "inline-flex shrink-0 items-center rounded-pill border border-accent/40 bg-accent/18 px-2 py-0.5 text-micro tabular-nums text-ink", children: puestos.length }))] }), !open ? null : (_jsx("div", { className: "flex flex-col pl-(--spacing-indent)", children: GRAIN_ORDER.filter((g) => grains.includes(g)).map((g) => {
|
|
34
|
+
const on = control.has(one.field, g);
|
|
35
|
+
return (_jsxs("button", { type: "button", onClick: () => control.toggle(one.field, g), "aria-pressed": on, className: `flex items-center gap-2 rounded-control px-1.5 py-1 text-left transition-colors duration-(--duration-instant) hover:bg-primary/8 ${on ? "bg-primary/6" : ""}`, children: [_jsx(Mark, { on: on }), _jsx("span", { className: `min-w-0 truncate text-meta ${on ? "font-medium text-ink" : "text-muted"}`, children: grain(g) })] }, g));
|
|
36
|
+
}) }))] }, one.field));
|
|
37
|
+
}), placed.length === 0 ? null : (_jsxs(_Fragment, { children: [_jsx("div", { className: "my-1 h-px bg-line", role: "separator" }), _jsx("button", { type: "button", onClick: () => control.clear(), className: "rounded-control px-2 py-1 text-left text-meta text-muted transition-colors duration-(--duration-instant) hover:bg-ink/4 hover:text-ink", children: copy.grouping.clear })] }))] }));
|
|
38
|
+
}
|
|
39
|
+
/** La marca de tildado. La columna existe SIEMPRE, tildada o no: sin ella,
|
|
40
|
+
* tildar corre el texto y la lista salta. Apagada se dibuja el aro, la misma
|
|
41
|
+
* marca que el panel de filtros, para que las dos columnas se lean como
|
|
42
|
+
* casillas y no como texto suelto con un hueco a la izquierda. */
|
|
43
|
+
function Mark({ on }) {
|
|
44
|
+
return (_jsx("span", { className: "flex size-(--size-icon-sm) shrink-0 items-center justify-center", children: on ? (_jsx("span", { className: "size-1.5 rounded-pill bg-primary" })) : (_jsx("span", { className: "size-3 rounded-pill border border-line-strong" })) }));
|
|
45
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Surface } from "../../foundations/types.js";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
export declare function Listing({ bar, metrics, sticky, surface, children, }: {
|
|
4
|
+
/** The `ListingBar`: actions, title, search, pagination and views. */
|
|
5
|
+
bar?: ReactNode;
|
|
6
|
+
/**
|
|
7
|
+
* La barra se queda arriba al scrollear, debajo de lo que ya esté pegado.
|
|
8
|
+
*
|
|
9
|
+
* Es opt-in y no el default porque el fondo tiene que ser el de lo que hay
|
|
10
|
+
* DETRÁS, y eso depende de dónde esté puesta: el arquetipo de lista vive
|
|
11
|
+
* sobre el piso de la página, pero un `FrameView` —«el vistazo» de un
|
|
12
|
+
* registro— vive sobre el PAPEL blanco, y ahí la banda color piso se ve como
|
|
13
|
+
* una mancha. Se descubrió comparando historias, no razonando.
|
|
14
|
+
*/
|
|
15
|
+
sticky?: boolean;
|
|
16
|
+
/** Sobre qué está puesta la lista. Sólo se usa para el fondo de la banda que
|
|
17
|
+
* se pega: `chrome` pinta el mismo azul que ya pinta la barra —o sea,
|
|
18
|
+
* invisible— y cualquier otra pinta el PISO, que es lo que hay detrás de una
|
|
19
|
+
* lista de página. Las que viven adentro de algo blanco —un cajón, una vista
|
|
20
|
+
* previa— son compactas, y en compacto la barra no se pega. */
|
|
21
|
+
surface?: Surface;
|
|
22
|
+
/** The metrics that also narrow down. They go BELOW the bar and above the
|
|
23
|
+
* rows: they belong to the set I am looking at, not to the frame that frames
|
|
24
|
+
* it — above the bar they read as part of the navigation. */
|
|
25
|
+
metrics?: ReactNode;
|
|
26
|
+
children: ReactNode;
|
|
27
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useHeaderSlot } from "../shell/headerSlot.js";
|
|
4
|
+
import { byDensity, useDensity } from "../../foundations/density.js";
|
|
5
|
+
import { useStickyLayer } from "../../foundations/sticky.js";
|
|
6
|
+
import { createPortal } from "react-dom";
|
|
7
|
+
/* ==================================================================== *
|
|
8
|
+
* LISTING — the shape of a screen of N records. A single one, for all of them.
|
|
9
|
+
*
|
|
10
|
+
* The request was literal: «in every list window don't forget that it has
|
|
11
|
+
* [Search (Filters)] [List]». Up to here every view built that by hand —one
|
|
12
|
+
* put the totals on top, another the search box below the upload panel, another
|
|
13
|
+
* had no search box at all— and the result is that two lists of the same
|
|
14
|
+
* product get used differently.
|
|
15
|
+
*
|
|
16
|
+
* Four bands, always in the same order:
|
|
17
|
+
*
|
|
18
|
+
* bar crumbs · search · pagination
|
|
19
|
+
* actions · title · columns · views
|
|
20
|
+
* body the rows, in a card with a border
|
|
21
|
+
*
|
|
22
|
+
* ── The list goes in a card and the background stays behind ─────────
|
|
23
|
+
*
|
|
24
|
+
* It is what separates an application from a document. Without the card the
|
|
25
|
+
* rows float over nothing and the screen reads as running text; the card puts
|
|
26
|
+
* a boundary around the set, and that boundary is what makes it read as «one
|
|
27
|
+
* thing with elements inside».
|
|
28
|
+
*
|
|
29
|
+
* ── Two densities, and both are needed ──────────────────────────────
|
|
30
|
+
*
|
|
31
|
+
* `table` aligned, sortable columns. It wins when there is width: the data
|
|
32
|
+
* gets compared across rows without reading, which is what a table
|
|
33
|
+
* exists for.
|
|
34
|
+
* `rows` two lines per record, big name and metadata underneath. It wins
|
|
35
|
+
* when there is NO width —a narrow panel, the preview— because a
|
|
36
|
+
* six-column table in 380 px is not a table, it is an accordion.
|
|
37
|
+
*
|
|
38
|
+
* It is not an aesthetic preference: it is the available width. That is why
|
|
39
|
+
* the consumer passes it and this piece does not choose it — only whoever
|
|
40
|
+
* composes knows which surface it is going to land on.
|
|
41
|
+
*
|
|
42
|
+
* ── The row lays itself out; nobody tells it how ────────────────────
|
|
43
|
+
*
|
|
44
|
+
* WHICH presentation is the consumer's call. How a row arranges itself inside
|
|
45
|
+
* the one that was chosen is nobody's: there is no width at which a row «turns
|
|
46
|
+
* narrow», and no breakpoint written anywhere. Every zone asks for what it
|
|
47
|
+
* needs —the name `--container-name`, the tags `--container-tags`, the
|
|
48
|
+
* metadata what it says— and the line does what a line does: what does not fit
|
|
49
|
+
* wraps, and the name keeps the first one.
|
|
50
|
+
*
|
|
51
|
+
* **That is why it holds up with a checkbox, an icon, actions, no tags, or a
|
|
52
|
+
* metadata three times longer.** None of that is declared anywhere: they all
|
|
53
|
+
* take their width from the same line, so a list that selects wraps a little
|
|
54
|
+
* earlier than one that does not, on its own. Asking the density instead —a
|
|
55
|
+
* decision per SURFACE— got it wrong in the one place it mattered: a list in a
|
|
56
|
+
* 480 px drawer is comfortable —it draws its title, its counters, its full
|
|
57
|
+
* padding— and narrow at the same time, and the name was left with whatever
|
|
58
|
+
* was over between a metadata that did not shrink and a tag column of 8 rem:
|
|
59
|
+
* about a dozen pixels, «ve…».
|
|
60
|
+
* ==================================================================== */
|
|
61
|
+
export function Listing({ bar, metrics, sticky = false, surface, children, }) {
|
|
62
|
+
const d = useDensity();
|
|
63
|
+
/* La barra se PEGA arriba y las filas se van debajo de ella.
|
|
64
|
+
Context: con 187 filas, scrollear para ver la última dejaba sin buscador,
|
|
65
|
+
sin paginador y sin el conmutador de presentación — o sea que recortar
|
|
66
|
+
después de mirar obligaba a subir de nuevo. Se pega debajo de lo que ya
|
|
67
|
+
esté pegado (la barra de la aplicación) y empuja la pila para el
|
|
68
|
+
encabezado de la tabla, que se pega debajo de ella. Ver `system/sticky.tsx`. */
|
|
69
|
+
const band = useStickyLayer();
|
|
70
|
+
/* En compacto NO se pega, y no es por falta de espacio: compacto es un cajón
|
|
71
|
+
o una vista previa, o sea una caja corta con su propia cabecera y su propio
|
|
72
|
+
scroll. Una banda pegada ahí se pinta encima del papel blanco del cajón —se
|
|
73
|
+
ve como una mancha— para resolver un scroll que en cinco filas no existe.
|
|
74
|
+
Es la misma razón por la que `ListingBar` tampoco dibuja la identidad ahí. */
|
|
75
|
+
const pegada = sticky && d === "comfortable";
|
|
76
|
+
/* With a sidebar the bar is the screen's header: it goes in the frame's full-width slot,
|
|
77
|
+
above the sidebar (see `shell/headerSlot.ts`), and the frame does the sticking. */
|
|
78
|
+
const slot = useHeaderSlot();
|
|
79
|
+
const inSlot = pegada && slot !== null;
|
|
80
|
+
return (
|
|
81
|
+
/* Context: the box of rows carries no border. What separates it from the
|
|
82
|
+
background is its white and its radius; a frame around a list that is
|
|
83
|
+
already inside a framed panel is a nested rectangle, and nested
|
|
84
|
+
rectangles are what make a product read like a cloud console. */
|
|
85
|
+
_jsxs("div", { className: `flex min-w-0 flex-col ${byDensity(d, "gap-2.5", "gap-2")}`, children: [inSlot ? (createPortal(bar, slot)) : (_jsx("div", { ref: band.ref, className: `min-w-0 ${pegada ? `sticky z-30 ${surface === "chrome" ? "bg-chrome" : "bg-ground"}` : ""}`, style: pegada ? band.self : undefined, children: bar })), inSlot && metrics !== undefined ? _jsx("div", { className: "pt-2", children: metrics }) : metrics, _jsx("div", { className: "min-w-0 overflow-clip rounded-box bg-surface", style: pegada && !inSlot ? band.below : undefined, children: children })] }));
|
|
86
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { Surface } from "../../foundations/types.js";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
/** THE BAR — two rows, two questions.
|
|
4
|
+
*
|
|
5
|
+
* ┌──────────┬──────────────────────────────┬──────────────┐
|
|
6
|
+
* │ crumbs │ 🔍 search… ⚙3 │ 1–8 of 187 ‹ › │
|
|
7
|
+
* ├──────────┼──────────────────────────────┼──────────────┤
|
|
8
|
+
* │ [New] │ Facturas · 180 · 7 · 8 │ [columns] [▤][▦] │
|
|
9
|
+
* └──────────┴──────────────────────────────┴──────────────┘
|
|
10
|
+
* WHERE / HOW MUCH I SEE HOW I LOOK
|
|
11
|
+
*
|
|
12
|
+
* Pagination answers how much of the filtered set is on screen, so it stays
|
|
13
|
+
* with the search. Columns and the presentation switcher answer how the set
|
|
14
|
+
* is looked at, so they sit with the actions — not next to the page range.
|
|
15
|
+
* Putting the four of them on the search line is how the field and «1–80»
|
|
16
|
+
* painted over each other the moment the window shrank.
|
|
17
|
+
*
|
|
18
|
+
* ── Why it is two lines tall ────────────────────────────────────────
|
|
19
|
+
*
|
|
20
|
+
* Because the three things that go on the left do not fit in one: a
|
|
21
|
+
* comfortable button is 34 px tall, a list title may have to wrap onto two
|
|
22
|
+
* lines, and below it goes a sentence saying what the list is made of. Forcing
|
|
23
|
+
* it into a single line means shrinking the button —the defect we already
|
|
24
|
+
* fixed— or truncating the title, which is worse: a list that cannot be named
|
|
25
|
+
* in full is indistinguishable from the one next to it.
|
|
26
|
+
*
|
|
27
|
+
* ── The selection transforms the bar, it does not add one ───────────
|
|
28
|
+
*
|
|
29
|
+
* When rows get selected, the identity zone turns into «3 elegidas ✕» plus the
|
|
30
|
+
* actions that apply. **In the same place and without pushing anything.** A
|
|
31
|
+
* bar that appears shifts the list downwards right when somebody is ticking
|
|
32
|
+
* checkboxes, and the row they were about to tick is no longer where it was.
|
|
33
|
+
*
|
|
34
|
+
* The actions on the left do NOT go away: «Nuevo» stays available with three
|
|
35
|
+
* rows selected, because selecting is not entering a mode. */
|
|
36
|
+
export declare function ListingBar({ actions, title, description, counters, selection, search, pagination, views, crumbs, surface, }: {
|
|
37
|
+
/** What starts something new. It stays visible even when there is a selection. */
|
|
38
|
+
actions?: ReactNode;
|
|
39
|
+
title?: string;
|
|
40
|
+
/** One sentence: what this list is made of, or what can be done with it. It
|
|
41
|
+
* goes below the title and that is why it can be a real sentence. */
|
|
42
|
+
description?: string;
|
|
43
|
+
/** The totals per category. The ones in the pagination say how many there
|
|
44
|
+
* are; these say what they are made of, which is another question. */
|
|
45
|
+
counters?: readonly {
|
|
46
|
+
label: string;
|
|
47
|
+
value: number | string;
|
|
48
|
+
}[];
|
|
49
|
+
/** When there are selected rows. It replaces the identity, in the same place. */
|
|
50
|
+
selection?: {
|
|
51
|
+
count: number;
|
|
52
|
+
onClear?: () => void;
|
|
53
|
+
actions?: ReactNode;
|
|
54
|
+
};
|
|
55
|
+
search?: ReactNode;
|
|
56
|
+
pagination?: ReactNode;
|
|
57
|
+
/** Columns menu and presentation switcher. They sit on the actions row, on
|
|
58
|
+
* the right: they are how I look at the set, not how many of it I see. */
|
|
59
|
+
views?: ReactNode;
|
|
60
|
+
/** Where I come from. It goes above the row and only on the chrome. */
|
|
61
|
+
crumbs?: ReactNode;
|
|
62
|
+
/** `chrome` paints the whole band navy and everything inside inherits it:
|
|
63
|
+
* the search box, the pagination and the switcher. See `system/surface.tsx`. */
|
|
64
|
+
surface?: Surface;
|
|
65
|
+
}): import("react").JSX.Element;
|
|
66
|
+
export type ListIdentity = {
|
|
67
|
+
title?: string;
|
|
68
|
+
description?: string;
|
|
69
|
+
counters?: readonly {
|
|
70
|
+
label: string;
|
|
71
|
+
value: number | string;
|
|
72
|
+
}[];
|
|
73
|
+
};
|