@sincpro/web-ui 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +157 -2
- package/dist/components/assist/AssistMark.d.ts +64 -0
- package/dist/components/assist/AssistMark.js +108 -0
- package/dist/components/assist/AssistPending.d.ts +24 -0
- package/dist/components/assist/AssistPending.js +26 -0
- package/dist/components/assist/AssistSuggestion.d.ts +42 -0
- package/dist/components/assist/AssistSuggestion.js +55 -0
- package/dist/components/assist/ChangeReview.d.ts +56 -0
- package/dist/components/assist/ChangeReview.js +121 -0
- package/dist/components/assist/assist.d.ts +77 -0
- package/dist/components/assist/assist.js +87 -0
- package/dist/components/assist/index.d.ts +5 -0
- package/dist/components/assist/index.js +7 -0
- package/dist/components/chatter/Chatter.d.ts +161 -0
- package/dist/components/chatter/Chatter.js +232 -0
- package/dist/components/chatter/Messages.d.ts +66 -0
- package/dist/components/chatter/Messages.js +43 -0
- package/dist/components/chatter/Timeline.d.ts +21 -0
- package/dist/components/chatter/Timeline.js +30 -0
- package/dist/components/chatter/index.d.ts +3 -0
- package/dist/components/chatter/index.js +4 -0
- package/dist/components/form/Form.d.ts +52 -0
- package/dist/components/form/Form.js +49 -0
- package/dist/components/form/Sheet.d.ts +43 -0
- package/dist/components/form/Sheet.js +83 -0
- package/dist/components/form/index.d.ts +2 -0
- package/dist/components/form/index.js +4 -0
- package/dist/components/frame/Chart.d.ts +13 -0
- package/dist/components/frame/Chart.js +24 -0
- package/dist/components/frame/FrameView.d.ts +33 -0
- package/dist/components/frame/FrameView.js +125 -0
- package/dist/components/frame/index.d.ts +3 -0
- package/dist/components/frame/index.js +3 -0
- package/dist/components/frame/model.d.ts +43 -0
- package/dist/components/frame/model.js +101 -0
- package/dist/components/index.d.ts +10 -0
- package/dist/components/index.js +11 -0
- package/dist/components/lines/LineItems.d.ts +74 -0
- package/dist/components/lines/LineItems.js +229 -0
- package/dist/components/lines/index.d.ts +1 -0
- package/dist/components/lines/index.js +2 -0
- package/dist/components/lines/lines.d.ts +23 -0
- package/dist/components/lines/lines.js +26 -0
- package/dist/components/listing/ColumnsMenu.d.ts +20 -0
- package/dist/components/listing/ColumnsMenu.js +46 -0
- package/dist/components/listing/GroupingLevels.d.ts +35 -0
- package/dist/components/listing/GroupingLevels.js +45 -0
- package/dist/components/listing/Listing.d.ts +27 -0
- package/dist/components/listing/Listing.js +86 -0
- package/dist/components/listing/ListingBar.d.ts +73 -0
- package/dist/components/listing/ListingBar.js +105 -0
- package/dist/components/listing/Pagination.d.ts +36 -0
- package/dist/components/listing/Pagination.js +69 -0
- package/dist/components/listing/RecordCard.d.ts +33 -0
- package/dist/components/listing/RecordCard.js +22 -0
- package/dist/components/listing/RecordLink.d.ts +7 -0
- package/dist/components/listing/RecordLink.js +22 -0
- package/dist/components/listing/Row.d.ts +106 -0
- package/dist/components/listing/Row.js +122 -0
- package/dist/components/listing/SortMenu.d.ts +62 -0
- package/dist/components/listing/SortMenu.js +105 -0
- package/dist/components/listing/ViewSwitcher.d.ts +24 -0
- package/dist/components/listing/ViewSwitcher.js +16 -0
- package/dist/components/listing/columns.d.ts +47 -0
- package/dist/components/listing/columns.js +72 -0
- package/dist/components/listing/grouping.d.ts +35 -0
- package/dist/components/listing/grouping.js +47 -0
- package/dist/components/listing/index.d.ts +17 -0
- package/dist/components/listing/index.js +20 -0
- package/dist/components/listing/modelColumns.d.ts +50 -0
- package/dist/components/listing/modelColumns.js +103 -0
- package/dist/components/listing/pages.d.ts +78 -0
- package/dist/components/listing/pages.js +135 -0
- package/dist/components/listing/selection.d.ts +21 -0
- package/dist/components/listing/selection.js +43 -0
- package/dist/components/listing/set.d.ts +48 -0
- package/dist/components/listing/set.js +24 -0
- package/dist/components/listing/tree.d.ts +28 -0
- package/dist/components/listing/tree.js +23 -0
- package/dist/components/navigation/CommandPalette.d.ts +40 -0
- package/dist/components/navigation/CommandPalette.js +114 -0
- package/dist/components/navigation/NavigationTree.d.ts +31 -0
- package/dist/components/navigation/NavigationTree.js +79 -0
- package/dist/components/navigation/commands.d.ts +30 -0
- package/dist/components/navigation/commands.js +30 -0
- package/dist/components/navigation/index.d.ts +3 -0
- package/dist/components/navigation/index.js +4 -0
- package/dist/components/record/Breadcrumb.d.ts +42 -0
- package/dist/components/record/Breadcrumb.js +98 -0
- package/dist/components/record/DirtyActions.d.ts +26 -0
- package/dist/components/record/DirtyActions.js +22 -0
- package/dist/components/record/Pager.d.ts +18 -0
- package/dist/components/record/Pager.js +33 -0
- package/dist/components/record/RecordHeader.d.ts +71 -0
- package/dist/components/record/RecordHeader.js +72 -0
- package/dist/components/record/RecordSkeleton.d.ts +3 -0
- package/dist/components/record/RecordSkeleton.js +21 -0
- package/dist/components/record/SmartButtons.d.ts +35 -0
- package/dist/components/record/SmartButtons.js +38 -0
- package/dist/components/record/StatusBar.d.ts +51 -0
- package/dist/components/record/StatusBar.js +168 -0
- package/dist/components/record/ThumbBar.d.ts +36 -0
- package/dist/components/record/ThumbBar.js +86 -0
- package/dist/components/record/index.d.ts +6 -0
- package/dist/components/record/index.js +8 -0
- package/dist/components/search/CriteriaBuilder.d.ts +58 -0
- package/dist/components/search/CriteriaBuilder.js +278 -0
- package/dist/components/search/CriteriaChips.d.ts +22 -0
- package/dist/components/search/CriteriaChips.js +64 -0
- package/dist/components/search/FilterBuilder.d.ts +142 -0
- package/dist/components/search/FilterBuilder.js +421 -0
- package/dist/components/search/JoinWord.d.ts +8 -0
- package/dist/components/search/JoinWord.js +47 -0
- package/dist/components/search/JsonEditor.d.ts +19 -0
- package/dist/components/search/JsonEditor.js +78 -0
- package/dist/components/search/SearchBar.d.ts +78 -0
- package/dist/components/search/SearchBar.js +387 -0
- package/dist/components/search/criteria.d.ts +116 -0
- package/dist/components/search/criteria.js +159 -0
- package/dist/components/search/datePeriods.d.ts +122 -0
- package/dist/components/search/datePeriods.js +134 -0
- package/dist/components/search/dateRanges.d.ts +54 -0
- package/dist/components/search/dateRanges.js +89 -0
- package/dist/components/search/index.d.ts +9 -0
- package/dist/components/search/index.js +20 -0
- package/dist/components/search/model.d.ts +54 -0
- package/dist/components/search/model.js +37 -0
- package/dist/components/search/query.d.ts +166 -0
- package/dist/components/search/query.js +436 -0
- package/dist/components/shell/AppFrame.d.ts +14 -0
- package/dist/components/shell/AppFrame.js +80 -0
- package/dist/components/shell/TopBar.d.ts +73 -0
- package/dist/components/shell/TopBar.js +54 -0
- package/dist/components/shell/headerSlot.d.ts +17 -0
- package/dist/components/shell/headerSlot.js +21 -0
- package/dist/components/shell/index.d.ts +2 -0
- package/dist/components/shell/index.js +3 -0
- package/dist/foundations/aria.d.ts +14 -0
- package/dist/foundations/aria.js +18 -0
- package/dist/foundations/calendar.d.ts +28 -0
- package/dist/foundations/calendar.js +183 -0
- package/dist/foundations/copy.d.ts +619 -0
- package/dist/foundations/copy.es.d.ts +2 -0
- package/dist/foundations/copy.es.js +377 -0
- package/dist/foundations/copy.js +472 -0
- package/dist/foundations/density.d.ts +16 -0
- package/dist/foundations/density.js +21 -0
- package/dist/foundations/grouping.d.ts +281 -0
- package/dist/foundations/grouping.js +0 -0
- package/dist/foundations/index.d.ts +14 -0
- package/dist/foundations/index.js +30 -0
- package/dist/foundations/layout.d.ts +9 -0
- package/dist/foundations/layout.js +9 -0
- package/dist/foundations/pivot.d.ts +222 -0
- package/dist/foundations/pivot.js +198 -0
- package/dist/foundations/pivotSheet.d.ts +61 -0
- package/dist/foundations/pivotSheet.js +0 -0
- package/dist/foundations/slots.d.ts +67 -0
- package/dist/foundations/slots.js +63 -0
- package/dist/foundations/sticky.d.ts +53 -0
- package/dist/foundations/sticky.js +150 -0
- package/dist/foundations/surface.d.ts +9 -0
- package/dist/foundations/surface.js +26 -0
- package/dist/foundations/theme.d.ts +42 -0
- package/dist/foundations/theme.js +88 -0
- package/dist/foundations/types.d.ts +23 -0
- package/dist/foundations/types.js +50 -0
- package/dist/foundations/variants.d.ts +19 -0
- package/dist/foundations/variants.js +48 -0
- package/dist/foundations/xlsx.d.ts +86 -0
- package/dist/foundations/xlsx.js +0 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +19 -0
- package/dist/primitives/block/Block.d.ts +66 -0
- package/dist/primitives/block/Block.js +34 -0
- package/dist/primitives/block/Line.d.ts +18 -0
- package/dist/primitives/block/Line.js +45 -0
- package/dist/primitives/block/SectionGrid.d.ts +30 -0
- package/dist/primitives/block/SectionGrid.js +29 -0
- package/dist/primitives/block/Steps.d.ts +76 -0
- package/dist/primitives/block/Steps.js +43 -0
- package/dist/primitives/block/index.d.ts +4 -0
- package/dist/primitives/block/index.js +7 -0
- package/dist/primitives/button/Button.d.ts +137 -0
- package/dist/primitives/button/Button.js +234 -0
- package/dist/primitives/button/index.d.ts +1 -0
- package/dist/primitives/button/index.js +2 -0
- package/dist/primitives/cell/CellStack.d.ts +47 -0
- package/dist/primitives/cell/CellStack.js +33 -0
- package/dist/primitives/cell/index.d.ts +1 -0
- package/dist/primitives/cell/index.js +3 -0
- package/dist/primitives/choice/ComboBox.d.ts +16 -0
- package/dist/primitives/choice/ComboBox.js +52 -0
- package/dist/primitives/choice/RadioGroup.d.ts +18 -0
- package/dist/primitives/choice/RadioGroup.js +50 -0
- package/dist/primitives/choice/SearchField.d.ts +24 -0
- package/dist/primitives/choice/SearchField.js +27 -0
- package/dist/primitives/choice/SegmentedControl.d.ts +16 -0
- package/dist/primitives/choice/SegmentedControl.js +69 -0
- package/dist/primitives/choice/Slider.d.ts +16 -0
- package/dist/primitives/choice/Slider.js +46 -0
- package/dist/primitives/choice/Switch.d.ts +14 -0
- package/dist/primitives/choice/Switch.js +65 -0
- package/dist/primitives/choice/ThemeSwitcher.d.ts +10 -0
- package/dist/primitives/choice/ThemeSwitcher.js +25 -0
- package/dist/primitives/choice/index.d.ts +8 -0
- package/dist/primitives/choice/index.js +11 -0
- package/dist/primitives/choice/options.d.ts +28 -0
- package/dist/primitives/choice/options.js +6 -0
- package/dist/primitives/date/Calendar.d.ts +31 -0
- package/dist/primitives/date/Calendar.js +110 -0
- package/dist/primitives/date/DatePicker.d.ts +34 -0
- package/dist/primitives/date/DatePicker.js +99 -0
- package/dist/primitives/date/index.d.ts +2 -0
- package/dist/primitives/date/index.js +4 -0
- package/dist/primitives/dialog/Dialog.d.ts +118 -0
- package/dist/primitives/dialog/Dialog.js +121 -0
- package/dist/primitives/dialog/index.d.ts +1 -0
- package/dist/primitives/dialog/index.js +2 -0
- package/dist/primitives/disclosure/Disclosure.d.ts +36 -0
- package/dist/primitives/disclosure/Disclosure.js +52 -0
- package/dist/primitives/disclosure/index.d.ts +1 -0
- package/dist/primitives/disclosure/index.js +2 -0
- package/dist/primitives/field/Checks.d.ts +57 -0
- package/dist/primitives/field/Checks.js +72 -0
- package/dist/primitives/field/DropZone.d.ts +42 -0
- package/dist/primitives/field/DropZone.js +52 -0
- package/dist/primitives/field/Field.d.ts +166 -0
- package/dist/primitives/field/Field.js +240 -0
- package/dist/primitives/field/Suggest.d.ts +48 -0
- package/dist/primitives/field/Suggest.js +43 -0
- package/dist/primitives/field/aria.d.ts +61 -0
- package/dist/primitives/field/aria.js +70 -0
- package/dist/primitives/field/context.d.ts +30 -0
- package/dist/primitives/field/context.js +13 -0
- package/dist/primitives/field/index.d.ts +7 -0
- package/dist/primitives/field/index.js +11 -0
- package/dist/primitives/field/layout.d.ts +24 -0
- package/dist/primitives/field/layout.js +10 -0
- package/dist/primitives/field/mode.d.ts +9 -0
- package/dist/primitives/field/mode.js +13 -0
- package/dist/primitives/hint/Hint.d.ts +31 -0
- package/dist/primitives/hint/Hint.js +42 -0
- package/dist/primitives/hint/index.d.ts +1 -0
- package/dist/primitives/hint/index.js +1 -0
- package/dist/primitives/index.d.ts +19 -0
- package/dist/primitives/index.js +20 -0
- package/dist/primitives/menu/Menu.d.ts +100 -0
- package/dist/primitives/menu/Menu.js +59 -0
- package/dist/primitives/menu/index.d.ts +1 -0
- package/dist/primitives/menu/index.js +2 -0
- package/dist/primitives/notice/Callout.d.ts +49 -0
- package/dist/primitives/notice/Callout.js +28 -0
- package/dist/primitives/notice/Notice.d.ts +45 -0
- package/dist/primitives/notice/Notice.js +61 -0
- package/dist/primitives/notice/Toast.d.ts +55 -0
- package/dist/primitives/notice/Toast.js +142 -0
- package/dist/primitives/notice/index.d.ts +3 -0
- package/dist/primitives/notice/index.js +5 -0
- package/dist/primitives/overlay/Popover.d.ts +6 -0
- package/dist/primitives/overlay/Popover.js +27 -0
- package/dist/primitives/overlay/PopoverTrigger.d.ts +55 -0
- package/dist/primitives/overlay/PopoverTrigger.js +38 -0
- package/dist/primitives/overlay/Tooltip.d.ts +38 -0
- package/dist/primitives/overlay/Tooltip.js +74 -0
- package/dist/primitives/overlay/index.d.ts +3 -0
- package/dist/primitives/overlay/index.js +5 -0
- package/dist/primitives/picker/Picker.d.ts +48 -0
- package/dist/primitives/picker/Picker.js +150 -0
- package/dist/primitives/picker/index.d.ts +1 -0
- package/dist/primitives/picker/index.js +2 -0
- package/dist/primitives/surface/Peek.d.ts +60 -0
- package/dist/primitives/surface/Peek.js +71 -0
- package/dist/primitives/surface/index.d.ts +1 -0
- package/dist/primitives/surface/index.js +2 -0
- package/dist/primitives/table/CellEditor.d.ts +14 -0
- package/dist/primitives/table/CellEditor.js +83 -0
- package/dist/primitives/table/DataTable.d.ts +135 -0
- package/dist/primitives/table/DataTable.js +244 -0
- package/dist/primitives/table/EditableTable.d.ts +96 -0
- package/dist/primitives/table/EditableTable.js +423 -0
- package/dist/primitives/table/PivotTable.d.ts +14 -0
- package/dist/primitives/table/PivotTable.js +0 -0
- package/dist/primitives/table/ReportTable.d.ts +38 -0
- package/dist/primitives/table/ReportTable.js +106 -0
- package/dist/primitives/table/RowActions.d.ts +16 -0
- package/dist/primitives/table/RowActions.js +28 -0
- package/dist/primitives/table/SheetTable.d.ts +70 -0
- package/dist/primitives/table/SheetTable.js +197 -0
- package/dist/primitives/table/Table.d.ts +106 -0
- package/dist/primitives/table/Table.js +116 -0
- package/dist/primitives/table/index.d.ts +7 -0
- package/dist/primitives/table/index.js +9 -0
- package/dist/primitives/tabs/Tabs.d.ts +91 -0
- package/dist/primitives/tabs/Tabs.js +56 -0
- package/dist/primitives/tabs/index.d.ts +1 -0
- package/dist/primitives/tabs/index.js +1 -0
- package/dist/primitives/tag/Tag.d.ts +36 -0
- package/dist/primitives/tag/Tag.js +68 -0
- package/dist/primitives/tag/Tags.d.ts +57 -0
- package/dist/primitives/tag/Tags.js +104 -0
- package/dist/primitives/tag/TagsField.d.ts +32 -0
- package/dist/primitives/tag/TagsField.js +182 -0
- package/dist/primitives/tag/choices.d.ts +38 -0
- package/dist/primitives/tag/choices.js +26 -0
- package/dist/primitives/tag/index.d.ts +4 -0
- package/dist/primitives/tag/index.js +8 -0
- package/dist/primitives/toolbar/Toolbar.d.ts +13 -0
- package/dist/primitives/toolbar/Toolbar.js +36 -0
- package/dist/primitives/toolbar/index.d.ts +1 -0
- package/dist/primitives/toolbar/index.js +2 -0
- package/dist/primitives/value/Copyable.d.ts +37 -0
- package/dist/primitives/value/Copyable.js +50 -0
- package/dist/primitives/value/Fecha.d.ts +12 -0
- package/dist/primitives/value/Fecha.js +80 -0
- package/dist/primitives/value/Meter.d.ts +29 -0
- package/dist/primitives/value/Meter.js +46 -0
- package/dist/primitives/value/Metric.d.ts +42 -0
- package/dist/primitives/value/Metric.js +38 -0
- package/dist/primitives/value/Person.d.ts +90 -0
- package/dist/primitives/value/Person.js +208 -0
- package/dist/primitives/value/Relation.d.ts +69 -0
- package/dist/primitives/value/Relation.js +57 -0
- package/dist/primitives/value/Totals.d.ts +26 -0
- package/dist/primitives/value/Totals.js +4 -0
- package/dist/primitives/value/Value.d.ts +77 -0
- package/dist/primitives/value/Value.js +119 -0
- package/dist/primitives/value/index.d.ts +8 -0
- package/dist/primitives/value/index.js +9 -0
- package/dist/tokens.css +1025 -0
- package/dist/views/index.d.ts +2 -0
- package/dist/views/index.js +3 -0
- package/dist/views/list/List.d.ts +113 -0
- package/dist/views/list/List.js +97 -0
- package/dist/views/list/ListView.d.ts +6 -0
- package/dist/views/list/ListView.js +269 -0
- package/dist/views/list/columns.d.ts +18 -0
- package/dist/views/list/columns.js +27 -0
- package/dist/views/list/derive.d.ts +76 -0
- package/dist/views/list/derive.js +181 -0
- package/dist/views/list/index.d.ts +2 -0
- package/dist/views/list/index.js +4 -0
- package/dist/views/list/presentations.d.ts +39 -0
- package/dist/views/list/presentations.js +103 -0
- package/dist/views/list/types.d.ts +272 -0
- package/dist/views/list/types.js +1 -0
- package/dist/views/list/useNarrowList.d.ts +17 -0
- package/dist/views/list/useNarrowList.js +32 -0
- package/dist/views/record/Record.d.ts +126 -0
- package/dist/views/record/Record.js +141 -0
- package/dist/views/record/index.d.ts +1 -0
- package/dist/views/record/index.js +2 -0
- package/package.json +109 -4
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,158 @@
|
|
|
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
|
+
## The idea
|
|
12
|
+
|
|
13
|
+
**Every management screen is one of two things: one record, or N records.** That is the
|
|
14
|
+
whole product surface, and the system is organised around it in four levels — each one
|
|
15
|
+
defined by what it _knows_, and each one importing only from the levels below:
|
|
16
|
+
|
|
17
|
+
| Level | What it knows | Examples |
|
|
18
|
+
| --------------- | --------------------------------------- | ------------------------------------------------------- |
|
|
19
|
+
| **Foundations** | The decisions. No components. | color, typography, scales, motion, surfaces |
|
|
20
|
+
| **Primitives** | Nothing about the screen it is on. | `Button`, `Field`, `Menu`, `Tag`, `Table`, `Dialog` |
|
|
21
|
+
| **Components** | One question of the product, by family. | data tables, search and criteria, record parts, chatter |
|
|
22
|
+
| **Views** | The two macros, and nothing else. | `Record` (one) · `List` (N) |
|
|
23
|
+
|
|
24
|
+
If something does not fit one of the two views, it is not a view — it is a component in
|
|
25
|
+
disguise. And if something needs to know what an invoice, a dataset or a customer _is_, it
|
|
26
|
+
does not belong here: that is domain, and it lives in the application.
|
|
27
|
+
|
|
28
|
+
### Principles
|
|
29
|
+
|
|
30
|
+
- **Tokens or nothing.** Every color, size and radius comes from `src/tokens.css`. No
|
|
31
|
+
arbitrary Tailwind values, no hand-written hex, no raw `px`, no raw Tailwind palette — a
|
|
32
|
+
test enforces it, and every exception is written down with its reason.
|
|
33
|
+
- **Contrast is measured, not guessed.** Each color token carries its WCAG ratio next to it,
|
|
34
|
+
and a test recomputes it: a number that drifted from its color fails the build.
|
|
35
|
+
- **Two surfaces.** The work happens on a warm light _canvas_; navigation lives on a dark
|
|
36
|
+
_chrome_. A piece is judged on the surface it will actually live on.
|
|
37
|
+
- **Density is a setting, not a fork.** The same components serve a dense ERP grid and a
|
|
38
|
+
roomy form (`DensityProvider`).
|
|
39
|
+
- **Language is data.** Interface copy is English by default and replaced as a whole table
|
|
40
|
+
(`installCopy(SPANISH_COPY)`, `UiCopyProvider`) — never hard-coded in a component.
|
|
41
|
+
- **Accessible by default.** Storybook runs the a11y checks as errors, not warnings.
|
|
42
|
+
|
|
43
|
+
## Using it
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
yarn add @sincpro/web-ui react react-dom tailwindcss @tanstack/react-table @tanstack/react-virtual
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Your bundler must run Tailwind v4 (`@tailwindcss/vite` or `@tailwindcss/postcss`). Then import
|
|
50
|
+
the tokens once — fonts, theme and Tailwind come with it:
|
|
51
|
+
|
|
52
|
+
```css
|
|
53
|
+
@import "@sincpro/web-ui/tokens.css";
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
import { Field, installCopy, SPANISH_COPY, TextInput } from "@sincpro/web-ui";
|
|
58
|
+
|
|
59
|
+
installCopy(SPANISH_COPY); // optional: the interface in Spanish
|
|
60
|
+
|
|
61
|
+
export function CompanyName({
|
|
62
|
+
value,
|
|
63
|
+
onChange,
|
|
64
|
+
}: {
|
|
65
|
+
value: string;
|
|
66
|
+
onChange: (v: string) => void;
|
|
67
|
+
}) {
|
|
68
|
+
return (
|
|
69
|
+
<Field label="Company name" required>
|
|
70
|
+
<TextInput value={value} onChange={onChange} />
|
|
71
|
+
</Field>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Each level is also its own entry — `@sincpro/web-ui/foundations`, `/primitives`,
|
|
77
|
+
`/components`, `/views` — so what a level offers is one import away.
|
|
78
|
+
|
|
79
|
+
**React Server Components.** Every module that needs the browser (hooks, context, events)
|
|
80
|
+
starts with `"use client"`, and a test keeps it that way; pure logic (calendar, pivot,
|
|
81
|
+
criteria) stays callable from the server. The views are client components: compose
|
|
82
|
+
`Record` inside a client component of your own.
|
|
83
|
+
|
|
84
|
+
### Theming (light / dark)
|
|
85
|
+
|
|
86
|
+
The palettes live in `tokens.css`: the same `--color-*` names, redefined for dark. Components
|
|
87
|
+
never read the theme — they use `bg-surface` / `text-ink` and the cascade resolves them — so
|
|
88
|
+
a screen needs no change to work in both.
|
|
89
|
+
|
|
90
|
+
- With nothing configured the stylesheet follows `prefers-color-scheme`.
|
|
91
|
+
- `data-theme="dark"` / `"light"` on **any** element forces that palette for everything under it
|
|
92
|
+
(a light invoice preview inside a dark app works). Portals live under `<html>`, so an app
|
|
93
|
+
switch writes it there.
|
|
94
|
+
- `ThemeProvider` + `useTheme()` do that write (`"system" | "light" | "dark"`; `"system"`
|
|
95
|
+
removes the attribute) and `ThemeSwitcher` is the tiny control. Remembering the choice is
|
|
96
|
+
the app's job (`onChange`).
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
<ThemeProvider
|
|
100
|
+
defaultValue="system"
|
|
101
|
+
onChange={(theme) => localStorage.setItem("theme", theme)}
|
|
102
|
+
>
|
|
103
|
+
<App /> {/* somewhere: <ThemeSwitcher /> */}
|
|
104
|
+
</ThemeProvider>
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Rules a new token must follow: define it in **both** palettes with its contrast on the same line
|
|
108
|
+
(`tests/tokens.test.ts` measures each theme against its own ground, surface and chrome, and fails
|
|
109
|
+
if a light token has no dark twin or the two dark copies drift); the gold stays the primary
|
|
110
|
+
action in both (`on-accent` keeps the text on it dark); never write a colour outside
|
|
111
|
+
`tokens.css`; use `bg-scrim` — not `bg-ink/30` — for a veil. In Storybook the toolbar has a
|
|
112
|
+
theme switch, or open a story with `&globals=theme:dark`.
|
|
113
|
+
|
|
114
|
+
## Developing it
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
make init # pre-commit + yarn install
|
|
118
|
+
make dev # Storybook on :6006 — the living documentation
|
|
119
|
+
make test # node --test: behaviour, tokens, contrast, client boundaries
|
|
120
|
+
make smoke # every story opened in headless Chrome: no console errors, no blanks
|
|
121
|
+
make build # dist/ (JS + .d.ts + tokens.css)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Node 26 (the active LTS) or newer — `nvm use` reads `.nvmrc`. Every operation — local or CI —
|
|
125
|
+
goes through the `Makefile`; run `make` to list the targets.
|
|
126
|
+
|
|
127
|
+
```text
|
|
128
|
+
src/
|
|
129
|
+
├── tokens.css the source of every value: color, type, spacing, radii, motion
|
|
130
|
+
├── index.ts the public surface: the four levels below
|
|
131
|
+
├── foundations/ density, surface, copy, slots, calendar, pivot, xlsx
|
|
132
|
+
├── primitives/ one folder per family: button, field, menu, table, tabs, …
|
|
133
|
+
├── components/ one folder per family: listing, record, search, form, chatter, frame, shell
|
|
134
|
+
└── views/ list (N records) · record (one record)
|
|
135
|
+
stories/ Storybook, by level — and templates/: whole pages built from the public API
|
|
136
|
+
tests/ node --test over the TypeScript sources
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`make doctor` rejects an import that climbs a level (a primitive importing a component, a
|
|
140
|
+
component importing a view), so the layering holds without anyone remembering it.
|
|
141
|
+
|
|
142
|
+
### Iterating with AI
|
|
143
|
+
|
|
144
|
+
This system is developed together with AI agents, and it is set up for it: the rules an
|
|
145
|
+
agent must follow live in [`AGENTS.md`](./AGENTS.md), the guards that catch what an agent
|
|
146
|
+
gets wrong (tokens, contrast, import direction, client boundaries, stories that blow up)
|
|
147
|
+
run in `make test`, `make doctor` and `make smoke`, and
|
|
148
|
+
while `make dev` runs, Storybook serves an MCP endpoint at `/mcp` so an agent reads the
|
|
149
|
+
stories that already exist instead of guessing them.
|
|
150
|
+
|
|
151
|
+
## Contributing
|
|
152
|
+
|
|
153
|
+
See [CONTRIBUTING.md](./CONTRIBUTING.md). Issues and pull requests are welcome — especially
|
|
154
|
+
from people who build business software for a living.
|
|
155
|
+
|
|
156
|
+
## License
|
|
157
|
+
|
|
158
|
+
[MIT](./LICENSE) © SINCPRO S.R.L.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { type AssistOrigin, type AssistSource } from "./assist.js";
|
|
2
|
+
import type { ChatterEvent } from "../chatter/Chatter.js";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
/** The robot and the assistant's name: the header of a suggestion, a review, a pending
|
|
5
|
+
* state. `children` replaces the name — «Asistente sugiere». */
|
|
6
|
+
export declare function AssistLabel({ children, id }: {
|
|
7
|
+
children?: ReactNode;
|
|
8
|
+
id?: string;
|
|
9
|
+
}): import("react").JSX.Element;
|
|
10
|
+
/**
|
|
11
|
+
* Where the assistant got it: «Fuente: orden de compra PO/2026/0042».
|
|
12
|
+
*
|
|
13
|
+
* Context: a value without its source is a value nobody can check, and an unchecked value
|
|
14
|
+
* from a machine is the one people stop trusting after the first mistake. The source is a
|
|
15
|
+
* LINK to the record it was read from — with `href` a real `<a>` (middle-click, copy), with
|
|
16
|
+
* `onOpen` a link the screen answers — so checking it is one press, not a search.
|
|
17
|
+
*/
|
|
18
|
+
export declare function AssistSourceLink({ source }: {
|
|
19
|
+
source: AssistSource;
|
|
20
|
+
}): import("react").JSX.Element;
|
|
21
|
+
export type AssistMarkProps = {
|
|
22
|
+
/** When, from what, and who accepted it. Each one that is present is said in the card. */
|
|
23
|
+
origin?: AssistOrigin;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* The small robot next to a value the assistant set — provenance, after the fact.
|
|
27
|
+
*
|
|
28
|
+
* Context: once a suggestion is accepted the value looks like any other, and a week later
|
|
29
|
+
* nobody remembers whether «30 días» was typed or proposed. The mark keeps that answer next
|
|
30
|
+
* to the value: its tooltip says «Lo completó el asistente», and pressing it opens WHO
|
|
31
|
+
* (the assistant, and the person who accepted), WHEN and FROM WHAT — with the source as a
|
|
32
|
+
* link. It is a popover and not a tooltip because the source is a link, and a tooltip can
|
|
33
|
+
* never be entered with the keyboard.
|
|
34
|
+
*
|
|
35
|
+
* Next to a field, write it in the field's `action`; next to a read value, use
|
|
36
|
+
* `AssistedValue`.
|
|
37
|
+
*/
|
|
38
|
+
export declare function AssistMark({ origin }: AssistMarkProps): import("react").JSX.Element;
|
|
39
|
+
/** A read value with the assistant's mark after it: a `Record.Facts` value, a table cell. */
|
|
40
|
+
export declare function AssistedValue({ children, origin, }: {
|
|
41
|
+
/** The value, drawn the way it would be without the mark. */
|
|
42
|
+
children: ReactNode;
|
|
43
|
+
origin?: AssistOrigin;
|
|
44
|
+
}): import("react").JSX.Element;
|
|
45
|
+
/**
|
|
46
|
+
* The assistant's entry in a record's history: the chatter's event, with the robot as its
|
|
47
|
+
* node, the assistant (and who accepted) as its author, and the source as its detail.
|
|
48
|
+
*
|
|
49
|
+
* Context: provenance has two places. The mark answers «where did THIS value come from»
|
|
50
|
+
* while looking at it; the history answers «what has the assistant done on this record»,
|
|
51
|
+
* in the order it happened, next to what people did. Same mark in both, so the eye learns
|
|
52
|
+
* it once.
|
|
53
|
+
*
|
|
54
|
+
* Call it when the change happens (in the handler), not at module load: it reads the
|
|
55
|
+
* installed copy, like the formatters do.
|
|
56
|
+
*/
|
|
57
|
+
export declare function assistEvent({ id, label, at, source, acceptedBy, }: {
|
|
58
|
+
id: string;
|
|
59
|
+
/** What happened, in the past: «completó 5 líneas desde la orden de compra». */
|
|
60
|
+
label: ReactNode;
|
|
61
|
+
at: Date | string | number;
|
|
62
|
+
source?: AssistSource;
|
|
63
|
+
acceptedBy?: string;
|
|
64
|
+
}): ChatterEvent;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { ASSIST_ICON, } from "./assist.js";
|
|
4
|
+
import { currentCopy, fillCopy, useUiCopy } from "../../foundations/copy.js";
|
|
5
|
+
import { cn, focusRing } from "../../foundations/variants.js";
|
|
6
|
+
import { PopoverTrigger } from "../../primitives/overlay/PopoverTrigger.js";
|
|
7
|
+
import { Tooltip, TooltipTrigger } from "../../primitives/overlay/Tooltip.js";
|
|
8
|
+
import { DateValue } from "../../primitives/value/Fecha.js";
|
|
9
|
+
import { Link } from "react-aria-components";
|
|
10
|
+
/* ==================================================================== *
|
|
11
|
+
* THE ASSISTANT'S MARK — how a screen says «this came from the assistant».
|
|
12
|
+
*
|
|
13
|
+
* Three pieces, one look:
|
|
14
|
+
*
|
|
15
|
+
* AssistLabel the robot and the word «Asistente», in the info tone:
|
|
16
|
+
* the header of anything the assistant proposes
|
|
17
|
+
* AssistSourceLink «Fuente: orden de compra PO/2026/0042», a real link
|
|
18
|
+
* AssistMark the small robot next to a value it SET, which opens
|
|
19
|
+
* who, when and from what
|
|
20
|
+
*
|
|
21
|
+
* ── Why the info tone, and never the gold ───────────────────────────
|
|
22
|
+
*
|
|
23
|
+
* The gold is the ONE main action of a screen (PRINCIPLES §2–3). The assistant
|
|
24
|
+
* is not an action, it is a source of information — and `info` already means
|
|
25
|
+
* «a fact that changes how you read what is below». It is also never the color
|
|
26
|
+
* alone: the robot and the word are always there, so a person who does not tell
|
|
27
|
+
* teal from navy still reads «Asistente».
|
|
28
|
+
*
|
|
29
|
+
* ── Why a robot and not sparkles ────────────────────────────────────
|
|
30
|
+
*
|
|
31
|
+
* Sparkles celebrate; a business screen states. «An automated assistant did
|
|
32
|
+
* this» is the fact a person needs to weigh a value, and the robot says exactly
|
|
33
|
+
* that. The icon lives in `ASSIST_ICON`, so changing it is one line.
|
|
34
|
+
* ==================================================================== */
|
|
35
|
+
/** The robot and the assistant's name: the header of a suggestion, a review, a pending
|
|
36
|
+
* state. `children` replaces the name — «Asistente sugiere». */
|
|
37
|
+
export function AssistLabel({ children, id }) {
|
|
38
|
+
const copy = useUiCopy();
|
|
39
|
+
const Icon = ASSIST_ICON;
|
|
40
|
+
return (_jsxs("span", { id: id, "data-assist": "label", className: "inline-flex shrink-0 items-center gap-1 text-micro font-medium text-info", children: [_jsx(Icon, { className: "size-(--size-icon-sm) shrink-0", strokeWidth: 2, "aria-hidden": true }), children ?? copy.assist.assistant] }));
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Where the assistant got it: «Fuente: orden de compra PO/2026/0042».
|
|
44
|
+
*
|
|
45
|
+
* Context: a value without its source is a value nobody can check, and an unchecked value
|
|
46
|
+
* from a machine is the one people stop trusting after the first mistake. The source is a
|
|
47
|
+
* LINK to the record it was read from — with `href` a real `<a>` (middle-click, copy), with
|
|
48
|
+
* `onOpen` a link the screen answers — so checking it is one press, not a search.
|
|
49
|
+
*/
|
|
50
|
+
export function AssistSourceLink({ source }) {
|
|
51
|
+
const copy = useUiCopy();
|
|
52
|
+
const goes = source.href !== undefined || source.onOpen !== undefined;
|
|
53
|
+
return (_jsxs("span", { className: "inline-flex max-w-full min-w-0 items-baseline gap-1 text-micro text-muted", children: [_jsxs("span", { className: "shrink-0", children: [copy.assist.source, ":"] }), goes ? (_jsx(Link, { ...(source.href === undefined ? {} : { href: source.href }), ...(source.onOpen === undefined ? {} : { onPress: source.onOpen }), className: ({ isFocusVisible }) => cn("min-w-0 cursor-pointer truncate rounded-mark text-primary underline-offset-2 hover:underline", focusRing({ isFocusVisible })), children: source.label })) : (_jsx("span", { className: "min-w-0 truncate text-ink", children: source.label }))] }));
|
|
54
|
+
}
|
|
55
|
+
/** The mark's own button. It forwards `ref` and spreads native props, so `PopoverTrigger`
|
|
56
|
+
* anchors to it and `TooltipTrigger` names it — the same contract as `IconButton`, in the
|
|
57
|
+
* assistant's tone instead of the three an icon button has. */
|
|
58
|
+
function MarkButton({ label, expanded, haspopup, ref, ...native }) {
|
|
59
|
+
const Icon = ASSIST_ICON;
|
|
60
|
+
return (_jsxs(TooltipTrigger, { children: [_jsx("button", { ...native, ref: ref, type: "button", "aria-label": label, ...(haspopup === undefined ? {} : { "aria-haspopup": haspopup }), ...(expanded === undefined ? {} : { "aria-expanded": expanded }), "data-assist": "mark", className: "inline-flex size-(--size-control-sm) shrink-0 items-center justify-center rounded-pill text-info transition-colors duration-(--duration-instant) hover:bg-info/10 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary", children: _jsx(Icon, { className: "size-(--size-icon-sm)", strokeWidth: 2, "aria-hidden": true }) }), _jsx(Tooltip, { children: label })] }));
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The small robot next to a value the assistant set — provenance, after the fact.
|
|
64
|
+
*
|
|
65
|
+
* Context: once a suggestion is accepted the value looks like any other, and a week later
|
|
66
|
+
* nobody remembers whether «30 días» was typed or proposed. The mark keeps that answer next
|
|
67
|
+
* to the value: its tooltip says «Lo completó el asistente», and pressing it opens WHO
|
|
68
|
+
* (the assistant, and the person who accepted), WHEN and FROM WHAT — with the source as a
|
|
69
|
+
* link. It is a popover and not a tooltip because the source is a link, and a tooltip can
|
|
70
|
+
* never be entered with the keyboard.
|
|
71
|
+
*
|
|
72
|
+
* Next to a field, write it in the field's `action`; next to a read value, use
|
|
73
|
+
* `AssistedValue`.
|
|
74
|
+
*/
|
|
75
|
+
export function AssistMark({ origin = {} }) {
|
|
76
|
+
const copy = useUiCopy();
|
|
77
|
+
return (_jsx(PopoverTrigger, { title: copy.assist.madeBy, placement: "bottom end", trigger: _jsx(MarkButton, { label: copy.assist.madeBy }), children: _jsxs("div", { className: "flex max-w-72 min-w-0 flex-col gap-1.5 text-micro text-muted", children: [origin.source === undefined ? null : _jsx(AssistSourceLink, { source: origin.source }), origin.at === undefined ? null : _jsx(DateValue, { when: origin.at, format: "exact" }), origin.acceptedBy === undefined ? null : (_jsx("span", { children: fillCopy(copy.assist.acceptedBy, { name: origin.acceptedBy }) }))] }) }));
|
|
78
|
+
}
|
|
79
|
+
/** A read value with the assistant's mark after it: a `Record.Facts` value, a table cell. */
|
|
80
|
+
export function AssistedValue({ children, origin, }) {
|
|
81
|
+
return (_jsxs("span", { className: "inline-flex max-w-full min-w-0 items-center gap-0.5", children: [_jsx("span", { className: "min-w-0 truncate", children: children }), _jsx(AssistMark, { ...(origin === undefined ? {} : { origin }) })] }));
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The assistant's entry in a record's history: the chatter's event, with the robot as its
|
|
85
|
+
* node, the assistant (and who accepted) as its author, and the source as its detail.
|
|
86
|
+
*
|
|
87
|
+
* Context: provenance has two places. The mark answers «where did THIS value come from»
|
|
88
|
+
* while looking at it; the history answers «what has the assistant done on this record»,
|
|
89
|
+
* in the order it happened, next to what people did. Same mark in both, so the eye learns
|
|
90
|
+
* it once.
|
|
91
|
+
*
|
|
92
|
+
* Call it when the change happens (in the handler), not at module load: it reads the
|
|
93
|
+
* installed copy, like the formatters do.
|
|
94
|
+
*/
|
|
95
|
+
export function assistEvent({ id, label, at, source, acceptedBy, }) {
|
|
96
|
+
const copy = currentCopy();
|
|
97
|
+
const by = acceptedBy === undefined
|
|
98
|
+
? copy.assist.assistant
|
|
99
|
+
: `${copy.assist.assistant} · ${fillCopy(copy.assist.acceptedBy, { name: acceptedBy })}`;
|
|
100
|
+
return {
|
|
101
|
+
id,
|
|
102
|
+
label,
|
|
103
|
+
at,
|
|
104
|
+
by,
|
|
105
|
+
icon: ASSIST_ICON,
|
|
106
|
+
...(source === undefined ? {} : { detail: _jsx(AssistSourceLink, { source: source }) }),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type AssistPendingProps = {
|
|
2
|
+
/** What the assistant is doing, in the screen's words: «Leyendo la orden de compra
|
|
3
|
+
* PO/2026/0042…». The copy's «El asistente está trabajando…» when absent. */
|
|
4
|
+
label?: string;
|
|
5
|
+
/** Stops it. Present = a «Cancelar» next to the sentence. */
|
|
6
|
+
onCancel?: () => void;
|
|
7
|
+
/** Skeleton bars under the sentence, standing in for the proposal that is coming —
|
|
8
|
+
* `0` (the default) is the one-line form, for under a field. */
|
|
9
|
+
rows?: number;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* The assistant is working on a suggestion, and the screen goes on.
|
|
13
|
+
*
|
|
14
|
+
* Context: the assistant is slower than a form, and a screen that locks while it thinks
|
|
15
|
+
* teaches people to wait for it — or to close it. So this BLOCKS NOTHING: it is a line in
|
|
16
|
+
* the place where the suggestion will appear (under a field, above the lines), the fields
|
|
17
|
+
* stay editable, and «Cancelar» stops it. What the person types in the meantime wins; the
|
|
18
|
+
* screen decides whether a suggestion that arrives late still makes sense.
|
|
19
|
+
*
|
|
20
|
+
* The sentence is a polite live region (`role="status"`), so a screen reader hears that
|
|
21
|
+
* the assistant started without being interrupted; the bars are silent. The spinner and
|
|
22
|
+
* the bars' pulse stop under reduced motion, by the global rule in `tokens.css`.
|
|
23
|
+
*/
|
|
24
|
+
export declare function AssistPending({ label, onCancel, rows }: AssistPendingProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { AssistLabel } from "./AssistMark.js";
|
|
4
|
+
import { useUiCopy } from "../../foundations/copy.js";
|
|
5
|
+
import { Button } from "../../primitives/button/Button.js";
|
|
6
|
+
import { LoaderCircle } from "lucide-react";
|
|
7
|
+
/** The widths of the skeleton's bars: uneven, so it reads as lines of content and not as
|
|
8
|
+
* a striped box. */
|
|
9
|
+
const BAR = ["w-2/3", "w-1/2", "w-3/4", "w-2/5"];
|
|
10
|
+
/**
|
|
11
|
+
* The assistant is working on a suggestion, and the screen goes on.
|
|
12
|
+
*
|
|
13
|
+
* Context: the assistant is slower than a form, and a screen that locks while it thinks
|
|
14
|
+
* teaches people to wait for it — or to close it. So this BLOCKS NOTHING: it is a line in
|
|
15
|
+
* the place where the suggestion will appear (under a field, above the lines), the fields
|
|
16
|
+
* stay editable, and «Cancelar» stops it. What the person types in the meantime wins; the
|
|
17
|
+
* screen decides whether a suggestion that arrives late still makes sense.
|
|
18
|
+
*
|
|
19
|
+
* The sentence is a polite live region (`role="status"`), so a screen reader hears that
|
|
20
|
+
* the assistant started without being interrupted; the bars are silent. The spinner and
|
|
21
|
+
* the bars' pulse stop under reduced motion, by the global rule in `tokens.css`.
|
|
22
|
+
*/
|
|
23
|
+
export function AssistPending({ label, onCancel, rows = 0 }) {
|
|
24
|
+
const copy = useUiCopy();
|
|
25
|
+
return (_jsxs("div", { "data-assist": "pending", "aria-busy": "true", className: "flex min-w-0 flex-col gap-2", children: [_jsxs("span", { className: "flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1", children: [_jsx(LoaderCircle, { className: "size-(--size-icon-sm) shrink-0 animate-spin text-info", strokeWidth: 2, "aria-hidden": true }), _jsx(AssistLabel, {}), _jsx("span", { role: "status", className: "min-w-0 text-micro text-muted", children: label ?? copy.assist.working }), onCancel === undefined ? null : (_jsx(Button, { role: "ghost", size: "sm", onClick: onCancel, children: copy.assist.cancel }))] }), rows <= 0 ? null : (_jsx("span", { "aria-hidden": true, className: "flex flex-col gap-2 pl-5", children: Array.from({ length: rows }, (_, i) => (_jsx("span", { className: `h-3 rounded-control bg-ink/8 motion-safe:animate-pulse ${BAR[i % BAR.length]}` }, i))) }))] }));
|
|
26
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type AssistApply, type AssistSource } from "./assist.js";
|
|
2
|
+
import { type ReactNode } from "react";
|
|
3
|
+
export type AssistSuggestionProps = {
|
|
4
|
+
/** The proposed value, drawn as it will read once applied: «30 días», «15/11/2026», a
|
|
5
|
+
* product with its code. */
|
|
6
|
+
value: ReactNode;
|
|
7
|
+
/** Where the assistant got it — a link to the record it read. */
|
|
8
|
+
source?: AssistSource;
|
|
9
|
+
/** Applies the value. Return the function that reverts it and the screen gets a
|
|
10
|
+
* «Deshacer» notice; the suggestion itself is the screen's to remove. */
|
|
11
|
+
onAccept: AssistApply;
|
|
12
|
+
/** «No, gracias»: the screen forgets the suggestion. */
|
|
13
|
+
onDismiss: () => void;
|
|
14
|
+
/** The field's name, for the notice after accepting. Inside a `Field` the accessible
|
|
15
|
+
* name already takes the field's label; the notice is read elsewhere, so it needs it. */
|
|
16
|
+
field?: string;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* The assistant proposes a value for ONE field: the customer's payment term, the product of
|
|
20
|
+
* a scanned line, the due date. It goes right under the control it would change.
|
|
21
|
+
*
|
|
22
|
+
* Context: a suggestion has to be noticed and must not be mistaken for the value. So it is
|
|
23
|
+
* NOT written into the control — a pre-filled field reads as «someone already decided» and
|
|
24
|
+
* gets saved without a look. It sits under the control, in a dashed box (dashed: proposed,
|
|
25
|
+
* not yet real), with the robot and «Asistente sugiere», the value, and its source as a
|
|
26
|
+
* link. «Aceptar» applies it at once, and the notice carries «Deshacer» — undo over
|
|
27
|
+
* confirm, the system's rule. The × forgets it.
|
|
28
|
+
*
|
|
29
|
+
* Written inside a `Field`, after the control:
|
|
30
|
+
*
|
|
31
|
+
* <Field label="Plazo de pago">
|
|
32
|
+
* <NumberInput … />
|
|
33
|
+
* <AssistSuggestion value="30 días" source={…} onAccept={…} onDismiss={…} />
|
|
34
|
+
* </Field>
|
|
35
|
+
*
|
|
36
|
+
* It is a group named «Sugerencia del asistente» + the field's label, so a screen reader
|
|
37
|
+
* hears which field it is about before the value. Both buttons are plain buttons: Tab
|
|
38
|
+
* reaches them, Enter and Space press them. After pressing either, the focus goes back to
|
|
39
|
+
* the field's control — the suggestion that held it is gone, and the control is what the
|
|
40
|
+
* person was working on.
|
|
41
|
+
*/
|
|
42
|
+
export declare function AssistSuggestion({ value, source, onAccept, onDismiss, field, }: AssistSuggestionProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { acceptedMessage, applyWithUndo, } from "./assist.js";
|
|
4
|
+
import { AssistLabel, AssistSourceLink } from "./AssistMark.js";
|
|
5
|
+
import { useUiCopy } from "../../foundations/copy.js";
|
|
6
|
+
import { Button, IconButton } from "../../primitives/button/Button.js";
|
|
7
|
+
import { FieldIdsContext } from "../../primitives/field/context.js";
|
|
8
|
+
import { useToastIfAny } from "../../primitives/notice/Toast.js";
|
|
9
|
+
import { Check, X } from "lucide-react";
|
|
10
|
+
import { useContext, useId } from "react";
|
|
11
|
+
/**
|
|
12
|
+
* The assistant proposes a value for ONE field: the customer's payment term, the product of
|
|
13
|
+
* a scanned line, the due date. It goes right under the control it would change.
|
|
14
|
+
*
|
|
15
|
+
* Context: a suggestion has to be noticed and must not be mistaken for the value. So it is
|
|
16
|
+
* NOT written into the control — a pre-filled field reads as «someone already decided» and
|
|
17
|
+
* gets saved without a look. It sits under the control, in a dashed box (dashed: proposed,
|
|
18
|
+
* not yet real), with the robot and «Asistente sugiere», the value, and its source as a
|
|
19
|
+
* link. «Aceptar» applies it at once, and the notice carries «Deshacer» — undo over
|
|
20
|
+
* confirm, the system's rule. The × forgets it.
|
|
21
|
+
*
|
|
22
|
+
* Written inside a `Field`, after the control:
|
|
23
|
+
*
|
|
24
|
+
* <Field label="Plazo de pago">
|
|
25
|
+
* <NumberInput … />
|
|
26
|
+
* <AssistSuggestion value="30 días" source={…} onAccept={…} onDismiss={…} />
|
|
27
|
+
* </Field>
|
|
28
|
+
*
|
|
29
|
+
* It is a group named «Sugerencia del asistente» + the field's label, so a screen reader
|
|
30
|
+
* hears which field it is about before the value. Both buttons are plain buttons: Tab
|
|
31
|
+
* reaches them, Enter and Space press them. After pressing either, the focus goes back to
|
|
32
|
+
* the field's control — the suggestion that held it is gone, and the control is what the
|
|
33
|
+
* person was working on.
|
|
34
|
+
*/
|
|
35
|
+
export function AssistSuggestion({ value, source, onAccept, onDismiss, field, }) {
|
|
36
|
+
const copy = useUiCopy();
|
|
37
|
+
const toasts = useToastIfAny();
|
|
38
|
+
const ids = useContext(FieldIdsContext);
|
|
39
|
+
const nameId = useId();
|
|
40
|
+
/* Back to the control once this box is gone. A frame later: the screen removes the
|
|
41
|
+
suggestion in the same update, and the control is only focusable after it. */
|
|
42
|
+
const refocus = () => {
|
|
43
|
+
const target = ids?.id;
|
|
44
|
+
if (target === undefined || typeof document === "undefined")
|
|
45
|
+
return;
|
|
46
|
+
requestAnimationFrame(() => document.getElementById(target)?.focus());
|
|
47
|
+
};
|
|
48
|
+
return (_jsxs("div", { role: "group", "aria-labelledby": ids === null ? nameId : `${nameId} ${ids.labelId}`, "data-assist": "suggestion", className: "flex min-w-0 flex-wrap items-center gap-x-3 gap-y-1.5 rounded-control border border-dashed border-info/40 bg-info/5 py-1 pr-1 pl-2.5", children: [_jsx("span", { id: nameId, hidden: true, children: copy.assist.suggestion }), _jsxs("span", { className: "flex min-w-0 flex-1 flex-col gap-0.5 py-0.5", children: [_jsxs("span", { className: "flex min-w-0 flex-wrap items-baseline gap-x-1.5", children: [_jsxs(AssistLabel, { children: [copy.assist.assistant, " ", copy.assist.suggests] }), _jsx("span", { className: "min-w-0 text-meta font-semibold text-ink tabular-nums", children: value })] }), source === undefined ? null : _jsx(AssistSourceLink, { source: source })] }), _jsxs("span", { className: "flex shrink-0 items-center gap-0.5", children: [_jsx(Button, { role: "secondary", size: "sm", icon: Check, onClick: () => {
|
|
49
|
+
applyWithUndo(onAccept, toasts, acceptedMessage(copy, field), copy.assist.undo);
|
|
50
|
+
refocus();
|
|
51
|
+
}, children: copy.assist.accept }), _jsx(IconButton, { icon: X, label: copy.assist.dismiss, onClick: () => {
|
|
52
|
+
onDismiss();
|
|
53
|
+
refocus();
|
|
54
|
+
} })] })] }));
|
|
55
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { type AssistApply, type AssistSource, type ProposedChange } from "./assist.js";
|
|
2
|
+
import { type DialogContext } from "../../primitives/dialog/Dialog.js";
|
|
3
|
+
import { type ReactNode } from "react";
|
|
4
|
+
type GroupProps = {
|
|
5
|
+
/** «Campos», «Líneas». Without it the changes are listed without a heading. */
|
|
6
|
+
title?: string;
|
|
7
|
+
children?: ReactNode;
|
|
8
|
+
};
|
|
9
|
+
type ReviewProps = {
|
|
10
|
+
/** What the proposal is, as a heading. The copy's «Cambios propuestos» when absent. */
|
|
11
|
+
title?: string;
|
|
12
|
+
/** One sentence under the title: what the assistant read and what it would do. */
|
|
13
|
+
summary?: string;
|
|
14
|
+
/** Where the whole proposal comes from. A change may carry its own. */
|
|
15
|
+
source?: AssistSource;
|
|
16
|
+
/** Applies the chosen changes — their ids, in the listed order. Return the function that
|
|
17
|
+
* reverts them, and the screen gets «Deshacer». */
|
|
18
|
+
onApply: AssistApply<[ids: string[]]>;
|
|
19
|
+
/** Rejects the proposal. */
|
|
20
|
+
onDiscard: () => void;
|
|
21
|
+
/** The chosen ids, controlled. Uncontrolled from «all but the `unchosen` ones». */
|
|
22
|
+
chosen?: readonly string[];
|
|
23
|
+
onChosenChange?: (ids: string[]) => void;
|
|
24
|
+
/** `ChangeReview.Group` and `ChangeReview.Change`. */
|
|
25
|
+
children?: ReactNode;
|
|
26
|
+
};
|
|
27
|
+
export type ChangeReviewProps = ReviewProps & {
|
|
28
|
+
/** The role of «Aplicar». `primary` by default; `secondary` inside a record whose rail
|
|
29
|
+
* already shows the screen's one primary. */
|
|
30
|
+
emphasis?: "primary" | "secondary";
|
|
31
|
+
};
|
|
32
|
+
/** The review inside the page: a dashed card in the record it would change. */
|
|
33
|
+
export declare function ChangeReview(props: ChangeReviewProps): import("react").JSX.Element;
|
|
34
|
+
export declare namespace ChangeReview {
|
|
35
|
+
var Group: ((props: GroupProps) => ReactNode) & import("../../foundations/slots.js").Slotted<"group">;
|
|
36
|
+
var Change: ((props: ProposedChange) => ReactNode) & import("../../foundations/slots.js").Slotted<"change">;
|
|
37
|
+
}
|
|
38
|
+
export type ChangeReviewDialogProps = ReviewProps & {
|
|
39
|
+
open: boolean;
|
|
40
|
+
/** Every way out ends here: ×, Escape, the scrim — «later», the proposal stays — and
|
|
41
|
+
* also after «Aplicar» and «Descartar». */
|
|
42
|
+
onClose: () => void;
|
|
43
|
+
/** What the dialog is about, in its header: «factura · FAC/2026/0120». */
|
|
44
|
+
context?: DialogContext;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* The same review, in a `Dialog`: opened on demand — from a notice that says «el asistente
|
|
48
|
+
* encontró la orden de compra» — and a bottom sheet on a phone. «Aplicar» is the dialog's
|
|
49
|
+
* primary; closing without choosing keeps the proposal for later.
|
|
50
|
+
*/
|
|
51
|
+
export declare function ChangeReviewDialog(props: ChangeReviewDialogProps): import("react").JSX.Element;
|
|
52
|
+
export declare namespace ChangeReviewDialog {
|
|
53
|
+
var Group: ((props: GroupProps) => ReactNode) & import("../../foundations/slots.js").Slotted<"group">;
|
|
54
|
+
var Change: ((props: ProposedChange) => ReactNode) & import("../../foundations/slots.js").Slotted<"change">;
|
|
55
|
+
}
|
|
56
|
+
export {};
|