@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,116 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { stickyTop, useScrollBox } from "../../foundations/sticky.js";
|
|
4
|
+
import { ArrowDown, ArrowUp, ChevronRight, ChevronsUpDown } from "lucide-react";
|
|
5
|
+
import { createContext, useContext, useId } from "react";
|
|
6
|
+
export function Table({ children,
|
|
7
|
+
/** With a height, the table scrolls on its own and the header sticks.
|
|
8
|
+
* Without a height, the page scrolls and the header goes away. */
|
|
9
|
+
height, }) {
|
|
10
|
+
const caja = useScrollBox(height === undefined ? {} : { height });
|
|
11
|
+
return (
|
|
12
|
+
/* Cuándo esta caja es un contenedor de scroll y cuándo no lo decide
|
|
13
|
+
`useScrollBox`: ahí vive el dilema entero y ahí vive su explicación. Las
|
|
14
|
+
tres tablas del sistema lo resolvían con tres copias de la misma
|
|
15
|
+
medición. */
|
|
16
|
+
_jsx("div", { ref: caja.ref, className: `min-w-0 ${caja.className}`, style: caja.style, children: _jsx("table", { className: "w-full border-separate border-spacing-0 px-1 text-left", children: children }) }));
|
|
17
|
+
}
|
|
18
|
+
export function THead({ children }) {
|
|
19
|
+
return (
|
|
20
|
+
/* Se pega DEBAJO de lo que ya esté pegado —la barra de la aplicación y la
|
|
21
|
+
de la lista— y no en `top: 0`, que lo metía atrás de las dos. Sin alto en
|
|
22
|
+
`Table` el que scrollea es la página, y ahí la diferencia es justamente
|
|
23
|
+
la que hacía desaparecer los nombres de columna. Ver `system/sticky.tsx`. */
|
|
24
|
+
_jsx("thead", { className: "sticky z-10 bg-ground", style: stickyTop, children: _jsx("tr", { children: children }) }));
|
|
25
|
+
}
|
|
26
|
+
/** A column. If it gets a `sortKey`, it can be sorted.
|
|
27
|
+
*
|
|
28
|
+
* The icon is ALWAYS there, dimmed when the column is not the sorted one: an
|
|
29
|
+
* indicator that only shows up once you sort does not say which columns are
|
|
30
|
+
* sortable, and then you have to try them one by one. */
|
|
31
|
+
export function Th({ children, sortKey, sort, onSort, numeric = false, align, width, }) {
|
|
32
|
+
const active = sortKey !== undefined && sort?.key === sortKey;
|
|
33
|
+
const dir = active ? sort.direction : undefined;
|
|
34
|
+
function nextSort() {
|
|
35
|
+
if (sortKey === undefined || onSort === undefined)
|
|
36
|
+
return;
|
|
37
|
+
// asc → desc → unsorted. The third click puts the table back the way it was.
|
|
38
|
+
if (!active)
|
|
39
|
+
onSort({ key: sortKey, direction: "asc" });
|
|
40
|
+
else if (dir === "asc")
|
|
41
|
+
onSort({ key: sortKey, direction: "desc" });
|
|
42
|
+
else
|
|
43
|
+
onSort(null);
|
|
44
|
+
}
|
|
45
|
+
const Icon = dir === "asc" ? ArrowUp : dir === "desc" ? ArrowDown : ChevronsUpDown;
|
|
46
|
+
return (_jsx("th", { scope: "col", style: width === undefined ? undefined : { width }, "aria-sort": active ? (dir === "asc" ? "ascending" : "descending") : "none", className: `border-b border-line-strong px-3 py-2 text-meta font-medium whitespace-nowrap text-muted ${(align ?? (numeric ? "right" : "left")) === "right" ? "text-right" : "text-left"}`, children: sortKey === undefined ? (children) : (_jsxs("button", { type: "button", onClick: nextSort, className: `inline-flex items-center gap-1 rounded-control transition-colors duration-(--duration-instant) hover:text-ink focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-primary ${(align ?? (numeric ? "right" : "left")) === "right" ? "flex-row-reverse" : ""} ${active ? "text-ink" : ""}`, children: [children, _jsx(Icon, { className: `size-(--size-icon-sm) shrink-0 ${active ? "text-primary" : "text-line-strong"}`, strokeWidth: 2, "aria-hidden": true })] })) }));
|
|
47
|
+
}
|
|
48
|
+
/** The wash of a row lives on the cells, not on the `<tr>`. A background on the
|
|
49
|
+
* row paints edge to edge; on the cells the ends can round and the table's
|
|
50
|
+
* padding keeps the mark off the card. */
|
|
51
|
+
const RowMark = createContext(false);
|
|
52
|
+
const ROW_WASH = "group-hover/row:bg-ink/4";
|
|
53
|
+
const ROW_ON = "bg-primary/8";
|
|
54
|
+
function rowWash(selected) {
|
|
55
|
+
return selected ? ROW_ON : ROW_WASH;
|
|
56
|
+
}
|
|
57
|
+
export function Tr({ children, onClick, selected = false, }) {
|
|
58
|
+
return (_jsx(RowMark.Provider, { value: selected, children: _jsx("tr", { onClick: onClick, className: `group/row ${onClick === undefined ? "" : "cursor-pointer"}`, children: children }) }));
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* A row that opens its own detail underneath.
|
|
62
|
+
*
|
|
63
|
+
* Context: `DataTable` expands GROUPS —TanStack takes care of that— and that is
|
|
64
|
+
* not enough for the most common case in this product: a column that opens up
|
|
65
|
+
* to show its distribution. That is not a group, it is the detail of ONE row,
|
|
66
|
+
* and every view was writing it by hand with its own `colSpan` and its own
|
|
67
|
+
* chevron.
|
|
68
|
+
*
|
|
69
|
+
* The trigger is the first cell and not a column of chevrons: a whole column
|
|
70
|
+
* for a 12 px control is width that a twelve-column table needs.
|
|
71
|
+
*/
|
|
72
|
+
export function TrDetail({ children, detail, open, onToggle, columns, label, selected = false, }) {
|
|
73
|
+
const id = useId();
|
|
74
|
+
return (_jsx(RowMark.Provider, { value: selected, children: _jsxs(_Fragment, { children: [_jsxs("tr", { onClick: onToggle, className: "group/row cursor-pointer", children: [_jsx("td", { className: `border-b border-line py-1.5 pr-0 pl-2.5 align-middle first:rounded-l-control ${rowWash(selected)}`, children: _jsx("button", { type: "button", onClick: (e) => {
|
|
75
|
+
e.stopPropagation();
|
|
76
|
+
onToggle();
|
|
77
|
+
}, "aria-expanded": open, "aria-controls": id, "aria-label": label, className: "inline-flex size-5 items-center justify-center rounded-pill text-muted transition-colors duration-(--duration-instant) hover:bg-ink/6 hover:text-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary", children: _jsx(ChevronRight, { className: `size-(--size-icon-sm) transition-transform duration-(--duration-instant) ${open ? "rotate-90" : ""}`, strokeWidth: 2 }) }) }), children] }), _jsx("tr", { id: id, hidden: !open, children: _jsx("td", { colSpan: columns + 1, className: "border-b border-line bg-ground px-3 py-3", children: detail }) })] }) }));
|
|
78
|
+
}
|
|
79
|
+
/** The background of a cell, by tone. It replaces the row wash on that cell:
|
|
80
|
+
* a hovered row and a painted cell are two signals, and stacking them reads
|
|
81
|
+
* as a third color. */
|
|
82
|
+
export const CELL_TONE = {
|
|
83
|
+
neutral: "bg-ink/6",
|
|
84
|
+
info: "bg-info/12",
|
|
85
|
+
good: "bg-good/12",
|
|
86
|
+
warn: "bg-warn/15",
|
|
87
|
+
bad: "bg-bad/12",
|
|
88
|
+
accent: "bg-accent/20",
|
|
89
|
+
};
|
|
90
|
+
export function Td({ children, numeric = false, align,
|
|
91
|
+
/** A CODE — an id, a hash, a key — set in the monospaced face. Off by default: a
|
|
92
|
+
* number does not need it (a numeric cell gets tabular figures on its own), and a
|
|
93
|
+
* name in mono reads like a log, not like a report. */
|
|
94
|
+
mono = false,
|
|
95
|
+
/** Paints the whole cell. For what the eye has to find without reading: an
|
|
96
|
+
* imputed value, one out of range, a row that changed.
|
|
97
|
+
*
|
|
98
|
+
* Context: it is the CELL and not the text. Tinting the text says «this
|
|
99
|
+
* number is of another kind»; tinting the cell says «look here», which is
|
|
100
|
+
* something else — and in a table of forty rows by six columns, colored text
|
|
101
|
+
* gets lost. */
|
|
102
|
+
tone, title, }) {
|
|
103
|
+
const selected = useContext(RowMark);
|
|
104
|
+
return (_jsx("td", { title: title, className: `border-b border-line px-3 py-1.5 text-meta text-ink first:rounded-l-control last:rounded-r-control ${(align ?? (numeric ? "right" : "left")) === "right" ? "text-right" : "text-left"} ${mono ? "font-mono tabular-nums" : numeric ? "tabular-nums" : ""} ${tone === undefined ? rowWash(selected) : CELL_TONE[tone]}`, children: children }));
|
|
105
|
+
}
|
|
106
|
+
/** The selection checkbox, with its third state.
|
|
107
|
+
*
|
|
108
|
+
* «some» is not an ornament: without it, the header checkbox of a table with
|
|
109
|
+
* three of forty rows selected lies in both directions — empty says «none»
|
|
110
|
+
* and filled says «all». */
|
|
111
|
+
export function SelectBox({ checked, indeterminate = false, onChange, label, }) {
|
|
112
|
+
return (_jsx("input", { type: "checkbox", checked: checked, "aria-label": label, ref: (el) => {
|
|
113
|
+
if (el !== null)
|
|
114
|
+
el.indeterminate = indeterminate && !checked;
|
|
115
|
+
}, onChange: (e) => onChange?.(e.target.checked), onClick: (e) => e.stopPropagation(), className: "size-3.5 shrink-0 cursor-pointer accent-primary" }));
|
|
116
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { type Aggregate, type DataColumn, DataTable, type DataTableCreate, type GroupColumn, } from "./DataTable.js";
|
|
2
|
+
export { EditableTable, type EditAdd, type EditBands, type EditColumn, type EditLayout, sectionLines, type StackPlace, stackPlaces, type TabTarget, tabTarget, } from "./EditableTable.js";
|
|
3
|
+
export { PivotTable, type PivotTableProps, } from "./PivotTable.js";
|
|
4
|
+
export { type ReportColumn, type ReportRow, type ReportRowKind, ReportTable, } from "./ReportTable.js";
|
|
5
|
+
export { type RowAction, RowActions } from "./RowActions.js";
|
|
6
|
+
export { type SheetColumn, SheetTable } from "./SheetTable.js";
|
|
7
|
+
export { CELL_TONE, type SortDirection, type SortState, Table, Td, Th, THead, Tr, } from "./Table.js";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/* The presentation (`Table`) is ours and is used on its own; `DataTable` is the
|
|
2
|
+
wiring to TanStack for grouping and sorting. See the note in `DataTable.tsx`. */
|
|
3
|
+
export { DataTable, } from "./DataTable.js";
|
|
4
|
+
export { EditableTable, sectionLines, stackPlaces, tabTarget, } from "./EditableTable.js";
|
|
5
|
+
export { PivotTable, } from "./PivotTable.js";
|
|
6
|
+
export { ReportTable, } from "./ReportTable.js";
|
|
7
|
+
export { RowActions } from "./RowActions.js";
|
|
8
|
+
export { SheetTable } from "./SheetTable.js";
|
|
9
|
+
export { CELL_TONE, Table, Td, Th, THead, Tr, } from "./Table.js";
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { Tone } from "../../foundations/types.js";
|
|
2
|
+
import type { LucideIcon } from "lucide-react";
|
|
3
|
+
/**
|
|
4
|
+
* UNA TIRA DE PESTAÑAS. La misma en todo el sistema.
|
|
5
|
+
*
|
|
6
|
+
* Context: esto vivía adentro del encabezado del registro y se llamaba
|
|
7
|
+
* `Aspects`, así que la actividad —que necesita exactamente lo mismo— no podía
|
|
8
|
+
* usarlo: un contexto no importa de otro contexto. El resultado era una tira de
|
|
9
|
+
* pestañas de segunda mano, con el activo marcado de otra manera, en la misma
|
|
10
|
+
* pantalla que la buena.
|
|
11
|
+
*
|
|
12
|
+
* Es una primitiva porque no sabe qué está separando: las caras de un registro,
|
|
13
|
+
* las dos fuentes de una historia, o lo que venga.
|
|
14
|
+
*
|
|
15
|
+
* ── Por qué el activo es una PESTAÑA de carpeta ─────────────────────
|
|
16
|
+
*
|
|
17
|
+
* Y no un subrayado. Un subrayado dice «esto está elegido»; una pestaña que sale
|
|
18
|
+
* del borde y tapa la línea dice «esto está ABIERTO y lo de abajo es su
|
|
19
|
+
* contenido», que es la relación real. La diferencia importa cuando hay tres y
|
|
20
|
+
* el contenido de abajo no cambia de forma al cambiar de una a otra.
|
|
21
|
+
*
|
|
22
|
+
* ── El contador apagado en cero ─────────────────────────────────────
|
|
23
|
+
*
|
|
24
|
+
* Un cero se dibuja, y apagado. Esconderlo obliga a abrir la pestaña para
|
|
25
|
+
* descubrir que está vacía; pintarlo como los demás la hace parecer que tiene
|
|
26
|
+
* algo. «No hay ninguna» es información y se dice.
|
|
27
|
+
*
|
|
28
|
+
* ── El contador y la NOTIFICACIÓN son dos cosas ─────────────────────
|
|
29
|
+
*
|
|
30
|
+
* El contador dice CUÁNTAS cosas hay detrás, y es neutro: «pagos 0» es una
|
|
31
|
+
* respuesta tan válida como «pagos 5». La notificación dice que algo de ahí
|
|
32
|
+
* PIDE que lo mires —falló, cambió, llegó— y por eso tiene color y por eso un
|
|
33
|
+
* cero no se dibuja: no hay nada que pedir.
|
|
34
|
+
*
|
|
35
|
+
* Confundirlos se ve así: una pestaña con «3» al lado y nadie sabe si son tres
|
|
36
|
+
* cosas o tres problemas. Son dos marcas porque son dos preguntas.
|
|
37
|
+
*/
|
|
38
|
+
/**
|
|
39
|
+
* ALGO DE ESTA PESTAÑA PIDE ATENCIÓN.
|
|
40
|
+
*
|
|
41
|
+
* No es un conteo: un conteo dice cuántas cosas hay y esto dice que hay algo que
|
|
42
|
+
* mirar. Por eso tiene color, por eso no se dibuja en cero, y por eso puede
|
|
43
|
+
* convivir con el contador — «pagos 4 ⬤» es cuatro cosas, una de ellas
|
|
44
|
+
* rechazada.
|
|
45
|
+
*/
|
|
46
|
+
export type TabNotice = {
|
|
47
|
+
/**
|
|
48
|
+
* Qué es lo que pide atención, en la voz del producto: «3 sin leer», «la
|
|
49
|
+
* factura no se pudo enviar».
|
|
50
|
+
*
|
|
51
|
+
* Obligatorio, y por la misma razón que en `StatusDot`: un punto de color sin
|
|
52
|
+
* su palabra al lado obliga a aprender una leyenda que no está escrita en
|
|
53
|
+
* ningún lado, y no le dice nada a quien no distingue el rojo del verde. Acá
|
|
54
|
+
* la marca acelera el barrido y la palabra informa — se lee al pasar por
|
|
55
|
+
* encima y la lee un lector de pantalla.
|
|
56
|
+
*/
|
|
57
|
+
label: string;
|
|
58
|
+
/** Cuántas cosas lo piden. Sin esto es un punto, que es lo que se pone cuando
|
|
59
|
+
* llegó algo nuevo y contarlo no agrega nada. Un `0` no dibuja nada: no hay
|
|
60
|
+
* nada que pedir. */
|
|
61
|
+
count?: number;
|
|
62
|
+
/** De qué clase: `bad` algo falló, `warn` algo avisa, `accent` o `info` algo
|
|
63
|
+
* llegó. Sin esto, `bad` — que es el caso por el que esto existe. */
|
|
64
|
+
tone?: Tone;
|
|
65
|
+
};
|
|
66
|
+
export type TabItem = {
|
|
67
|
+
id: string;
|
|
68
|
+
label: string;
|
|
69
|
+
/** La marca de la pestaña. Con tres o más, el ícono es lo que se reconoce sin
|
|
70
|
+
* leer — y en una tira angosta es lo único que sobrevive al truncado. */
|
|
71
|
+
icon?: LucideIcon;
|
|
72
|
+
/** Cuántas cosas hay detrás. `undefined` = no se cuenta; `0` se dibuja
|
|
73
|
+
* apagado, que es distinto de no contar. */
|
|
74
|
+
count?: number;
|
|
75
|
+
/** Que algo de acá pide que lo mires. Aparte del contador: ver `TabNotice`. */
|
|
76
|
+
notice?: TabNotice;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* The strip only: what each tab opens is drawn by whoever uses it (`Record`, `Activity`).
|
|
80
|
+
*
|
|
81
|
+
* Built on React Aria's `Tabs`, so the strip is a real tab list for the keyboard: the
|
|
82
|
+
* arrows move between tabs and select them, Home/End jump to the ends, and only the open
|
|
83
|
+
* tab is in the Tab order. The hand-written version had the roles and none of the keys.
|
|
84
|
+
*/
|
|
85
|
+
export declare function Tabs({ tabs, current, onSelect, label, }: {
|
|
86
|
+
tabs: readonly TabItem[];
|
|
87
|
+
current: string;
|
|
88
|
+
onSelect?: (id: string) => void;
|
|
89
|
+
/** El rótulo accesible de la tira. Sin esto, «las caras del registro». */
|
|
90
|
+
label?: string;
|
|
91
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useUiCopy } from "../../foundations/copy.js";
|
|
4
|
+
import { Tab, TabList, Tabs as AriaTabs } from "react-aria-components";
|
|
5
|
+
/* El color de la notificación. Estático y en una tabla porque Tailwind no puede
|
|
6
|
+
componer una clase con un pedazo variable: `bg-${tone}/12` no existe en la
|
|
7
|
+
hoja. */
|
|
8
|
+
const NOTICE_TONE = {
|
|
9
|
+
neutral: "bg-ink/8 text-muted",
|
|
10
|
+
info: "bg-info/12 text-info",
|
|
11
|
+
good: "bg-good/12 text-good",
|
|
12
|
+
warn: "bg-warn/14 text-warn",
|
|
13
|
+
bad: "bg-bad/12 text-bad",
|
|
14
|
+
accent: "bg-accent/14 text-accent",
|
|
15
|
+
};
|
|
16
|
+
const NOTICE_DOT = {
|
|
17
|
+
neutral: "bg-muted",
|
|
18
|
+
info: "bg-info",
|
|
19
|
+
good: "bg-good",
|
|
20
|
+
warn: "bg-warn",
|
|
21
|
+
bad: "bg-bad",
|
|
22
|
+
accent: "bg-accent",
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* The strip only: what each tab opens is drawn by whoever uses it (`Record`, `Activity`).
|
|
26
|
+
*
|
|
27
|
+
* Built on React Aria's `Tabs`, so the strip is a real tab list for the keyboard: the
|
|
28
|
+
* arrows move between tabs and select them, Home/End jump to the ends, and only the open
|
|
29
|
+
* tab is in the Tab order. The hand-written version had the roles and none of the keys.
|
|
30
|
+
*/
|
|
31
|
+
export function Tabs({ tabs, current, onSelect, label, }) {
|
|
32
|
+
const copy = useUiCopy();
|
|
33
|
+
return (_jsx(AriaTabs, { selectedKey: current, onSelectionChange: (key) => onSelect?.(String(key)), children: _jsx(TabList, { "aria-label": label ?? copy.record.faces, className: "flex min-w-0 flex-wrap items-end gap-1", children: tabs.map((a) => {
|
|
34
|
+
const Icon = a.icon;
|
|
35
|
+
const empty = a.count === 0;
|
|
36
|
+
/* Un cero no pide nada, así que no se dibuja. Es la diferencia con el
|
|
37
|
+
contador, donde «0» es una respuesta. */
|
|
38
|
+
const notice = a.notice?.count === 0 ? undefined : a.notice;
|
|
39
|
+
return (_jsx(Tab, { id: a.id, ...(notice === undefined
|
|
40
|
+
? {}
|
|
41
|
+
: { "aria-label": `${a.label} · ${notice.label}` }), className: ({ isSelected }) => `inline-flex shrink-0 cursor-default items-center px-3 py-2 text-meta outline-0 transition-colors duration-(--duration-instant) focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-primary ${isSelected
|
|
42
|
+
? "-mb-px rounded-t-control border border-b-0 border-line-strong bg-surface font-medium text-ink"
|
|
43
|
+
: "border border-transparent text-muted hover:text-ink"}`, children: ({ isSelected }) => (
|
|
44
|
+
/* The `title` goes on the content and not on the tab: React Aria does not
|
|
45
|
+
forward it, and on the content the native tooltip still covers the
|
|
46
|
+
whole tab. */
|
|
47
|
+
_jsxs("span", { className: "inline-flex items-center gap-1.5", ...(notice === undefined ? {} : { title: notice.label }), children: [Icon === undefined ? null : (_jsx(Icon, { className: `size-(--size-icon-sm) shrink-0 ${isSelected ? "text-ink" : "text-muted"}`, strokeWidth: 2, "aria-hidden": true })), a.label, a.count === undefined ? null : (_jsx("span", { className: `rounded-pill px-1.5 text-micro tabular-nums ${empty
|
|
48
|
+
? "bg-ink/5 text-muted/70"
|
|
49
|
+
: isSelected
|
|
50
|
+
? "bg-ink/8 text-ink"
|
|
51
|
+
: "bg-ink/8 text-muted"}`, children: a.count.toLocaleString(copy.locale) })), notice === undefined ? null : notice.count === undefined ? (
|
|
52
|
+
/* Sin número, un punto. La palabra no se dibuja pero está: en el
|
|
53
|
+
`title` y en el nombre accesible de la pestaña. */
|
|
54
|
+
_jsx("span", { className: `size-1.5 shrink-0 rounded-pill ${NOTICE_DOT[notice.tone ?? "bad"]}`, "aria-hidden": true })) : (_jsx("span", { className: `rounded-pill px-1.5 text-micro font-medium tabular-nums ${NOTICE_TONE[notice.tone ?? "bad"]}`, "aria-hidden": true, children: notice.count.toLocaleString(copy.locale) }))] })) }, a.id));
|
|
55
|
+
}) }) }));
|
|
56
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { type TabItem, type TabNotice, Tabs } from "./Tabs.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Tabs } from "./Tabs.js";
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Tone } from "../../foundations/types.js";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
export type { Tone };
|
|
4
|
+
/** The classes of a tone on a surface — the capsule's colors, shared with the chips of
|
|
5
|
+
* `Tags` so a classification reads the same as a value and as a removable chip. */
|
|
6
|
+
export declare function tagTone(tone: Tone, surface: "canvas" | "chrome"): string;
|
|
7
|
+
export declare function Tag({ children, tone, surface,
|
|
8
|
+
/** Context: a one-word classification sometimes needs a sentence that does not
|
|
9
|
+
* fit. The sentence goes into the `title` and not underneath, because the
|
|
10
|
+
* line below is taken up by the row's metadata. */
|
|
11
|
+
title, }: {
|
|
12
|
+
children: ReactNode;
|
|
13
|
+
tone?: Tone;
|
|
14
|
+
surface?: "canvas" | "chrome";
|
|
15
|
+
title?: string;
|
|
16
|
+
}): import("react").JSX.Element;
|
|
17
|
+
/** The status dot.
|
|
18
|
+
*
|
|
19
|
+
* **It never goes alone.** A colored dot without its word next to it forces
|
|
20
|
+
* you to learn a legend that is written down nowhere, and it says nothing to
|
|
21
|
+
* someone who cannot tell red from green. The dot speeds up the scanning of a
|
|
22
|
+
* column; the word is what informs. That is why `label` is mandatory and gets
|
|
23
|
+
* drawn — if it needs to be hidden, it is hidden with `aria` and not by
|
|
24
|
+
* deleting it.
|
|
25
|
+
*
|
|
26
|
+
* `pulse` for what is ALIVE: a bank statement still importing. It is the only animation in
|
|
27
|
+
* the system, and it exists because «importando» and «importado» differ by one
|
|
28
|
+
* letter and that is far too little for a state that changes on its own. */
|
|
29
|
+
export declare function StatusDot({ tone, label, pulse, surface, }: {
|
|
30
|
+
tone?: Tone;
|
|
31
|
+
/** ReactNode and not `string`: the label is visible text, and sometimes it
|
|
32
|
+
* carries a figure or a name already formatted by whoever composes. */
|
|
33
|
+
label: ReactNode;
|
|
34
|
+
pulse?: boolean;
|
|
35
|
+
surface?: "canvas" | "chrome";
|
|
36
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/** Each tone on its two surfaces. The state does not change meaning when
|
|
3
|
+
* crossing from the canvas to the chrome, but its value does: `good` #2F6B3C
|
|
4
|
+
* gives 1.7:1 on #1C1A17 and the lightened one has to be used. */
|
|
5
|
+
const TAG_TONE = {
|
|
6
|
+
neutral: {
|
|
7
|
+
canvas: "border border-line-strong bg-ink/5 text-ink",
|
|
8
|
+
chrome: "border border-chrome-muted/30 bg-chrome-ink/10 text-chrome-ink",
|
|
9
|
+
},
|
|
10
|
+
info: {
|
|
11
|
+
canvas: "border border-info/25 bg-info/10 text-info",
|
|
12
|
+
chrome: "border border-chrome-link/30 bg-chrome-link/15 text-chrome-link",
|
|
13
|
+
},
|
|
14
|
+
good: {
|
|
15
|
+
canvas: "border border-good/25 bg-good/10 text-good",
|
|
16
|
+
chrome: "border border-chrome-good/30 bg-chrome-good/15 text-chrome-good",
|
|
17
|
+
},
|
|
18
|
+
warn: {
|
|
19
|
+
canvas: "border border-warn/30 bg-warn/10 text-warn",
|
|
20
|
+
chrome: "border border-chrome-warn/35 bg-chrome-warn/15 text-chrome-warn",
|
|
21
|
+
},
|
|
22
|
+
bad: {
|
|
23
|
+
canvas: "border border-bad/25 bg-bad/10 text-bad",
|
|
24
|
+
chrome: "border border-chrome-bad/35 bg-chrome-bad/15 text-chrome-bad",
|
|
25
|
+
},
|
|
26
|
+
accent: {
|
|
27
|
+
// The gold as TEXT only exists on the chrome (7.39:1). On the canvas it
|
|
28
|
+
// gives 2.43:1, so there this tag falls back to ink on a gold wash: the
|
|
29
|
+
// brand is visible and the word is readable.
|
|
30
|
+
canvas: "border border-accent/40 bg-accent/18 text-ink",
|
|
31
|
+
chrome: "border border-chrome-accent/40 bg-chrome-accent/15 text-chrome-accent",
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
/** The classes of a tone on a surface — the capsule's colors, shared with the chips of
|
|
35
|
+
* `Tags` so a classification reads the same as a value and as a removable chip. */
|
|
36
|
+
export function tagTone(tone, surface) {
|
|
37
|
+
return TAG_TONE[tone][surface];
|
|
38
|
+
}
|
|
39
|
+
export function Tag({ children, tone = "neutral", surface = "canvas",
|
|
40
|
+
/** Context: a one-word classification sometimes needs a sentence that does not
|
|
41
|
+
* fit. The sentence goes into the `title` and not underneath, because the
|
|
42
|
+
* line below is taken up by the row's metadata. */
|
|
43
|
+
title, }) {
|
|
44
|
+
return (_jsx("span", { title: title, className: `inline-flex shrink-0 items-center rounded-pill px-2 py-0.5 text-micro font-medium whitespace-nowrap ${TAG_TONE[tone][surface]}`, children: children }));
|
|
45
|
+
}
|
|
46
|
+
const DOT_TONE = {
|
|
47
|
+
neutral: { canvas: "bg-muted", chrome: "bg-chrome-muted" },
|
|
48
|
+
info: { canvas: "bg-info", chrome: "bg-chrome-link" },
|
|
49
|
+
good: { canvas: "bg-good", chrome: "bg-chrome-good" },
|
|
50
|
+
warn: { canvas: "bg-warn", chrome: "bg-chrome-warn" },
|
|
51
|
+
bad: { canvas: "bg-bad", chrome: "bg-chrome-bad" },
|
|
52
|
+
accent: { canvas: "bg-accent", chrome: "bg-chrome-accent" },
|
|
53
|
+
};
|
|
54
|
+
/** The status dot.
|
|
55
|
+
*
|
|
56
|
+
* **It never goes alone.** A colored dot without its word next to it forces
|
|
57
|
+
* you to learn a legend that is written down nowhere, and it says nothing to
|
|
58
|
+
* someone who cannot tell red from green. The dot speeds up the scanning of a
|
|
59
|
+
* column; the word is what informs. That is why `label` is mandatory and gets
|
|
60
|
+
* drawn — if it needs to be hidden, it is hidden with `aria` and not by
|
|
61
|
+
* deleting it.
|
|
62
|
+
*
|
|
63
|
+
* `pulse` for what is ALIVE: a bank statement still importing. It is the only animation in
|
|
64
|
+
* the system, and it exists because «importando» and «importado» differ by one
|
|
65
|
+
* letter and that is far too little for a state that changes on its own. */
|
|
66
|
+
export function StatusDot({ tone = "neutral", label, pulse = false, surface = "canvas", }) {
|
|
67
|
+
return (_jsxs("span", { className: "inline-flex shrink-0 items-center gap-1.5 whitespace-nowrap", children: [_jsxs("span", { className: "relative flex size-1.5 shrink-0", children: [pulse ? (_jsx("span", { className: `absolute inline-flex size-full animate-ping rounded-pill opacity-70 ${DOT_TONE[tone][surface]}` })) : null, _jsx("span", { className: `relative inline-flex size-1.5 rounded-pill ${DOT_TONE[tone][surface]}` })] }), _jsx("span", { className: `text-micro ${surface === "chrome" ? "text-chrome-muted" : "text-muted"}`, children: label })] }));
|
|
68
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Surface, Tone } from "../../foundations/types.js";
|
|
2
|
+
import type { LucideIcon } from "lucide-react";
|
|
3
|
+
export type TagItem = {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
/** What clarifies it without having to read it whole: which table a field
|
|
7
|
+
* comes from, which symbol a comparison is. It goes muted and inside. */
|
|
8
|
+
note?: string;
|
|
9
|
+
icon?: LucideIcon;
|
|
10
|
+
/** Choose it. Without this the chip is a value and is not pressed. */
|
|
11
|
+
onSelect?: () => void;
|
|
12
|
+
/** Remove it, when removing it means something. It is a separate button
|
|
13
|
+
* inside the chip — and Backspace/Delete on the focused chip — so removing
|
|
14
|
+
* is not the same as choosing. */
|
|
15
|
+
onRemove?: () => void;
|
|
16
|
+
/** The accessible name of the × . React Aria already names it «Remove» +
|
|
17
|
+
* the chip; this replaces the verb when the chip needs a more exact one
|
|
18
|
+
* («quitar el filtro»). */
|
|
19
|
+
removeLabel?: string;
|
|
20
|
+
/** It is chosen right now. */
|
|
21
|
+
selected?: boolean;
|
|
22
|
+
/** It is MARKED by the keyboard of someone else — the search input that
|
|
23
|
+
* drives a list of suggestions — which is different from chosen: marked is
|
|
24
|
+
* «Enter takes this one», chosen is «this one is already set». */
|
|
25
|
+
active?: boolean;
|
|
26
|
+
/** What the chip MEANS, the same tones as `Tag`: a customer category «crédito vencido»
|
|
27
|
+
* in `bad`. Neutral by default — most tags classify, they do not warn. Chosen and
|
|
28
|
+
* marked chips keep their own paint: that state is what the strip is saying then. */
|
|
29
|
+
tone?: Tone;
|
|
30
|
+
title?: string;
|
|
31
|
+
};
|
|
32
|
+
export declare function Tags({ items, surface, label, labelledBy, stack, multiple, }: {
|
|
33
|
+
items: readonly TagItem[];
|
|
34
|
+
/** By default it inherits the surface that wraps it. */
|
|
35
|
+
surface?: Surface;
|
|
36
|
+
/** The accessible name of the strip. A strip of values is read as text and
|
|
37
|
+
* does not need one; a strip that can be pressed or removed is a control,
|
|
38
|
+
* and a control without a name is announced as «grid» and nothing else. */
|
|
39
|
+
label?: string;
|
|
40
|
+
/** The id of a visible label that names the strip — the label of the `Field` it lives
|
|
41
|
+
* in. It wins over `label`. */
|
|
42
|
+
labelledBy?: string;
|
|
43
|
+
/**
|
|
44
|
+
* One below the other, and not on a line that wraps.
|
|
45
|
+
*
|
|
46
|
+
* The difference is not aesthetic: a strip ACCOMPANIES —they are the parts of
|
|
47
|
+
* something being written, and are read together— and a list IS CHOSEN FROM,
|
|
48
|
+
* and there the eye goes down a column and so does the keyboard. Twelve
|
|
49
|
+
* attributes wrapped in three lines are twelve things to sweep in a zigzag to
|
|
50
|
+
* find one.
|
|
51
|
+
*/
|
|
52
|
+
stack?: boolean;
|
|
53
|
+
/** When chips carry `selected`, more than one can be chosen at a time. By
|
|
54
|
+
* default the choice is ONE — a join, a grain, a period — and the strip is
|
|
55
|
+
* announced as single-select. */
|
|
56
|
+
multiple?: boolean;
|
|
57
|
+
}): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { AriaLocale } from "../../foundations/aria.js";
|
|
4
|
+
import { bySurface, useSurface } from "../../foundations/surface.js";
|
|
5
|
+
import { cn, focusRing } from "../../foundations/variants.js";
|
|
6
|
+
import { tagTone } from "./Tag.js";
|
|
7
|
+
import { X } from "lucide-react";
|
|
8
|
+
import { Button as AriaButton, Tag as AriaTag, TagGroup, TagList, } from "react-aria-components";
|
|
9
|
+
export function Tags({ items, surface, label, labelledBy, stack = false, multiple = false, }) {
|
|
10
|
+
const sup = useSurface(surface);
|
|
11
|
+
if (items.length === 0)
|
|
12
|
+
return null;
|
|
13
|
+
const interactive = items.some((i) => i.onSelect !== undefined || i.onRemove !== undefined);
|
|
14
|
+
if (!interactive)
|
|
15
|
+
return _jsx(StillStrip, { items: items, surface: sup, label: label, stack: stack });
|
|
16
|
+
return (_jsx(AriaLocale, { children: _jsx(ChipGroup, { items: items, surface: sup, label: label, labelledBy: labelledBy, stack: stack, multiple: multiple }) }));
|
|
17
|
+
}
|
|
18
|
+
const STRIP = "flex min-w-0 flex-wrap items-center gap-1";
|
|
19
|
+
const STACK = "flex min-w-0 flex-col items-stretch gap-0.5";
|
|
20
|
+
/** A strip nobody presses: text in a row, a list only when it is named. */
|
|
21
|
+
function StillStrip({ items, surface, label, stack, }) {
|
|
22
|
+
const listed = label !== undefined;
|
|
23
|
+
return (_jsx("span", { className: stack ? STACK : STRIP, ...(listed ? { role: "list", "aria-label": label } : {}), children: items.map((item) => (_jsx("span", { ...(listed ? { role: "listitem" } : {}), ...(item.title === undefined ? {} : { title: item.title }), className: shell({ item, surface, stack, removable: false, pressable: false }), children: _jsx(Inside, { item: item }) }, item.id))) }));
|
|
24
|
+
}
|
|
25
|
+
function ChipGroup({ items, surface, label, labelledBy, stack, multiple, }) {
|
|
26
|
+
const byId = new Map(items.map((item) => [item.id, item]));
|
|
27
|
+
const removable = items.some((i) => i.onRemove !== undefined);
|
|
28
|
+
/* Selection is React Aria's only when the chips SAY whether they are chosen: then the
|
|
29
|
+
strip is announced as a selectable list and each chip as selected or not. Chips that
|
|
30
|
+
only act (`onSelect` without `selected`) are actions, and pressing one does not leave
|
|
31
|
+
anything «selected» behind. */
|
|
32
|
+
const selecting = items.some((i) => i.selected !== undefined && i.onSelect !== undefined);
|
|
33
|
+
const chosen = new Set(items.filter((i) => i.selected === true).map((i) => i.id));
|
|
34
|
+
/* The choice is the consumer's: the strip reports which chip was pressed and waits for the
|
|
35
|
+
next `selected`. In a single choice the chip that ARRIVES is the one pressed; pressing
|
|
36
|
+
the chosen one again reports that one, the way a radio reports the click on itself. */
|
|
37
|
+
const onSelectionChange = (next) => {
|
|
38
|
+
if (next === "all")
|
|
39
|
+
return;
|
|
40
|
+
const keys = new Set([...next].map(String));
|
|
41
|
+
const added = [...keys].filter((k) => !chosen.has(k));
|
|
42
|
+
const dropped = [...chosen].filter((k) => !keys.has(k));
|
|
43
|
+
const pressed = multiple ? [...added, ...dropped] : added.length > 0 ? added : dropped;
|
|
44
|
+
for (const key of pressed)
|
|
45
|
+
byId.get(key)?.onSelect?.();
|
|
46
|
+
};
|
|
47
|
+
/* Backspace on a chip removes it — and only the chips that can be removed: a chip without
|
|
48
|
+
`onRemove` in a strip that has some is still a value. */
|
|
49
|
+
const onRemove = (keys) => {
|
|
50
|
+
for (const key of keys)
|
|
51
|
+
byId.get(String(key))?.onRemove?.();
|
|
52
|
+
};
|
|
53
|
+
return (_jsx(TagGroup
|
|
54
|
+
/* The name falls back to the chips themselves: «cliente: Andina, estado: vencida» says
|
|
55
|
+
what the strip holds, which is better than an anonymous grid. */
|
|
56
|
+
, { ...(labelledBy === undefined
|
|
57
|
+
? { "aria-label": label ?? items.map((i) => i.label).join(", ") }
|
|
58
|
+
: { "aria-labelledby": labelledBy }), ...(removable ? { onRemove } : {}), ...(selecting
|
|
59
|
+
? {
|
|
60
|
+
selectionMode: multiple ? "multiple" : "single",
|
|
61
|
+
selectedKeys: chosen,
|
|
62
|
+
onSelectionChange,
|
|
63
|
+
}
|
|
64
|
+
: {}),
|
|
65
|
+
/* Escape belongs to whatever holds the strip — the popover it lives in closes with it —
|
|
66
|
+
and not to «clear the selection», which would erase the choice without a word. */
|
|
67
|
+
escapeKeyBehavior: "none", className: "min-w-0", children: _jsx(TagList, { items: items, className: stack ? STACK : STRIP, children: (item) => (_jsxs(AriaTag, { id: item.id, textValue: item.note === undefined ? item.label : `${item.label} ${item.note}`, ...(!selecting && item.onSelect !== undefined
|
|
68
|
+
? { onAction: item.onSelect }
|
|
69
|
+
: {}), ...(item.title === undefined ? {} : { title: item.title }), className: ({ isFocusVisible, isHovered }) => cn(shell({
|
|
70
|
+
item,
|
|
71
|
+
surface,
|
|
72
|
+
stack,
|
|
73
|
+
removable: item.onRemove !== undefined,
|
|
74
|
+
pressable: item.onSelect !== undefined,
|
|
75
|
+
}), item.onSelect !== undefined && isHovered && item.active !== true
|
|
76
|
+
? bySurface(surface, "border-ink/40", "border-chrome-ink/40")
|
|
77
|
+
: "", "outline-offset-1", focusRing({ isFocusVisible })), children: [_jsx(Inside, { item: item }), item.onRemove === undefined ? null : _jsx(Remove, { item: item, surface: surface })] })) }) }));
|
|
78
|
+
}
|
|
79
|
+
function paint(item, surface) {
|
|
80
|
+
if (item.active === true) {
|
|
81
|
+
return bySurface(surface, "border border-primary bg-primary text-surface", "border border-chrome-link bg-chrome-link text-chrome");
|
|
82
|
+
}
|
|
83
|
+
if (item.selected === true) {
|
|
84
|
+
return bySurface(surface, "border border-accent/40 bg-accent/18 text-ink", "border border-chrome-link/40 bg-chrome-link/25 text-chrome-ink");
|
|
85
|
+
}
|
|
86
|
+
if (item.tone !== undefined && item.tone !== "neutral")
|
|
87
|
+
return tagTone(item.tone, surface);
|
|
88
|
+
return bySurface(surface, "border border-line-strong bg-ink/5 text-ink", "border border-chrome-muted/30 bg-chrome-ink/12 text-chrome-ink");
|
|
89
|
+
}
|
|
90
|
+
/* Stacked, each chip takes its whole line: a column of chips of different widths does not
|
|
91
|
+
read as a list, it reads as a mess — and the blank on the right is what lets it be
|
|
92
|
+
pressed without aiming. */
|
|
93
|
+
function shell({ item, surface, stack, removable, pressable, }) {
|
|
94
|
+
return cn(stack ? "flex w-full justify-start" : "inline-flex shrink-0", "min-w-0 items-center gap-1 rounded-pill py-0.5 text-micro transition-colors duration-(--duration-instant)", paint(item, surface), removable ? "pr-0.5 pl-2" : "px-2", pressable ? "cursor-pointer" : "cursor-default");
|
|
95
|
+
}
|
|
96
|
+
function Inside({ item }) {
|
|
97
|
+
const Icon = item.icon;
|
|
98
|
+
return (_jsxs(_Fragment, { children: [Icon === undefined ? null : (_jsx(Icon, { className: "size-3 shrink-0 opacity-70", strokeWidth: 2, "aria-hidden": true })), _jsx("span", { className: "min-w-0 truncate", children: item.label }), item.note === undefined ? null : (_jsxs("span", { className: "shrink-0 opacity-60", children: ["\u00B7 ", item.note] }))] }));
|
|
99
|
+
}
|
|
100
|
+
/* The × is React Aria's `remove` slot: it removes THIS chip and never presses it, so
|
|
101
|
+
removing a pressable chip does not also choose it. */
|
|
102
|
+
function Remove({ item, surface }) {
|
|
103
|
+
return (_jsx(AriaButton, { slot: "remove", ...(item.removeLabel === undefined ? {} : { "aria-label": item.removeLabel }), className: ({ isFocusVisible }) => cn("inline-flex size-4 shrink-0 cursor-default items-center justify-center rounded-pill transition-colors duration-(--duration-instant)", bySurface(surface, "hover:bg-ink/15 pressed:bg-ink/20", "hover:bg-chrome-ink/25 pressed:bg-chrome-ink/30"), focusRing({ isFocusVisible })), children: _jsx(X, { className: "size-3", strokeWidth: 2.5, "aria-hidden": true }) }));
|
|
104
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type FormMode } from "../field/mode.js";
|
|
2
|
+
import { type TagOption } from "./choices.js";
|
|
3
|
+
export type TagsFieldProps = {
|
|
4
|
+
/** The ids of the chosen tags, in the order they were added. */
|
|
5
|
+
value: readonly string[];
|
|
6
|
+
onChange?: (next: string[]) => void;
|
|
7
|
+
/** Every tag that can be put on the record — chosen ones included, for their labels. */
|
|
8
|
+
options: readonly TagOption[];
|
|
9
|
+
/**
|
|
10
|
+
* Create a tag from the typed text and return its new id (or a promise of it); the field
|
|
11
|
+
* then adds it. Without it there is no «create» row — the catalog is closed.
|
|
12
|
+
*
|
|
13
|
+
* Context: the id is returned and not invented here because the record is created by the
|
|
14
|
+
* backend (Odoo's `name_create`); the consumer also appends the new option to `options`.
|
|
15
|
+
*/
|
|
16
|
+
onCreate?: (label: string) => string | Promise<string>;
|
|
17
|
+
/** The input's placeholder while no tag is chosen. Defaults to the copy table's «Add…». */
|
|
18
|
+
placeholder?: string;
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
id?: string;
|
|
21
|
+
describedBy?: string;
|
|
22
|
+
invalid?: boolean;
|
|
23
|
+
/** The name of the control when it is NOT inside a `Field` — a cell of an editable table.
|
|
24
|
+
* Inside a field, the field's label names it. */
|
|
25
|
+
"aria-label"?: string;
|
|
26
|
+
/** Forces the mode. By default it inherits the form's: in read mode it draws `Tags`. */
|
|
27
|
+
mode?: FormMode;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* A many2many as removable chips plus an inline combobox that adds one more.
|
|
31
|
+
*
|
|
32
|
+
* Works inside `<Field label=…>`: the field's label names both the input and the strip of
|
|
33
|
+
* chips, and its hint and error describe the input. In read mode it is `Tags`.
|
|
34
|
+
*/
|
|
35
|
+
export declare function TagsField({ mode, ...props }: TagsFieldProps): import("react").JSX.Element;
|