@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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SINCPRO S.R.L
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,185 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @sincpro/web-ui
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**A design system for enterprise web software** — ERPs, back offices, management tools: the
|
|
4
|
+
screens where people spend eight hours a day reading records, filtering lists and filling
|
|
5
|
+
forms. React + Tailwind v4, open source, by [Sincpro](https://github.com/Sincpro-SRL).
|
|
6
|
+
|
|
7
|
+
Most design systems are built for marketing sites and consumer apps, then stretched over
|
|
8
|
+
business software. This one starts from the other end: dense data, long sessions, keyboard
|
|
9
|
+
users, numbers that must be legible, and screens that are always about _records_.
|
|
10
|
+
|
|
11
|
+
## A UI that evolves — a method that holds
|
|
12
|
+
|
|
13
|
+
`@sincpro/web-ui` **will keep changing.** Its look, the set of components and their props grow
|
|
14
|
+
and get corrected as real screens ask for them. It is `0.x`: a minor release may break an API,
|
|
15
|
+
and each release says what changed in
|
|
16
|
+
[GitHub Releases](https://github.com/Sincpro-SRL/sincpro_web_ui/releases).
|
|
17
|
+
|
|
18
|
+
What does not change is **how the components are managed** — and that is what this project is
|
|
19
|
+
about:
|
|
20
|
+
|
|
21
|
+
| Stable — the contract | Evolving — the content |
|
|
22
|
+
| ----------------------------------------------------------- | --------------------------------- |
|
|
23
|
+
| Every piece belongs to one of four levels, and imports down | Which components exist |
|
|
24
|
+
| Values come from tokens; contrast is measured | The palette, sizes, density steps |
|
|
25
|
+
| Interface copy is data, never written in a component | The wording |
|
|
26
|
+
| Every piece ships with a test and a story | Props, their names and defaults |
|
|
27
|
+
| Whole pages are built from the public API alone | Visual details of each piece |
|
|
28
|
+
|
|
29
|
+
Each rule on the left is checked by a machine — `make test`, `make doctor`, `make smoke` — not
|
|
30
|
+
by anyone's memory, so the catalogue can grow fast without rotting.
|
|
31
|
+
|
|
32
|
+
## The idea
|
|
33
|
+
|
|
34
|
+
**Every management screen is one of two things: one record, or N records.** That is the
|
|
35
|
+
whole product surface, and the system is organised around it in four levels — each one
|
|
36
|
+
defined by what it _knows_, and each one importing only from the levels below:
|
|
37
|
+
|
|
38
|
+
| Level | What it knows | Examples |
|
|
39
|
+
| --------------- | --------------------------------------- | -------------------------------------------------------- |
|
|
40
|
+
| **Foundations** | The decisions. No components. | color, typography, scales, motion, surfaces |
|
|
41
|
+
| **Primitives** | Nothing about the screen it is on. | `Button`, `Field`, `Menu`, `Tag`, `Table`, `Dialog` |
|
|
42
|
+
| **Components** | One question of the product, by family. | data tables, search and criteria, record parts, activity |
|
|
43
|
+
| **Views** | The two macros, and nothing else. | `Record` (one) · `List` (N) |
|
|
44
|
+
|
|
45
|
+
If something does not fit one of the two views, it is not a view — it is a component in
|
|
46
|
+
disguise. And if something needs to know what an invoice, a dataset or a customer _is_, it
|
|
47
|
+
does not belong here: that is domain, and it lives in the application.
|
|
48
|
+
|
|
49
|
+
### Principles
|
|
50
|
+
|
|
51
|
+
- **Tokens or nothing.** Every color, size and radius comes from `src/tokens.css`. No
|
|
52
|
+
arbitrary Tailwind values, no hand-written hex, no raw `px`, no raw Tailwind palette — a
|
|
53
|
+
test enforces it, and every exception is written down with its reason.
|
|
54
|
+
- **Contrast is measured, not guessed.** Each color token carries its WCAG ratio next to it,
|
|
55
|
+
and a test recomputes it: a number that drifted from its color fails the build.
|
|
56
|
+
- **Two surfaces.** The work happens on a warm light _canvas_; navigation lives on a dark
|
|
57
|
+
_chrome_. A piece is judged on the surface it will actually live on.
|
|
58
|
+
- **Density is a setting, not a fork.** The same components serve a dense ERP grid and a
|
|
59
|
+
roomy form (`DensityProvider`).
|
|
60
|
+
- **Language is data.** Interface copy is English by default and replaced as a whole table
|
|
61
|
+
(`installCopy(SPANISH_COPY)`, `UiCopyProvider`) — never hard-coded in a component.
|
|
62
|
+
- **Accessible by default.** Storybook runs the a11y checks as errors, not warnings.
|
|
63
|
+
|
|
64
|
+
The full set — hierarchy, one primary action, color that means something, progressive
|
|
65
|
+
disclosure, undo over confirm… — each with the piece that applies it and the check that
|
|
66
|
+
enforces it, is in
|
|
67
|
+
[`docs/PRINCIPLES.md`](https://github.com/Sincpro-SRL/sincpro_web_ui/blob/main/docs/PRINCIPLES.md)
|
|
68
|
+
(in Spanish).
|
|
69
|
+
|
|
70
|
+
## Using it
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
yarn add @sincpro/web-ui react react-dom tailwindcss @tanstack/react-table @tanstack/react-virtual
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Your bundler must run Tailwind v4 (`@tailwindcss/vite` or `@tailwindcss/postcss`). Then import
|
|
77
|
+
the tokens once — fonts, theme and Tailwind come with it:
|
|
78
|
+
|
|
79
|
+
```css
|
|
80
|
+
@import "@sincpro/web-ui/tokens.css";
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
import { Field, installCopy, SPANISH_COPY, TextInput } from "@sincpro/web-ui";
|
|
85
|
+
|
|
86
|
+
installCopy(SPANISH_COPY); // optional: the interface in Spanish
|
|
87
|
+
|
|
88
|
+
export function CompanyName({
|
|
89
|
+
value,
|
|
90
|
+
onChange,
|
|
91
|
+
}: {
|
|
92
|
+
value: string;
|
|
93
|
+
onChange: (v: string) => void;
|
|
94
|
+
}) {
|
|
95
|
+
return (
|
|
96
|
+
<Field label="Company name" required>
|
|
97
|
+
<TextInput value={value} onChange={onChange} />
|
|
98
|
+
</Field>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Each level is also its own entry — `@sincpro/web-ui/foundations`, `/primitives`,
|
|
104
|
+
`/components`, `/views` — so what a level offers is one import away.
|
|
105
|
+
|
|
106
|
+
**React Server Components.** Every module that needs the browser (hooks, context, events)
|
|
107
|
+
starts with `"use client"`, and a test keeps it that way; pure logic (calendar, pivot,
|
|
108
|
+
criteria) stays callable from the server. The views are client components: compose
|
|
109
|
+
`Record` inside a client component of your own.
|
|
110
|
+
|
|
111
|
+
### Theming (light / dark)
|
|
112
|
+
|
|
113
|
+
The palettes live in `tokens.css`: the same `--color-*` names, redefined for dark. Components
|
|
114
|
+
never read the theme — they use `bg-surface` / `text-ink` and the cascade resolves them — so
|
|
115
|
+
a screen needs no change to work in both.
|
|
116
|
+
|
|
117
|
+
- With nothing configured the stylesheet follows `prefers-color-scheme`.
|
|
118
|
+
- `data-theme="dark"` / `"light"` on **any** element forces that palette for everything under it
|
|
119
|
+
(a light invoice preview inside a dark app works). Portals live under `<html>`, so an app
|
|
120
|
+
switch writes it there.
|
|
121
|
+
- `ThemeProvider` + `useTheme()` do that write (`"system" | "light" | "dark"`; `"system"`
|
|
122
|
+
removes the attribute) and `ThemeSwitcher` is the tiny control. Remembering the choice is
|
|
123
|
+
the app's job (`onChange`).
|
|
124
|
+
|
|
125
|
+
```tsx
|
|
126
|
+
<ThemeProvider
|
|
127
|
+
defaultValue="system"
|
|
128
|
+
onChange={(theme) => localStorage.setItem("theme", theme)}
|
|
129
|
+
>
|
|
130
|
+
<App /> {/* somewhere: <ThemeSwitcher /> */}
|
|
131
|
+
</ThemeProvider>
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Rules a new token must follow: define it in **both** palettes with its contrast on the same line
|
|
135
|
+
(`tests/tokens.test.ts` measures each theme against its own ground, surface and chrome, and fails
|
|
136
|
+
if a light token has no dark twin or the two dark copies drift); the gold stays the primary
|
|
137
|
+
action in both (`on-accent` keeps the text on it dark); never write a colour outside
|
|
138
|
+
`tokens.css`; use `bg-scrim` — not `bg-ink/30` — for a veil. In Storybook the toolbar has a
|
|
139
|
+
theme switch, or open a story with `&globals=theme:dark`.
|
|
140
|
+
|
|
141
|
+
## Developing it
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
make init # pre-commit + yarn install
|
|
145
|
+
make dev # Storybook on :6006 — the living documentation
|
|
146
|
+
make test # node --test: behaviour, tokens, contrast, client boundaries
|
|
147
|
+
make smoke # every story opened in headless Chrome: no console errors, no blanks
|
|
148
|
+
make build # dist/ (JS + .d.ts + tokens.css)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Node 26 (the active LTS) or newer — `nvm use` reads `.nvmrc`. Every operation — local or CI —
|
|
152
|
+
goes through the `Makefile`; run `make` to list the targets.
|
|
153
|
+
|
|
154
|
+
```text
|
|
155
|
+
src/
|
|
156
|
+
├── tokens.css the source of every value: color, type, spacing, radii, motion
|
|
157
|
+
├── index.ts the public surface: the four levels below
|
|
158
|
+
├── foundations/ density, surface, copy, slots, calendar, pivot, xlsx
|
|
159
|
+
├── primitives/ one folder per family: button, field, menu, table, tabs, …
|
|
160
|
+
├── components/ one folder per family: listing, record, search, form, activity, frame, shell
|
|
161
|
+
└── views/ list (N records) · record (one record)
|
|
162
|
+
stories/ Storybook, by level — and templates/: whole pages built from the public API
|
|
163
|
+
tests/ node --test over the TypeScript sources
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
`make doctor` rejects an import that climbs a level (a primitive importing a component, a
|
|
167
|
+
component importing a view), so the layering holds without anyone remembering it.
|
|
168
|
+
|
|
169
|
+
### Iterating with AI
|
|
170
|
+
|
|
171
|
+
This system is developed together with AI agents, and it is set up for it: the rules an
|
|
172
|
+
agent must follow live in [`AGENTS.md`](https://github.com/Sincpro-SRL/sincpro_web_ui/blob/main/AGENTS.md), the guards that catch what an agent
|
|
173
|
+
gets wrong (tokens, contrast, import direction, client boundaries, stories that blow up)
|
|
174
|
+
run in `make test`, `make doctor` and `make smoke`, and
|
|
175
|
+
while `make dev` runs, Storybook serves an MCP endpoint at `/mcp` so an agent reads the
|
|
176
|
+
stories that already exist instead of guessing them.
|
|
177
|
+
|
|
178
|
+
## Contributing
|
|
179
|
+
|
|
180
|
+
See [CONTRIBUTING.md](https://github.com/Sincpro-SRL/sincpro_web_ui/blob/main/CONTRIBUTING.md). Issues and pull requests are welcome — especially
|
|
181
|
+
from people who build business software for a living.
|
|
182
|
+
|
|
183
|
+
## License
|
|
184
|
+
|
|
185
|
+
[MIT](https://github.com/Sincpro-SRL/sincpro_web_ui/blob/main/LICENSE) © SINCPRO S.R.L.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { Message } from "./Messages.js";
|
|
2
|
+
import type { Tone } from "../../foundations/types.js";
|
|
3
|
+
import { type CalloutProps } from "../../primitives/notice/Callout.js";
|
|
4
|
+
import type { LucideIcon } from "lucide-react";
|
|
5
|
+
import { type ReactNode } from "react";
|
|
6
|
+
/**
|
|
7
|
+
* Algo que el sistema anotó.
|
|
8
|
+
*
|
|
9
|
+
* Deliberadamente NO es el evento del producto: lo que una historia necesita
|
|
10
|
+
* para dibujar uno es qué pasó, cuándo, quién, y —cuando las hay— las tres cosas
|
|
11
|
+
* que uno termina yendo a buscar a otro lado.
|
|
12
|
+
*
|
|
13
|
+
* by QUIÉN. Ausente = lo hizo el sistema, que también es una respuesta.
|
|
14
|
+
* moved DE QUÉ A QUÉ. «borrador → confirmado» se lee de una.
|
|
15
|
+
* error QUÉ falló, entero y textual: quien mira esta pantalla muchas
|
|
16
|
+
* veces no tiene acceso a los logs.
|
|
17
|
+
*/
|
|
18
|
+
export type ActivityEvent = {
|
|
19
|
+
id: string;
|
|
20
|
+
/** Qué pasó, en una oración y en pasado. */
|
|
21
|
+
label: ReactNode;
|
|
22
|
+
at: Date | string | number;
|
|
23
|
+
tone?: Tone;
|
|
24
|
+
/** Quién lo hizo. Ausente = el sistema. */
|
|
25
|
+
by?: string;
|
|
26
|
+
/** El cambio de estado, cuando lo hubo. `from` ausente = nació así. */
|
|
27
|
+
moved?: {
|
|
28
|
+
from?: string;
|
|
29
|
+
to: string;
|
|
30
|
+
};
|
|
31
|
+
/** Lo que hay que saber además del título, cuando el título no alcanza. */
|
|
32
|
+
detail?: ReactNode;
|
|
33
|
+
/** Lo que falló, textual. Se muestra la PRIMERA línea y el resto se abre. */
|
|
34
|
+
error?: string;
|
|
35
|
+
/** El ícono del punto, cuando el rayo no dice lo suficiente. */
|
|
36
|
+
icon?: LucideIcon;
|
|
37
|
+
};
|
|
38
|
+
/** A notice of the record, as the panel lists it: a `Callout` without its × label. */
|
|
39
|
+
export type ActivityAlert = Omit<CalloutProps, "dismissLabel">;
|
|
40
|
+
/** `auto`: beside the sheet when the record is wide, below when not. `below`: always below. */
|
|
41
|
+
export type ActivityLayout = "auto" | "below";
|
|
42
|
+
export type ActivityProps = {
|
|
43
|
+
/** What people wrote. Absent —not empty— while there is no service behind it:
|
|
44
|
+
* an empty thread says «nobody wrote yet», which is a different claim. */
|
|
45
|
+
messages?: readonly Message[];
|
|
46
|
+
/** What the system logged. */
|
|
47
|
+
events?: readonly ActivityEvent[];
|
|
48
|
+
/** What is true now about the record, in urgency order — `Record` passes its notices. */
|
|
49
|
+
alerts?: readonly ActivityAlert[];
|
|
50
|
+
/** The id of the notices section: where the rail's indicator sends the focus. */
|
|
51
|
+
alertsId?: string;
|
|
52
|
+
/** The box to write in, when this record takes messages. It heads the conversation:
|
|
53
|
+
* writing is the thing you came to do, reading is what you do next. */
|
|
54
|
+
composer?: ReactNode;
|
|
55
|
+
open: boolean;
|
|
56
|
+
onToggle: () => void;
|
|
57
|
+
/** The panel's name. «Actividad» by default. */
|
|
58
|
+
label?: string;
|
|
59
|
+
layout?: ActivityLayout;
|
|
60
|
+
};
|
|
61
|
+
export declare function Activity({ messages, events, alerts, alertsId: ownAlertsId, composer, open, onToggle, label: ownLabel, layout, }: ActivityProps): import("react").JSX.Element;
|
|
62
|
+
/** Whether a standalone activity panel is open. Inside a `Record`, `Record.Activity`
|
|
63
|
+
* keeps this state itself (open by default: on a big screen you see what the record has). */
|
|
64
|
+
export declare function useActivity(initial?: boolean): {
|
|
65
|
+
open: boolean;
|
|
66
|
+
toggle: () => void;
|
|
67
|
+
/** Open it without closing it if it already was. */
|
|
68
|
+
show: () => void;
|
|
69
|
+
};
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { usePagination } from "../listing/pages.js";
|
|
4
|
+
import { Pagination } from "../listing/Pagination.js";
|
|
5
|
+
import { fillCopy, useUiCopy } from "../../foundations/copy.js";
|
|
6
|
+
import { cn } from "../../foundations/variants.js";
|
|
7
|
+
import { IconButton } from "../../primitives/button/Button.js";
|
|
8
|
+
import { SegmentedControl } from "../../primitives/choice/SegmentedControl.js";
|
|
9
|
+
import { useNoteEntries } from "../../primitives/field/note.js";
|
|
10
|
+
import { Callout } from "../../primitives/notice/Callout.js";
|
|
11
|
+
import { Tag } from "../../primitives/tag/Tag.js";
|
|
12
|
+
import { DateValue } from "../../primitives/value/Fecha.js";
|
|
13
|
+
import { Avatar } from "../../primitives/value/Person.js";
|
|
14
|
+
import { Activity as ActivityIcon, ArrowRight, CheckCircle2, ChevronDown, ChevronRight, ChevronUp, CornerDownRight, History, Info, Lock, MessageSquare, OctagonAlert, PanelRightClose, PanelRightOpen, TriangleAlert, Zap, } from "lucide-react";
|
|
15
|
+
import { Fragment, useId, useState } from "react";
|
|
16
|
+
/** The day an instant falls on, here — a `YYYY-MM-DD` key in local time. */
|
|
17
|
+
function dayKey(time) {
|
|
18
|
+
const d = new Date(time);
|
|
19
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
20
|
+
}
|
|
21
|
+
/** How a day separator reads: «Hoy», «Ayer», or the date — with the year only when it is
|
|
22
|
+
* not this one, since «13 de octubre» is all a recent history needs. */
|
|
23
|
+
function dayLabel(key, locale, words, now = new Date()) {
|
|
24
|
+
const today = dayKey(now.getTime());
|
|
25
|
+
const yesterday = dayKey(now.getTime() - 86_400_000);
|
|
26
|
+
if (key === today)
|
|
27
|
+
return words.today;
|
|
28
|
+
if (key === yesterday)
|
|
29
|
+
return words.yesterday;
|
|
30
|
+
const [y, m, d] = key.split("-").map(Number);
|
|
31
|
+
const date = new Date(y, m - 1, d);
|
|
32
|
+
return new Intl.DateTimeFormat(locale, {
|
|
33
|
+
weekday: "long",
|
|
34
|
+
day: "numeric",
|
|
35
|
+
month: "long",
|
|
36
|
+
...(y === now.getFullYear() ? {} : { year: "numeric" }),
|
|
37
|
+
}).format(date);
|
|
38
|
+
}
|
|
39
|
+
/* The most urgent tone first: the alerts' count takes the tone of the first. */
|
|
40
|
+
const URGENCY = {
|
|
41
|
+
bad: 0,
|
|
42
|
+
warn: 1,
|
|
43
|
+
info: 2,
|
|
44
|
+
good: 3,
|
|
45
|
+
};
|
|
46
|
+
function mostUrgent(alerts) {
|
|
47
|
+
return alerts.reduce((best, one) => {
|
|
48
|
+
const tone = one.tone ?? "info";
|
|
49
|
+
return URGENCY[tone] < URGENCY[best] ? tone : best;
|
|
50
|
+
}, "good");
|
|
51
|
+
}
|
|
52
|
+
/* The tone's shape, the same as `Callout`'s: a triangle warns, an «i» informs. */
|
|
53
|
+
const ALERT_ICON = {
|
|
54
|
+
bad: OctagonAlert,
|
|
55
|
+
warn: TriangleAlert,
|
|
56
|
+
info: Info,
|
|
57
|
+
good: CheckCircle2,
|
|
58
|
+
};
|
|
59
|
+
/* ONE icon for every event: the bolt, which is what happened — something ran. One per
|
|
60
|
+
tone turned the history into a sample book; what has to stand out is a failure, and
|
|
61
|
+
that is said by the row's tint. */
|
|
62
|
+
const EVENT_ICON = Zap;
|
|
63
|
+
const NODE_SKIN = {
|
|
64
|
+
ok: "bg-ink/8 text-muted",
|
|
65
|
+
failed: "bg-bad/12 text-bad",
|
|
66
|
+
};
|
|
67
|
+
function at(when) {
|
|
68
|
+
return when instanceof Date ? when.getTime() : new Date(when).getTime();
|
|
69
|
+
}
|
|
70
|
+
/* The stream's two kinds of entry share one GEOMETRY: a 1.5rem disc (the face, or the
|
|
71
|
+
event's node) on the left, the text beside it, the meta line under it. Two geometries
|
|
72
|
+
in one list read as two lists glued together. */
|
|
73
|
+
function MessageEntry({ message }) {
|
|
74
|
+
const copy = useUiCopy();
|
|
75
|
+
const internal = message.internal === true;
|
|
76
|
+
return (_jsxs("li", { className: "flex min-w-0 gap-2.5 px-2 py-2", children: [_jsx(Avatar, { name: message.author, size: "md" }), _jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-1", children: [_jsxs("div", { className: "flex min-w-0 flex-wrap items-baseline gap-x-2 gap-y-0.5", children: [_jsx("span", { className: "truncate text-meta font-medium text-ink", children: message.author }), _jsx("span", { className: "text-micro text-muted", children: _jsx(DateValue, { when: message.at, format: "relative" }) }), internal ? (_jsxs("span", { className: "inline-flex items-center gap-1 text-micro text-muted", title: copy.activity.internalNote, children: [_jsx(Lock, { className: "size-3", "aria-hidden": true }), copy.activity.internal] })) : null] }), _jsx("p", { className: cn("max-w-(--container-prose) rounded-box px-2.5 py-1.5 text-body whitespace-pre-line text-ink", internal ? "border border-warn/35 bg-warn/6" : "bg-ground"), children: message.body })] })] }));
|
|
77
|
+
}
|
|
78
|
+
/** The change of state: from what to what. The old one quiet, the new one in ink. */
|
|
79
|
+
const STATE_CHIP = "inline-flex max-w-32 min-w-0 items-center truncate rounded-pill border border-line-strong bg-ink/5 px-2 py-0.5 text-micro";
|
|
80
|
+
function Moved({ from, to }) {
|
|
81
|
+
return (_jsxs("span", { className: "inline-flex min-w-0 items-center gap-1", children: [from === undefined ? null : (_jsxs(_Fragment, { children: [_jsx("span", { className: `${STATE_CHIP} text-muted`, children: from }), _jsx(ArrowRight, { className: "size-3 shrink-0 text-muted", strokeWidth: 2.5, "aria-hidden": true })] })), _jsx("span", { className: `${STATE_CHIP} font-medium text-ink`, children: to })] }));
|
|
82
|
+
}
|
|
83
|
+
/** The error: the first line, and the rest one press away — kept, not dropped. */
|
|
84
|
+
function ErrorLine({ error }) {
|
|
85
|
+
const copy = useUiCopy();
|
|
86
|
+
const [open, setOpen] = useState(false);
|
|
87
|
+
const lines = error.split("\n");
|
|
88
|
+
const first = lines[0] ?? "";
|
|
89
|
+
const more = lines.length > 1 || first.length > 120;
|
|
90
|
+
return (_jsxs("div", { className: "min-w-0 rounded-control border border-line bg-surface px-2 py-1", children: [_jsx("p", { className: `min-w-0 font-mono text-micro text-ink ${open ? "break-words whitespace-pre-wrap" : "truncate"}`, children: open ? error : first }), !more ? null : (_jsx("button", { type: "button", onClick: () => setOpen(!open), className: "mt-0.5 cursor-pointer text-micro text-muted underline-offset-2 hover:text-ink hover:underline", children: open ? copy.activity.less : copy.activity.more }))] }));
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* UN EVENTO: a row that opens, not a line.
|
|
94
|
+
*
|
|
95
|
+
* Closed it says what happened —in up to two lines, never cut to «se envió …»— and
|
|
96
|
+
* under it who and when, in the quiet size. That second line is what made the narrow
|
|
97
|
+
* column legible: side by side, the name and the date took half the width and the
|
|
98
|
+
* label was clipped to three words. A failure tints the row; the rest stays ink.
|
|
99
|
+
*/
|
|
100
|
+
function EventEntry({ event }) {
|
|
101
|
+
const copy = useUiCopy();
|
|
102
|
+
const failed = event.tone === "bad" || event.error !== undefined;
|
|
103
|
+
const skin = failed ? "failed" : "ok";
|
|
104
|
+
const Icon = event.icon ?? EVENT_ICON;
|
|
105
|
+
const [open, setOpen] = useState(false);
|
|
106
|
+
const more = event.moved !== undefined || event.detail !== undefined || event.error !== undefined;
|
|
107
|
+
const row = (_jsxs(_Fragment, { children: [_jsx("span", { className: `relative z-10 flex size-6 shrink-0 items-center justify-center rounded-pill ring-2 ring-surface ${NODE_SKIN[skin]}`, children: _jsx(Icon, { className: "size-3", strokeWidth: 2.5, "aria-hidden": true }) }), _jsxs("span", { className: "flex min-w-0 flex-1 flex-col gap-0.5", children: [_jsx("span", { className: "line-clamp-2 text-meta text-ink", children: event.label }), _jsxs("span", { className: "flex min-w-0 flex-wrap items-baseline gap-x-1.5 text-micro text-muted", children: [_jsx("span", { className: "truncate", children: event.by ?? copy.activity.bySystem }), _jsx("span", { "aria-hidden": true, children: "\u00B7" }), _jsx(DateValue, { when: event.at, format: "relative" })] })] }), more ? (_jsx(ChevronRight, { className: `mt-1 size-3.5 shrink-0 text-muted transition-transform duration-(--duration-instant) ${open ? "rotate-90" : ""}`, strokeWidth: 2.5, "aria-hidden": true })) : null] }));
|
|
108
|
+
const box = `flex w-full min-w-0 items-start gap-2.5 rounded-control px-2 py-2 text-left ${failed ? "bg-bad/6" : ""}`;
|
|
109
|
+
return (_jsxs("li", { className: "group/event relative", children: [_jsx("span", { "aria-hidden": true, className: "absolute top-8 bottom-0 left-5 w-px bg-line group-last/event:hidden" }), more ? (_jsx("button", { type: "button", onClick: () => setOpen(!open), "aria-expanded": open, className: `${box} cursor-pointer transition-colors duration-(--duration-instant) focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-primary ${failed ? "hover:bg-bad/10" : "hover:bg-ink/4"}`, children: row })) : (_jsx("div", { className: box, children: row })), !open ? null : (_jsxs("div", { className: "mb-1 ml-10.5 flex min-w-0 flex-col gap-1.5 rounded-control bg-ground px-2.5 py-2", children: [event.moved === undefined ? null : (_jsx(Moved, { ...(event.moved.from === undefined ? {} : { from: event.moved.from }), to: event.moved.to })), event.detail === undefined ? null : (_jsx("div", { className: "min-w-0 text-micro text-muted", children: event.detail })), event.error === undefined ? null : _jsx(ErrorLine, { error: event.error })] }))] }));
|
|
110
|
+
}
|
|
111
|
+
/** Takes you to the field or row a note belongs to, and lights it for a moment so the eye
|
|
112
|
+
* lands on it. The light is the system's focus colour, faded out; with reduced motion it
|
|
113
|
+
* is shown and removed without the fade. */
|
|
114
|
+
function goTo(entry) {
|
|
115
|
+
const target = entry.anchor();
|
|
116
|
+
if (target === null)
|
|
117
|
+
return;
|
|
118
|
+
const still = typeof window !== "undefined" &&
|
|
119
|
+
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
120
|
+
target.scrollIntoView({ block: "center", behavior: still ? "auto" : "smooth" });
|
|
121
|
+
target.animate([
|
|
122
|
+
{
|
|
123
|
+
boxShadow: "0 0 0 0.125rem var(--color-primary)",
|
|
124
|
+
borderRadius: "var(--radius-control)",
|
|
125
|
+
},
|
|
126
|
+
{ boxShadow: "0 0 0 0.125rem transparent", borderRadius: "var(--radius-control)" },
|
|
127
|
+
], { duration: still ? 1 : 1800, easing: "ease-out", delay: still ? 1200 : 300 });
|
|
128
|
+
}
|
|
129
|
+
/** A field note or row flag in the timeline: its sentence, and WHAT it belongs to — a link
|
|
130
|
+
* that takes you there. */
|
|
131
|
+
function NoteLine({ entry }) {
|
|
132
|
+
const copy = useUiCopy();
|
|
133
|
+
const tone = entry.note.tone ?? "info";
|
|
134
|
+
const Icon = ALERT_ICON[tone];
|
|
135
|
+
return (_jsx("li", { className: "min-w-0", children: _jsxs("button", { type: "button", onClick: () => goTo(entry), title: entry.owner === ""
|
|
136
|
+
? undefined
|
|
137
|
+
: fillCopy(copy.activity.goTo, { owner: entry.owner }), className: "flex w-full min-w-0 cursor-pointer items-start gap-2 rounded-control px-2 py-1.5 text-left transition-colors duration-(--duration-instant) hover:bg-ink/4 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-primary", children: [_jsx("span", { className: `mt-0.5 shrink-0 ${TONE_TEXT[tone]}`, children: _jsx(Icon, { className: "size-(--size-icon-sm)", strokeWidth: 2.25, "aria-hidden": true }) }), _jsxs("span", { className: "flex min-w-0 flex-1 flex-col gap-0.5", children: [_jsx("span", { className: "text-meta text-ink", children: entry.note.label }), entry.owner === "" ? null : (_jsxs("span", { className: "flex min-w-0 items-center gap-1 text-micro text-muted", children: [_jsx(CornerDownRight, { className: "size-3 shrink-0", "aria-hidden": true }), _jsx("span", { className: "truncate", children: entry.owner })] }))] })] }) }));
|
|
138
|
+
}
|
|
139
|
+
const TONE_TEXT = {
|
|
140
|
+
bad: "text-bad",
|
|
141
|
+
warn: "text-warn",
|
|
142
|
+
info: "text-info",
|
|
143
|
+
good: "text-good",
|
|
144
|
+
};
|
|
145
|
+
export function Activity({ messages, events, alerts = [], alertsId: ownAlertsId, composer, open, onToggle, label: ownLabel, layout = "auto", }) {
|
|
146
|
+
const copy = useUiCopy();
|
|
147
|
+
const label = ownLabel ?? copy.activity.title;
|
|
148
|
+
const titleId = useId();
|
|
149
|
+
const autoAlertsId = useId();
|
|
150
|
+
const alertsId = ownAlertsId ?? autoAlertsId;
|
|
151
|
+
const auto = layout === "auto";
|
|
152
|
+
/* The notes of the fields and rows of the record around — `Record` keeps them. */
|
|
153
|
+
const notes = useNoteEntries();
|
|
154
|
+
const byTime = (items) => [...items].sort((x, y) => at(y.at) - at(x.at));
|
|
155
|
+
const said = byTime(messages ?? []);
|
|
156
|
+
const logged = byTime(events ?? []);
|
|
157
|
+
const notices = alerts.length + notes.length;
|
|
158
|
+
const tones = [
|
|
159
|
+
...alerts.map((a) => a.tone ?? "info"),
|
|
160
|
+
...notes.map((n) => n.note.tone ?? "info"),
|
|
161
|
+
];
|
|
162
|
+
const alertTone = tones.length === 0 ? undefined : mostUrgent(tones.map((tone) => ({ tone })));
|
|
163
|
+
const AlertIcon = ALERT_ICON[alertTone ?? "info"];
|
|
164
|
+
/* ONE stream, by time: what happened before what is the whole point of a history. */
|
|
165
|
+
const [filter, setFilter] = useState("all");
|
|
166
|
+
const stream = [
|
|
167
|
+
...said.map((message) => ({
|
|
168
|
+
kind: "message",
|
|
169
|
+
at: at(message.at),
|
|
170
|
+
id: `m-${message.id}`,
|
|
171
|
+
message,
|
|
172
|
+
})),
|
|
173
|
+
...logged.map((event) => ({
|
|
174
|
+
kind: "event",
|
|
175
|
+
at: at(event.at),
|
|
176
|
+
id: `e-${event.id}`,
|
|
177
|
+
event,
|
|
178
|
+
})),
|
|
179
|
+
].sort((x, y) => y.at - x.at);
|
|
180
|
+
const kept = stream.filter((entry) => filter === "all"
|
|
181
|
+
? true
|
|
182
|
+
: filter === "events"
|
|
183
|
+
? entry.kind === "event"
|
|
184
|
+
: entry.kind === "message" &&
|
|
185
|
+
(filter === "notes") === (entry.message.internal === true));
|
|
186
|
+
const pages = usePagination(kept, { pageSize: 20 });
|
|
187
|
+
const counts = {
|
|
188
|
+
conversation: said.filter((m) => m.internal !== true).length,
|
|
189
|
+
notes: said.filter((m) => m.internal === true).length,
|
|
190
|
+
events: logged.length,
|
|
191
|
+
};
|
|
192
|
+
/* The notices, compact: the first three, the rest one press away. */
|
|
193
|
+
const [allAlerts, setAllAlerts] = useState(false);
|
|
194
|
+
const SHOWN = 3;
|
|
195
|
+
/* A count with its shape and, for a screen reader, its word: «💬 1» is «Conversación 1». */
|
|
196
|
+
const counted = (Icon, n, word) => (_jsxs("span", { className: "inline-flex shrink-0 items-center gap-1 text-micro text-muted tabular-nums", children: [_jsx(Icon, { className: "size-3.5", strokeWidth: 2, "aria-hidden": true }), n, _jsx("span", { className: "sr-only", children: word })] }));
|
|
197
|
+
/* ── Folded: a strip that still says how much there is ──────────── */
|
|
198
|
+
if (!open) {
|
|
199
|
+
return (_jsxs("button", { type: "button", onClick: onToggle,
|
|
200
|
+
/* Named by what it shows — «Actividad, 2 avisos, 1 Conversación, 5 Historial» —
|
|
201
|
+
so the counts are heard too; the tooltip says what pressing it does. */
|
|
202
|
+
title: fillCopy(copy.activity.open, { label }), "aria-expanded": false, "data-activity": "folded", className: cn("flex w-full min-w-0 cursor-pointer items-center gap-2.5 rounded-box border border-line bg-surface px-3 py-2.5 text-left transition-colors duration-(--duration-instant) hover:bg-ink/3 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary",
|
|
203
|
+
/* Beside the sheet it folds to a slim column JOINED to it: the toggle on top
|
|
204
|
+
where the eye starts, and under it the counts, stacked — nothing hidden
|
|
205
|
+
silently. The sheet's own right edge is the hairline between the two. */
|
|
206
|
+
auto &&
|
|
207
|
+
"@beside/page:w-auto @beside/page:flex-col @beside/page:justify-start @beside/page:gap-3 @beside/page:rounded-l-none @beside/page:border-l-0 @beside/page:px-2 @beside/page:py-3"), children: [_jsx("span", { className: cn("hidden shrink-0 text-muted", auto && "@beside/page:block"), children: _jsx(PanelRightOpen, { className: "size-(--size-icon-md)", "aria-hidden": true }) }), _jsx(ActivityIcon, { className: cn("size-(--size-icon-md) shrink-0 text-muted", auto && "@beside/page:hidden"), "aria-hidden": true }), _jsx("span", { className: cn("min-w-0 truncate text-meta font-medium text-ink", auto && "@beside/page:sr-only"), children: label }), alertTone === undefined ? null : (_jsxs(Tag, { tone: alertTone, children: [_jsxs("span", { className: cn("hidden items-center gap-1", auto && "@beside/page:inline-flex"), children: [_jsx(AlertIcon, { className: "size-3", strokeWidth: 2.5, "aria-hidden": true }), notices] }), _jsx("span", { className: cn(auto && "@beside/page:sr-only"), children: fillCopy(notices === 1 ? copy.record.noticeOne : copy.record.noticeMany, {
|
|
208
|
+
count: notices,
|
|
209
|
+
}) })] })), messages === undefined
|
|
210
|
+
? null
|
|
211
|
+
: counted(MessageSquare, said.length, copy.activity.conversation), events === undefined ? null : counted(History, logged.length, copy.activity.history), _jsx("span", { className: cn("ml-auto shrink-0 text-muted", auto && "@beside/page:hidden"), children: _jsx(ChevronDown, { className: "size-(--size-icon-md)", "aria-hidden": true }) })] }));
|
|
212
|
+
}
|
|
213
|
+
return (_jsxs("section", { "aria-labelledby": titleId, "data-activity": "open", className: cn("flex w-full min-w-0 flex-col rounded-box border border-line bg-surface",
|
|
214
|
+
/* Beside the sheet: its column, joined to it — the sheet's right edge is the line. */
|
|
215
|
+
auto &&
|
|
216
|
+
"@beside/page:w-(--container-activity) @beside/page:rounded-l-none @beside/page:border-l-0"), children: [_jsxs("header", { className: "flex min-w-0 items-center gap-2 border-b border-line py-1.5 pr-1.5 pl-3", children: [_jsx(ActivityIcon, { className: "size-(--size-icon-md) shrink-0 text-muted", "aria-hidden": true }), _jsx("h2", { id: titleId, className: "min-w-0 truncate text-meta font-semibold text-ink", children: label }), _jsxs("span", { className: "ml-auto flex shrink-0", children: [_jsx("span", { className: cn("flex", auto && "@beside/page:hidden"), children: _jsx(IconButton, { icon: ChevronUp, label: fillCopy(copy.activity.fold, { label }), onClick: onToggle }) }), auto ? (_jsx("span", { className: "hidden @beside/page:flex", children: _jsx(IconButton, { icon: PanelRightClose, label: fillCopy(copy.activity.fold, { label }), onClick: onToggle }) })) : null] })] }), notices === 0 ? null : (
|
|
217
|
+
/* NOW, before what happened. The rail's indicator sends the focus here, so it is
|
|
218
|
+
focusable without being a tab stop. */
|
|
219
|
+
_jsxs("section", { id: alertsId, tabIndex: -1, "aria-label": copy.activity.alerts, "data-activity-alerts": "", className: "flex min-w-0 scroll-mt-(--size-appbar) flex-col gap-2 border-b border-line px-3 py-3 outline-0 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-primary", children: [_jsxs("h3", { className: "flex items-center gap-2 px-1 text-micro font-medium text-muted", children: [copy.activity.alerts, _jsx(Tag, { tone: alertTone ?? "info", children: notices })] }), (() => {
|
|
220
|
+
const rows = [
|
|
221
|
+
...alerts.map(({ children, ...alert }, i) => ({
|
|
222
|
+
key: `a${i}`,
|
|
223
|
+
node: (_jsx(Callout, { ...alert, dismissLabel: copy.shell.closeNotice, children: children })),
|
|
224
|
+
})),
|
|
225
|
+
...notes.map((entry) => ({
|
|
226
|
+
key: entry.id,
|
|
227
|
+
node: (_jsx("ul", { className: "-mx-1 flex min-w-0 flex-col", children: _jsx(NoteLine, { entry: entry }) })),
|
|
228
|
+
})),
|
|
229
|
+
];
|
|
230
|
+
const shown = allAlerts ? rows : rows.slice(0, SHOWN);
|
|
231
|
+
return (_jsxs(_Fragment, { children: [shown.map((row) => (_jsx(Fragment, { children: row.node }, row.key))), rows.length <= SHOWN ? null : (_jsx("button", { type: "button", "aria-expanded": allAlerts, onClick: () => setAllAlerts(!allAlerts), className: "self-start rounded-control px-1 text-micro font-medium text-primary hover:underline focus-visible:outline-2 focus-visible:outline-primary", children: allAlerts
|
|
232
|
+
? copy.activity.fewerAlerts
|
|
233
|
+
: fillCopy(copy.activity.moreAlerts, { count: rows.length - SHOWN }) }))] }));
|
|
234
|
+
})()] })), _jsxs("section", { "aria-label": copy.activity.viewTimeline, className: "flex min-w-0 flex-col gap-3 px-3 py-3", children: [_jsx("h3", { className: "px-1 text-micro font-medium text-muted", children: copy.activity.viewTimeline }), composer, stream.length === 0 ? null : (_jsx(SegmentedControl, { size: "sm", "aria-label": copy.activity.filters, value: filter, onChange: (next) => setFilter(next), options: [
|
|
235
|
+
{ value: "all", label: copy.activity.filterAll },
|
|
236
|
+
...(messages === undefined
|
|
237
|
+
? []
|
|
238
|
+
: [
|
|
239
|
+
{
|
|
240
|
+
value: "conversation",
|
|
241
|
+
label: `${copy.activity.filterConversation} · ${counts.conversation}`,
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
value: "notes",
|
|
245
|
+
label: `${copy.activity.filterNotes} · ${counts.notes}`,
|
|
246
|
+
},
|
|
247
|
+
]),
|
|
248
|
+
...(events === undefined
|
|
249
|
+
? []
|
|
250
|
+
: [
|
|
251
|
+
{
|
|
252
|
+
value: "events",
|
|
253
|
+
label: `${copy.activity.filterEvents} · ${counts.events}`,
|
|
254
|
+
},
|
|
255
|
+
]),
|
|
256
|
+
] })), stream.length === 0 ? (_jsx("p", { className: "px-1 text-meta text-muted", children: messages === undefined ? copy.activity.emptyEvents : copy.activity.emptyMessages })) : kept.length === 0 ? (_jsx("p", { className: "px-1 text-meta text-muted", children: copy.activity.emptyFilter })) : (_jsxs(_Fragment, { children: [_jsx("ol", { "aria-label": copy.activity.viewTimeline, className: "-mx-1 flex min-w-0 flex-col", children: pages.pageItems.map((entry, i) => {
|
|
257
|
+
const day = dayKey(entry.at);
|
|
258
|
+
const before = pages.pageItems[i - 1];
|
|
259
|
+
const newDay = before === undefined || dayKey(before.at) !== day;
|
|
260
|
+
return (_jsxs(Fragment, { children: [newDay ? (
|
|
261
|
+
/* The day: a quiet rule with its name, so «what happened on the 13th»
|
|
262
|
+
is found by scanning, not by reading every date. */
|
|
263
|
+
_jsxs("li", { "aria-hidden": true, className: "flex items-center gap-2 px-1 pt-2 pb-1 text-micro font-medium text-muted first:pt-0", children: [_jsx("span", { className: "first-letter:uppercase", children: dayLabel(day, copy.locale, {
|
|
264
|
+
today: copy.activity.today,
|
|
265
|
+
yesterday: copy.activity.yesterday,
|
|
266
|
+
}) }), _jsx("span", { className: "h-px flex-1 bg-line" })] })) : null, entry.kind === "message" ? (_jsx(MessageEntry, { message: entry.message })) : (_jsx(EventEntry, { event: entry.event }))] }, entry.id));
|
|
267
|
+
}) }), kept.length <= 20 ? null : _jsx(Pagination, { ...pages.bridge })] }))] })] }));
|
|
268
|
+
}
|
|
269
|
+
/** Whether a standalone activity panel is open. Inside a `Record`, `Record.Activity`
|
|
270
|
+
* keeps this state itself (open by default: on a big screen you see what the record has). */
|
|
271
|
+
export function useActivity(initial = true) {
|
|
272
|
+
const [open, setOpen] = useState(initial);
|
|
273
|
+
return {
|
|
274
|
+
open,
|
|
275
|
+
toggle: () => setOpen((current) => !current),
|
|
276
|
+
/** Open it without closing it if it already was. */
|
|
277
|
+
show: () => setOpen(true),
|
|
278
|
+
};
|
|
279
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* The box to write in. What gets written is drawn by `Activity`, together with
|
|
4
|
+
* what the system logged — see its note on why the merge is the default.
|
|
5
|
+
*
|
|
6
|
+
* Odoo's chatter —our activity— is two things sharing a column — a log the system writes and a
|
|
7
|
+
* thread people write — and they are usually confused because they sit
|
|
8
|
+
* together. They are not the same: an event is a fact that happened, and a
|
|
9
|
+
* message is somebody's account of it. The system's half never needs a reply
|
|
10
|
+
* box; this half is nothing without one.
|
|
11
|
+
*
|
|
12
|
+
* ── A note is not a message, and the difference is who reads it ─────
|
|
13
|
+
*
|
|
14
|
+
* Odoo splits «log an internal note» from «send a message» and the split is
|
|
15
|
+
* worth keeping: one stays inside, the other leaves. Drawing them the same and
|
|
16
|
+
* hoping the writer remembers is how somebody's internal remark reaches a
|
|
17
|
+
* client. So the CHOICE COMES FIRST: the buttons are what you see, and the box
|
|
18
|
+
* only appears once you said which kind of thing you are writing. A box you can
|
|
19
|
+
* type into before choosing is a box whose destination you find out afterwards.
|
|
20
|
+
*
|
|
21
|
+
* ── And who receives it, before you send it ─────────────────────────
|
|
22
|
+
*
|
|
23
|
+
* A message that leaves goes to somebody, and «somebody» has to be readable
|
|
24
|
+
* without pressing send to find out. `recipients` is drawn next to the box for
|
|
25
|
+
* the kinds that leave, and never for an internal note — whose whole definition
|
|
26
|
+
* is that it reaches nobody outside.
|
|
27
|
+
*/
|
|
28
|
+
export type Message = {
|
|
29
|
+
id: string;
|
|
30
|
+
author: string;
|
|
31
|
+
at: Date | string | number;
|
|
32
|
+
body: string;
|
|
33
|
+
/** An internal note: it stays inside. Drawn differently on purpose. */
|
|
34
|
+
internal?: boolean;
|
|
35
|
+
};
|
|
36
|
+
/** A way of writing. `note` stays inside; everything else leaves, and what
|
|
37
|
+
* changes between them is only the channel and who is on the other side. */
|
|
38
|
+
export type MessageKind = {
|
|
39
|
+
id: string;
|
|
40
|
+
label: string;
|
|
41
|
+
/** It stays inside. Without this the kit assumes it leaves, which is the
|
|
42
|
+
* answer that errs on the side of asking. */
|
|
43
|
+
internal?: boolean;
|
|
44
|
+
/** Who gets it, in their own words: names for a mail, numbers for WhatsApp.
|
|
45
|
+
* Shown before sending, never after. */
|
|
46
|
+
recipients?: readonly string[];
|
|
47
|
+
placeholder?: string;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* ESCRIBIR UNA NOTA, la manera que toda lista tiene.
|
|
51
|
+
*
|
|
52
|
+
* Es una función y no una constante porque sus palabras salen de la copia, y una
|
|
53
|
+
* constante las congelaría con las que hubiera al cargar el módulo —o sea, las
|
|
54
|
+
* de fábrica— por más que después se instale otra.
|
|
55
|
+
*/
|
|
56
|
+
export declare function writeANote(): MessageKind;
|
|
57
|
+
export type MessageComposerProps = {
|
|
58
|
+
/** The ways of writing this record takes. Its own list because it is the
|
|
59
|
+
* application that knows whether there is WhatsApp, and to whose number. */
|
|
60
|
+
kinds?: readonly MessageKind[];
|
|
61
|
+
onSend: (body: string, kind: MessageKind) => void;
|
|
62
|
+
sending?: boolean;
|
|
63
|
+
/** Next to the ways of writing, for whatever the application has to say about the send. */
|
|
64
|
+
footer?: ReactNode;
|
|
65
|
+
};
|
|
66
|
+
export declare function MessageComposer({ kinds, onSend, sending, footer, }: MessageComposerProps): import("react").JSX.Element;
|