@sincpro/web-ui 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +184 -2
- package/dist/components/activity/Activity.d.ts +69 -0
- package/dist/components/activity/Activity.js +279 -0
- package/dist/components/activity/Messages.d.ts +66 -0
- package/dist/components/activity/Messages.js +46 -0
- package/dist/components/activity/Timeline.d.ts +21 -0
- package/dist/components/activity/Timeline.js +30 -0
- package/dist/components/activity/index.d.ts +3 -0
- package/dist/components/activity/index.js +4 -0
- package/dist/components/assist/AssistMark.d.ts +64 -0
- package/dist/components/assist/AssistMark.js +108 -0
- package/dist/components/assist/AssistPending.d.ts +24 -0
- package/dist/components/assist/AssistPending.js +26 -0
- package/dist/components/assist/AssistSuggestion.d.ts +42 -0
- package/dist/components/assist/AssistSuggestion.js +55 -0
- package/dist/components/assist/ChangeReview.d.ts +56 -0
- package/dist/components/assist/ChangeReview.js +121 -0
- package/dist/components/assist/assist.d.ts +77 -0
- package/dist/components/assist/assist.js +87 -0
- package/dist/components/assist/index.d.ts +5 -0
- package/dist/components/assist/index.js +7 -0
- package/dist/components/form/Form.d.ts +52 -0
- package/dist/components/form/Form.js +49 -0
- package/dist/components/form/Sheet.d.ts +47 -0
- package/dist/components/form/Sheet.js +90 -0
- package/dist/components/form/index.d.ts +2 -0
- package/dist/components/form/index.js +4 -0
- package/dist/components/frame/Chart.d.ts +13 -0
- package/dist/components/frame/Chart.js +24 -0
- package/dist/components/frame/FrameView.d.ts +33 -0
- package/dist/components/frame/FrameView.js +125 -0
- package/dist/components/frame/index.d.ts +3 -0
- package/dist/components/frame/index.js +3 -0
- package/dist/components/frame/model.d.ts +43 -0
- package/dist/components/frame/model.js +101 -0
- package/dist/components/index.d.ts +10 -0
- package/dist/components/index.js +11 -0
- package/dist/components/lines/LineItems.d.ts +111 -0
- package/dist/components/lines/LineItems.js +317 -0
- package/dist/components/lines/index.d.ts +1 -0
- package/dist/components/lines/index.js +2 -0
- package/dist/components/lines/lines.d.ts +23 -0
- package/dist/components/lines/lines.js +26 -0
- package/dist/components/listing/ColumnsMenu.d.ts +20 -0
- package/dist/components/listing/ColumnsMenu.js +46 -0
- package/dist/components/listing/GroupingLevels.d.ts +35 -0
- package/dist/components/listing/GroupingLevels.js +45 -0
- package/dist/components/listing/Listing.d.ts +27 -0
- package/dist/components/listing/Listing.js +86 -0
- package/dist/components/listing/ListingBar.d.ts +112 -0
- package/dist/components/listing/ListingBar.js +144 -0
- package/dist/components/listing/Pagination.d.ts +36 -0
- package/dist/components/listing/Pagination.js +69 -0
- package/dist/components/listing/RecordCard.d.ts +33 -0
- package/dist/components/listing/RecordCard.js +22 -0
- package/dist/components/listing/RecordLink.d.ts +7 -0
- package/dist/components/listing/RecordLink.js +22 -0
- package/dist/components/listing/Row.d.ts +110 -0
- package/dist/components/listing/Row.js +123 -0
- package/dist/components/listing/SortMenu.d.ts +62 -0
- package/dist/components/listing/SortMenu.js +105 -0
- package/dist/components/listing/ViewSwitcher.d.ts +24 -0
- package/dist/components/listing/ViewSwitcher.js +16 -0
- package/dist/components/listing/columns.d.ts +47 -0
- package/dist/components/listing/columns.js +72 -0
- package/dist/components/listing/grouping.d.ts +35 -0
- package/dist/components/listing/grouping.js +47 -0
- package/dist/components/listing/index.d.ts +17 -0
- package/dist/components/listing/index.js +20 -0
- package/dist/components/listing/modelColumns.d.ts +50 -0
- package/dist/components/listing/modelColumns.js +103 -0
- package/dist/components/listing/pages.d.ts +78 -0
- package/dist/components/listing/pages.js +135 -0
- package/dist/components/listing/selection.d.ts +21 -0
- package/dist/components/listing/selection.js +43 -0
- package/dist/components/listing/set.d.ts +48 -0
- package/dist/components/listing/set.js +24 -0
- package/dist/components/listing/tree.d.ts +28 -0
- package/dist/components/listing/tree.js +23 -0
- package/dist/components/navigation/CommandPalette.d.ts +40 -0
- package/dist/components/navigation/CommandPalette.js +114 -0
- package/dist/components/navigation/NavigationTree.d.ts +31 -0
- package/dist/components/navigation/NavigationTree.js +79 -0
- package/dist/components/navigation/commands.d.ts +30 -0
- package/dist/components/navigation/commands.js +30 -0
- package/dist/components/navigation/index.d.ts +3 -0
- package/dist/components/navigation/index.js +4 -0
- package/dist/components/record/Breadcrumb.d.ts +42 -0
- package/dist/components/record/Breadcrumb.js +98 -0
- package/dist/components/record/DirtyActions.d.ts +26 -0
- package/dist/components/record/DirtyActions.js +22 -0
- package/dist/components/record/Pager.d.ts +18 -0
- package/dist/components/record/Pager.js +33 -0
- package/dist/components/record/RecordHeader.d.ts +75 -0
- package/dist/components/record/RecordHeader.js +104 -0
- package/dist/components/record/RecordNotices.d.ts +37 -0
- package/dist/components/record/RecordNotices.js +75 -0
- package/dist/components/record/RecordSkeleton.d.ts +3 -0
- package/dist/components/record/RecordSkeleton.js +21 -0
- package/dist/components/record/SmartButtons.d.ts +35 -0
- package/dist/components/record/SmartButtons.js +38 -0
- package/dist/components/record/StatusBar.d.ts +51 -0
- package/dist/components/record/StatusBar.js +168 -0
- package/dist/components/record/ThumbBar.d.ts +36 -0
- package/dist/components/record/ThumbBar.js +86 -0
- package/dist/components/record/index.d.ts +6 -0
- package/dist/components/record/index.js +8 -0
- package/dist/components/search/CriteriaBuilder.d.ts +58 -0
- package/dist/components/search/CriteriaBuilder.js +278 -0
- package/dist/components/search/CriteriaChips.d.ts +22 -0
- package/dist/components/search/CriteriaChips.js +64 -0
- package/dist/components/search/FilterBuilder.d.ts +142 -0
- package/dist/components/search/FilterBuilder.js +421 -0
- package/dist/components/search/JoinWord.d.ts +8 -0
- package/dist/components/search/JoinWord.js +47 -0
- package/dist/components/search/JsonEditor.d.ts +19 -0
- package/dist/components/search/JsonEditor.js +78 -0
- package/dist/components/search/SearchBar.d.ts +78 -0
- package/dist/components/search/SearchBar.js +387 -0
- package/dist/components/search/criteria.d.ts +116 -0
- package/dist/components/search/criteria.js +159 -0
- package/dist/components/search/datePeriods.d.ts +122 -0
- package/dist/components/search/datePeriods.js +134 -0
- package/dist/components/search/dateRanges.d.ts +54 -0
- package/dist/components/search/dateRanges.js +89 -0
- package/dist/components/search/index.d.ts +9 -0
- package/dist/components/search/index.js +20 -0
- package/dist/components/search/model.d.ts +54 -0
- package/dist/components/search/model.js +37 -0
- package/dist/components/search/query.d.ts +166 -0
- package/dist/components/search/query.js +436 -0
- package/dist/components/shell/AppFrame.d.ts +14 -0
- package/dist/components/shell/AppFrame.js +80 -0
- package/dist/components/shell/TopBar.d.ts +73 -0
- package/dist/components/shell/TopBar.js +54 -0
- package/dist/components/shell/headerSlot.d.ts +17 -0
- package/dist/components/shell/headerSlot.js +21 -0
- package/dist/components/shell/index.d.ts +2 -0
- package/dist/components/shell/index.js +3 -0
- package/dist/foundations/aria.d.ts +14 -0
- package/dist/foundations/aria.js +18 -0
- package/dist/foundations/calendar.d.ts +28 -0
- package/dist/foundations/calendar.js +183 -0
- package/dist/foundations/copy.d.ts +703 -0
- package/dist/foundations/copy.es.d.ts +2 -0
- package/dist/foundations/copy.es.js +430 -0
- package/dist/foundations/copy.js +520 -0
- package/dist/foundations/density.d.ts +16 -0
- package/dist/foundations/density.js +21 -0
- package/dist/foundations/grouping.d.ts +281 -0
- package/dist/foundations/grouping.js +0 -0
- package/dist/foundations/index.d.ts +14 -0
- package/dist/foundations/index.js +30 -0
- package/dist/foundations/layout.d.ts +9 -0
- package/dist/foundations/layout.js +9 -0
- package/dist/foundations/pivot.d.ts +222 -0
- package/dist/foundations/pivot.js +198 -0
- package/dist/foundations/pivotSheet.d.ts +61 -0
- package/dist/foundations/pivotSheet.js +0 -0
- package/dist/foundations/slots.d.ts +67 -0
- package/dist/foundations/slots.js +63 -0
- package/dist/foundations/sticky.d.ts +53 -0
- package/dist/foundations/sticky.js +150 -0
- package/dist/foundations/surface.d.ts +9 -0
- package/dist/foundations/surface.js +26 -0
- package/dist/foundations/theme.d.ts +42 -0
- package/dist/foundations/theme.js +88 -0
- package/dist/foundations/types.d.ts +23 -0
- package/dist/foundations/types.js +50 -0
- package/dist/foundations/variants.d.ts +19 -0
- package/dist/foundations/variants.js +48 -0
- package/dist/foundations/xlsx.d.ts +86 -0
- package/dist/foundations/xlsx.js +0 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +19 -0
- package/dist/primitives/block/Block.d.ts +66 -0
- package/dist/primitives/block/Block.js +34 -0
- package/dist/primitives/block/Line.d.ts +18 -0
- package/dist/primitives/block/Line.js +45 -0
- package/dist/primitives/block/SectionGrid.d.ts +30 -0
- package/dist/primitives/block/SectionGrid.js +29 -0
- package/dist/primitives/block/Steps.d.ts +76 -0
- package/dist/primitives/block/Steps.js +43 -0
- package/dist/primitives/block/index.d.ts +4 -0
- package/dist/primitives/block/index.js +7 -0
- package/dist/primitives/button/Button.d.ts +137 -0
- package/dist/primitives/button/Button.js +234 -0
- package/dist/primitives/button/index.d.ts +1 -0
- package/dist/primitives/button/index.js +2 -0
- package/dist/primitives/cell/CellStack.d.ts +47 -0
- package/dist/primitives/cell/CellStack.js +33 -0
- package/dist/primitives/cell/index.d.ts +1 -0
- package/dist/primitives/cell/index.js +3 -0
- package/dist/primitives/choice/ComboBox.d.ts +19 -0
- package/dist/primitives/choice/ComboBox.js +52 -0
- package/dist/primitives/choice/RadioGroup.d.ts +18 -0
- package/dist/primitives/choice/RadioGroup.js +50 -0
- package/dist/primitives/choice/SearchField.d.ts +33 -0
- package/dist/primitives/choice/SearchField.js +46 -0
- package/dist/primitives/choice/SegmentedControl.d.ts +16 -0
- package/dist/primitives/choice/SegmentedControl.js +69 -0
- package/dist/primitives/choice/Slider.d.ts +16 -0
- package/dist/primitives/choice/Slider.js +46 -0
- package/dist/primitives/choice/Switch.d.ts +14 -0
- package/dist/primitives/choice/Switch.js +65 -0
- package/dist/primitives/choice/ThemeSwitcher.d.ts +10 -0
- package/dist/primitives/choice/ThemeSwitcher.js +25 -0
- package/dist/primitives/choice/index.d.ts +8 -0
- package/dist/primitives/choice/index.js +11 -0
- package/dist/primitives/choice/options.d.ts +28 -0
- package/dist/primitives/choice/options.js +6 -0
- package/dist/primitives/date/Calendar.d.ts +31 -0
- package/dist/primitives/date/Calendar.js +134 -0
- package/dist/primitives/date/DatePicker.d.ts +42 -0
- package/dist/primitives/date/DatePicker.js +103 -0
- package/dist/primitives/date/DateTime.d.ts +35 -0
- package/dist/primitives/date/DateTime.js +100 -0
- package/dist/primitives/date/index.d.ts +3 -0
- package/dist/primitives/date/index.js +6 -0
- package/dist/primitives/dialog/Dialog.d.ts +118 -0
- package/dist/primitives/dialog/Dialog.js +121 -0
- package/dist/primitives/dialog/Drill.d.ts +90 -0
- package/dist/primitives/dialog/Drill.js +172 -0
- package/dist/primitives/dialog/index.d.ts +2 -0
- package/dist/primitives/dialog/index.js +4 -0
- package/dist/primitives/disclosure/Disclosure.d.ts +36 -0
- package/dist/primitives/disclosure/Disclosure.js +52 -0
- package/dist/primitives/disclosure/index.d.ts +1 -0
- package/dist/primitives/disclosure/index.js +2 -0
- package/dist/primitives/field/Checks.d.ts +57 -0
- package/dist/primitives/field/Checks.js +72 -0
- package/dist/primitives/field/DropZone.d.ts +42 -0
- package/dist/primitives/field/DropZone.js +52 -0
- package/dist/primitives/field/Field.d.ts +178 -0
- package/dist/primitives/field/Field.js +251 -0
- package/dist/primitives/field/Suggest.d.ts +48 -0
- package/dist/primitives/field/Suggest.js +43 -0
- package/dist/primitives/field/aria.d.ts +61 -0
- package/dist/primitives/field/aria.js +70 -0
- package/dist/primitives/field/context.d.ts +30 -0
- package/dist/primitives/field/context.js +13 -0
- package/dist/primitives/field/copy.d.ts +16 -0
- package/dist/primitives/field/copy.js +69 -0
- package/dist/primitives/field/index.d.ts +9 -0
- package/dist/primitives/field/index.js +15 -0
- package/dist/primitives/field/layout.d.ts +24 -0
- package/dist/primitives/field/layout.js +10 -0
- package/dist/primitives/field/mode.d.ts +9 -0
- package/dist/primitives/field/mode.js +13 -0
- package/dist/primitives/field/note.d.ts +55 -0
- package/dist/primitives/field/note.js +117 -0
- package/dist/primitives/hint/Hint.d.ts +31 -0
- package/dist/primitives/hint/Hint.js +42 -0
- package/dist/primitives/hint/index.d.ts +1 -0
- package/dist/primitives/hint/index.js +1 -0
- package/dist/primitives/index.d.ts +20 -0
- package/dist/primitives/index.js +21 -0
- package/dist/primitives/menu/Menu.d.ts +128 -0
- package/dist/primitives/menu/Menu.js +89 -0
- package/dist/primitives/menu/index.d.ts +1 -0
- package/dist/primitives/menu/index.js +2 -0
- package/dist/primitives/notice/Callout.d.ts +49 -0
- package/dist/primitives/notice/Callout.js +28 -0
- package/dist/primitives/notice/Flag.d.ts +13 -0
- package/dist/primitives/notice/Flag.js +31 -0
- package/dist/primitives/notice/Notice.d.ts +45 -0
- package/dist/primitives/notice/Notice.js +61 -0
- package/dist/primitives/notice/Toast.d.ts +55 -0
- package/dist/primitives/notice/Toast.js +142 -0
- package/dist/primitives/notice/index.d.ts +4 -0
- package/dist/primitives/notice/index.js +6 -0
- package/dist/primitives/overlay/Popover.d.ts +6 -0
- package/dist/primitives/overlay/Popover.js +27 -0
- package/dist/primitives/overlay/PopoverTrigger.d.ts +55 -0
- package/dist/primitives/overlay/PopoverTrigger.js +38 -0
- package/dist/primitives/overlay/Preview.d.ts +19 -0
- package/dist/primitives/overlay/Preview.js +60 -0
- package/dist/primitives/overlay/Tooltip.d.ts +38 -0
- package/dist/primitives/overlay/Tooltip.js +74 -0
- package/dist/primitives/overlay/index.d.ts +4 -0
- package/dist/primitives/overlay/index.js +7 -0
- package/dist/primitives/picker/Picker.d.ts +89 -0
- package/dist/primitives/picker/Picker.js +204 -0
- package/dist/primitives/picker/index.d.ts +1 -0
- package/dist/primitives/picker/index.js +2 -0
- package/dist/primitives/surface/Peek.d.ts +60 -0
- package/dist/primitives/surface/Peek.js +71 -0
- package/dist/primitives/surface/index.d.ts +1 -0
- package/dist/primitives/surface/index.js +2 -0
- package/dist/primitives/table/CellCopy.d.ts +20 -0
- package/dist/primitives/table/CellCopy.js +23 -0
- package/dist/primitives/table/CellEditor.d.ts +14 -0
- package/dist/primitives/table/CellEditor.js +83 -0
- package/dist/primitives/table/DataTable.d.ts +189 -0
- package/dist/primitives/table/DataTable.js +330 -0
- package/dist/primitives/table/EditableTable.d.ts +191 -0
- package/dist/primitives/table/EditableTable.js +617 -0
- package/dist/primitives/table/PivotTable.d.ts +14 -0
- package/dist/primitives/table/PivotTable.js +0 -0
- package/dist/primitives/table/ReportTable.d.ts +38 -0
- package/dist/primitives/table/ReportTable.js +106 -0
- package/dist/primitives/table/RowActions.d.ts +16 -0
- package/dist/primitives/table/RowActions.js +28 -0
- package/dist/primitives/table/SheetTable.d.ts +73 -0
- package/dist/primitives/table/SheetTable.js +230 -0
- package/dist/primitives/table/Table.d.ts +106 -0
- package/dist/primitives/table/Table.js +116 -0
- package/dist/primitives/table/index.d.ts +7 -0
- package/dist/primitives/table/index.js +9 -0
- package/dist/primitives/tabs/Tabs.d.ts +91 -0
- package/dist/primitives/tabs/Tabs.js +56 -0
- package/dist/primitives/tabs/index.d.ts +1 -0
- package/dist/primitives/tabs/index.js +1 -0
- package/dist/primitives/tag/Tag.d.ts +36 -0
- package/dist/primitives/tag/Tag.js +68 -0
- package/dist/primitives/tag/Tags.d.ts +57 -0
- package/dist/primitives/tag/Tags.js +104 -0
- package/dist/primitives/tag/TagsField.d.ts +35 -0
- package/dist/primitives/tag/TagsField.js +186 -0
- package/dist/primitives/tag/choices.d.ts +38 -0
- package/dist/primitives/tag/choices.js +26 -0
- package/dist/primitives/tag/index.d.ts +4 -0
- package/dist/primitives/tag/index.js +8 -0
- package/dist/primitives/toolbar/Toolbar.d.ts +13 -0
- package/dist/primitives/toolbar/Toolbar.js +36 -0
- package/dist/primitives/toolbar/index.d.ts +1 -0
- package/dist/primitives/toolbar/index.js +2 -0
- package/dist/primitives/tree/Tree.d.ts +56 -0
- package/dist/primitives/tree/Tree.js +74 -0
- package/dist/primitives/tree/index.d.ts +1 -0
- package/dist/primitives/tree/index.js +3 -0
- package/dist/primitives/value/Copyable.d.ts +48 -0
- package/dist/primitives/value/Copyable.js +71 -0
- package/dist/primitives/value/Fecha.d.ts +12 -0
- package/dist/primitives/value/Fecha.js +89 -0
- package/dist/primitives/value/Meter.d.ts +29 -0
- package/dist/primitives/value/Meter.js +46 -0
- package/dist/primitives/value/Metric.d.ts +42 -0
- package/dist/primitives/value/Metric.js +38 -0
- package/dist/primitives/value/Person.d.ts +90 -0
- package/dist/primitives/value/Person.js +208 -0
- package/dist/primitives/value/ProgressBar.d.ts +31 -0
- package/dist/primitives/value/ProgressBar.js +53 -0
- package/dist/primitives/value/Relation.d.ts +74 -0
- package/dist/primitives/value/Relation.js +74 -0
- package/dist/primitives/value/Totals.d.ts +121 -0
- package/dist/primitives/value/Totals.js +54 -0
- package/dist/primitives/value/Value.d.ts +40 -0
- package/dist/primitives/value/Value.js +80 -0
- package/dist/primitives/value/index.d.ts +9 -0
- package/dist/primitives/value/index.js +10 -0
- package/dist/tokens.css +1062 -0
- package/dist/views/index.d.ts +2 -0
- package/dist/views/index.js +3 -0
- package/dist/views/list/List.d.ts +119 -0
- package/dist/views/list/List.js +101 -0
- package/dist/views/list/ListView.d.ts +6 -0
- package/dist/views/list/ListView.js +295 -0
- package/dist/views/list/columns.d.ts +18 -0
- package/dist/views/list/columns.js +27 -0
- package/dist/views/list/derive.d.ts +87 -0
- package/dist/views/list/derive.js +207 -0
- package/dist/views/list/index.d.ts +2 -0
- package/dist/views/list/index.js +4 -0
- package/dist/views/list/presentations.d.ts +39 -0
- package/dist/views/list/presentations.js +105 -0
- package/dist/views/list/types.d.ts +284 -0
- package/dist/views/list/types.js +1 -0
- package/dist/views/list/useNarrowList.d.ts +17 -0
- package/dist/views/list/useNarrowList.js +32 -0
- package/dist/views/record/Record.d.ts +145 -0
- package/dist/views/record/Record.js +211 -0
- package/dist/views/record/index.d.ts +1 -0
- package/dist/views/record/index.js +2 -0
- package/package.json +113 -4
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { currentCopy, currentCopyLocale, useUiCopy } from "../../foundations/copy.js";
|
|
4
|
+
/* ==================================================================== *
|
|
5
|
+
* WHEN — a date says two things and they can't both be said together.
|
|
6
|
+
*
|
|
7
|
+
* «2 min ago» how recent it is. Understood without thinking.
|
|
8
|
+
* «14/09/2026 06:21» which one it is. It gets compared, copied, cited.
|
|
9
|
+
*
|
|
10
|
+
* The first is the one that helps in a list —what is being asked is «is this
|
|
11
|
+
* from now?»— and the second the one that helps when something has to be done
|
|
12
|
+
* with the datum. Writing both spends twice the width to answer a single
|
|
13
|
+
* question, so the relative one is drawn and the exact one goes in the `title`:
|
|
14
|
+
* a hover away, taking up nothing.
|
|
15
|
+
*
|
|
16
|
+
* ── Why there is no «4 months ago» ──────────────────────────────────
|
|
17
|
+
*
|
|
18
|
+
* Past the week, the relative stops informing: «4 months ago» doesn't let you
|
|
19
|
+
* order two things or remember which one it was. From there on the date is
|
|
20
|
+
* drawn, which is what at that distance is really being asked.
|
|
21
|
+
* ==================================================================== */
|
|
22
|
+
function relativeFormat(locale) {
|
|
23
|
+
return new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
|
|
24
|
+
}
|
|
25
|
+
function dateFormat(locale) {
|
|
26
|
+
return new Intl.DateTimeFormat(locale, {
|
|
27
|
+
day: "2-digit",
|
|
28
|
+
month: "2-digit",
|
|
29
|
+
year: "numeric",
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function timeFormat(locale) {
|
|
33
|
+
return new Intl.DateTimeFormat(locale, {
|
|
34
|
+
hour: "2-digit",
|
|
35
|
+
minute: "2-digit",
|
|
36
|
+
hourCycle: "h23",
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
const MINUTE = 60_000;
|
|
40
|
+
const HOUR = 60 * MINUTE;
|
|
41
|
+
const DAY = 24 * HOUR;
|
|
42
|
+
/** The steps, from finest to coarsest. The first one that fits, wins. */
|
|
43
|
+
const STEPS = [
|
|
44
|
+
[MINUTE, "minute"],
|
|
45
|
+
[HOUR, "hour"],
|
|
46
|
+
[DAY, "day"],
|
|
47
|
+
];
|
|
48
|
+
export function formatRelative(when, now = new Date()) {
|
|
49
|
+
const locale = currentCopyLocale();
|
|
50
|
+
const delta = when.getTime() - now.getTime();
|
|
51
|
+
const abs = Math.abs(delta);
|
|
52
|
+
if (abs < MINUTE)
|
|
53
|
+
return currentCopy().time.justNow;
|
|
54
|
+
if (abs > 7 * DAY)
|
|
55
|
+
return dateFormat(locale).format(when);
|
|
56
|
+
const step = STEPS.find(([ms]) => abs < ms * 60) ?? STEPS[STEPS.length - 1];
|
|
57
|
+
const [ms, unit] = step;
|
|
58
|
+
return relativeFormat(locale).format(Math.round(delta / ms), unit);
|
|
59
|
+
}
|
|
60
|
+
export function formatDateTime(when) {
|
|
61
|
+
const locale = currentCopyLocale();
|
|
62
|
+
return `${dateFormat(locale).format(when)} ${timeFormat(locale).format(when)}`;
|
|
63
|
+
}
|
|
64
|
+
/** A day written as `YYYY-MM-DD` (what a date field stores) is that day HERE. `new Date()`
|
|
65
|
+
* reads it as midnight in UTC, and west of Greenwich that is the evening before: a due date
|
|
66
|
+
* of the 15th was drawn as the 14th in La Paz. Anything else goes through `Date` as is. */
|
|
67
|
+
function toDate(when) {
|
|
68
|
+
const day = typeof when === "string" ? /^(\d{4})-(\d{2})-(\d{2})$/.exec(when) : null;
|
|
69
|
+
if (day === null)
|
|
70
|
+
return new Date(when);
|
|
71
|
+
return new Date(Number(day[1]), Number(day[2]) - 1, Number(day[3]));
|
|
72
|
+
}
|
|
73
|
+
export function DateValue({ when, format = "relative",
|
|
74
|
+
/** So it can be tested: without this, a story with «hace 2 min» changes its
|
|
75
|
+
* text every minute and no snapshot works as a reference. */
|
|
76
|
+
now, }) {
|
|
77
|
+
const copy = useUiCopy();
|
|
78
|
+
const d = when instanceof Date ? when : toDate(when);
|
|
79
|
+
if (Number.isNaN(d.getTime())) {
|
|
80
|
+
return (_jsx("span", { className: "text-meta text-muted italic", title: copy.time.invalid, children: copy.time.noDate }));
|
|
81
|
+
}
|
|
82
|
+
const exact = formatDateTime(d);
|
|
83
|
+
const text = format === "exact"
|
|
84
|
+
? exact
|
|
85
|
+
: format === "date"
|
|
86
|
+
? dateFormat(copy.locale).format(d)
|
|
87
|
+
: formatRelative(d, now);
|
|
88
|
+
return (_jsx("time", { dateTime: d.toISOString(), title: exact, className: "text-meta whitespace-nowrap tabular-nums text-muted", children: text }));
|
|
89
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export type MeterLevel = "good" | "warn" | "danger";
|
|
3
|
+
/** Percentages of the range where the color changes. */
|
|
4
|
+
export type MeterThresholds = {
|
|
5
|
+
warn: number;
|
|
6
|
+
danger: number;
|
|
7
|
+
};
|
|
8
|
+
/** The level of a percentage: under `warn` is good, up to `danger` is warn, above is danger. */
|
|
9
|
+
export declare function meterLevel(percentage: number, thresholds?: MeterThresholds): MeterLevel;
|
|
10
|
+
export type MeterProps = {
|
|
11
|
+
/** What is measured: «Crédito usado», «Stock». */
|
|
12
|
+
label: string;
|
|
13
|
+
value: number;
|
|
14
|
+
minValue?: number;
|
|
15
|
+
/** The capacity. 100 by default, so a plain percentage needs nothing else. */
|
|
16
|
+
maxValue?: number;
|
|
17
|
+
/** How the figure is written. Absent: the percentage of the range. */
|
|
18
|
+
formatOptions?: Intl.NumberFormatOptions;
|
|
19
|
+
/** The figure written by hand, when a single formatted number does not say it:
|
|
20
|
+
* «Bs 42.500 de 50.000». It is also what a screen reader reads. */
|
|
21
|
+
valueLabel?: ReactNode;
|
|
22
|
+
/** Where the color changes, in percent of the range. */
|
|
23
|
+
thresholds?: MeterThresholds;
|
|
24
|
+
/** Off when the figure is already beside it — a cell with its column. */
|
|
25
|
+
showValue?: boolean;
|
|
26
|
+
/** The width of the track. A meter in a table cell is shorter than one in a card. */
|
|
27
|
+
width?: string;
|
|
28
|
+
};
|
|
29
|
+
export declare function Meter({ label, value, minValue, maxValue, formatOptions, valueLabel, thresholds, showValue, width, }: MeterProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { AriaLocale } from "../../foundations/aria.js";
|
|
4
|
+
import { tv } from "../../foundations/variants.js";
|
|
5
|
+
import { Meter as AriaMeter } from "react-aria-components";
|
|
6
|
+
/**
|
|
7
|
+
* Where a quantity stands against a known range: the credit used, the stock against the
|
|
8
|
+
* capacity of the warehouse, the budget spent.
|
|
9
|
+
*
|
|
10
|
+
* Context: it looks like `ProgressBar` and it is the opposite thing. A progress bar is a
|
|
11
|
+
* TASK advancing toward done — an import, a stock count — and full is good news. A meter is a
|
|
12
|
+
* MEASUREMENT that just is, and full is often the alarm: a credit line at 95 % is a client
|
|
13
|
+
* who cannot buy tomorrow. That is why its color is not a tone the consumer picks but the
|
|
14
|
+
* reading of the value — `good` under 70 %, `warn` from 70 to 90, `danger` above — so the
|
|
15
|
+
* one that needs a look is the red one in a column of thirty. The thresholds move for the
|
|
16
|
+
* measure where full is fine (a warehouse meant to run at 95 %).
|
|
17
|
+
*
|
|
18
|
+
* React Aria writes `role="meter"` and the `aria-value*`; the visible figure is formatted by
|
|
19
|
+
* React Aria in the locale of the copy table (`AriaLocale`), in tabular figures so a column
|
|
20
|
+
* of meters keeps its digits aligned.
|
|
21
|
+
*/
|
|
22
|
+
const fill = tv({
|
|
23
|
+
base: "block h-full rounded-pill transition-[width] duration-(--duration-panel) ease-(--ease-out) forced-colors:bg-[Highlight]",
|
|
24
|
+
variants: {
|
|
25
|
+
level: {
|
|
26
|
+
good: "bg-good",
|
|
27
|
+
warn: "bg-warn",
|
|
28
|
+
danger: "bg-danger",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
const DEFAULT_THRESHOLDS = { warn: 70, danger: 90 };
|
|
33
|
+
/** The level of a percentage: under `warn` is good, up to `danger` is warn, above is danger. */
|
|
34
|
+
export function meterLevel(percentage, thresholds = DEFAULT_THRESHOLDS) {
|
|
35
|
+
if (percentage > thresholds.danger)
|
|
36
|
+
return "danger";
|
|
37
|
+
if (percentage >= thresholds.warn)
|
|
38
|
+
return "warn";
|
|
39
|
+
return "good";
|
|
40
|
+
}
|
|
41
|
+
export function Meter({ label, value, minValue = 0, maxValue = 100, formatOptions, valueLabel, thresholds = DEFAULT_THRESHOLDS, showValue = true, width = "w-64", }) {
|
|
42
|
+
return (_jsx(AriaLocale, { children: _jsx(AriaMeter, { "aria-label": label, value: value, minValue: minValue, maxValue: maxValue, formatOptions: formatOptions, valueLabel: valueLabel, className: `flex max-w-full flex-col gap-1.5 ${width}`, children: ({ percentage, valueText }) => {
|
|
43
|
+
const level = meterLevel(percentage, thresholds);
|
|
44
|
+
return (_jsxs(_Fragment, { children: [_jsxs("span", { className: "flex items-baseline justify-between gap-2", children: [_jsx("span", { className: "truncate text-meta text-ink", children: label }), showValue ? (_jsx("span", { "data-level": level, className: `shrink-0 text-meta tabular-nums ${level === "danger" ? "font-medium text-danger" : "text-muted"}`, children: valueText })) : null] }), _jsx("span", { className: "block h-(--size-track) overflow-hidden rounded-pill bg-ink/10 forced-colors:outline forced-colors:outline-[ButtonBorder]", children: _jsx("span", { "data-level": level, className: fill({ level }), style: { width: `${percentage}%` } }) })] }));
|
|
45
|
+
} }) }));
|
|
46
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Tone } from "../../foundations/types.js";
|
|
2
|
+
import type { LucideIcon } from "lucide-react";
|
|
3
|
+
/**
|
|
4
|
+
* A metric that also FILTERS: «2 por confirmar», «41 por entregar».
|
|
5
|
+
*
|
|
6
|
+
* Context: it is different from `KeyValue` and from a `ListingBar` counter, and
|
|
7
|
+
* the difference is that this one gets pressed. A number that leads somewhere has
|
|
8
|
+
* to look like something that gets pressed, or nobody discovers that the list can
|
|
9
|
+
* be narrowed from there — which is the most common case of «I didn't know you
|
|
10
|
+
* could do that».
|
|
11
|
+
*
|
|
12
|
+
* ── The color shows up when the narrowing is APPLIED ─────────────────
|
|
13
|
+
*
|
|
14
|
+
* At rest the cards are all the same: sheet background and a hair of border. The
|
|
15
|
+
* only tinted thing is the FIGURE, which is what the card is about. Four
|
|
16
|
+
* different color backgrounds on top of a list are not four filters, they are
|
|
17
|
+
* four stains competing with the rows that don't say which one is applied.
|
|
18
|
+
*
|
|
19
|
+
* The tone paints the background only when the metric is active. There the color
|
|
20
|
+
* does mean something —«I'm only seeing this»— and with a single card lit up
|
|
21
|
+
* there is nothing to compete with.
|
|
22
|
+
*
|
|
23
|
+
* The figure goes big on top and the label small underneath, the other way around
|
|
24
|
+
* from a smart button: here what gets compared at a glance across three cards is
|
|
25
|
+
* the NUMBER.
|
|
26
|
+
*/
|
|
27
|
+
export type MetricProps = {
|
|
28
|
+
value: number | string;
|
|
29
|
+
label: string;
|
|
30
|
+
tone?: Tone;
|
|
31
|
+
icon?: LucideIcon;
|
|
32
|
+
/** Present = narrows the list. Absent = only informs, and doesn't get pressed. */
|
|
33
|
+
onClick?: () => void;
|
|
34
|
+
/** This narrowing is applied. */
|
|
35
|
+
active?: boolean;
|
|
36
|
+
};
|
|
37
|
+
export declare function Metric({ value, label, tone, icon: Icon, onClick, active, }: MetricProps): import("react").JSX.Element;
|
|
38
|
+
/** The row of metrics. It wraps if they don't fit; it doesn't scroll: a hidden
|
|
39
|
+
* metric is a filter nobody finds. */
|
|
40
|
+
export declare function Metrics({ children }: {
|
|
41
|
+
children: React.ReactNode;
|
|
42
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useUiCopy } from "../../foundations/copy.js";
|
|
4
|
+
import { bySurface, useSurface } from "../../foundations/surface.js";
|
|
5
|
+
const TONE = {
|
|
6
|
+
neutral: { background: "bg-ink/6", text: "text-ink", edge: "border-ink/20" },
|
|
7
|
+
info: { background: "bg-info/10", text: "text-info", edge: "border-info/35" },
|
|
8
|
+
good: { background: "bg-good/10", text: "text-good", edge: "border-good/35" },
|
|
9
|
+
warn: { background: "bg-warn/12", text: "text-warn", edge: "border-warn/40" },
|
|
10
|
+
bad: { background: "bg-bad/10", text: "text-bad", edge: "border-bad/35" },
|
|
11
|
+
/* The gold can't be text over the canvas (2,43:1), so here the figure goes in
|
|
12
|
+
ink and the tone is only visible when the card is lit up. */
|
|
13
|
+
accent: { background: "bg-accent/20", text: "text-ink", edge: "border-accent/50" },
|
|
14
|
+
};
|
|
15
|
+
export function Metric({ value, label, tone = "neutral", icon: Icon, onClick, active = false, }) {
|
|
16
|
+
const copy = useUiCopy();
|
|
17
|
+
const sup = useSurface();
|
|
18
|
+
const t = TONE[tone];
|
|
19
|
+
/* At rest the box is the sheet with a hair of border: that is what makes four
|
|
20
|
+
metrics read as one row and not as four notices. Lit up it gets tinted, and
|
|
21
|
+
there the color says which one is applied. */
|
|
22
|
+
const skin = active
|
|
23
|
+
? `${t.background} ${t.edge}`
|
|
24
|
+
: bySurface(sup, "bg-surface border-line", "bg-chrome-ink/6 border-chrome-ink/15");
|
|
25
|
+
const box = `flex min-w-(--container-metric) shrink-0 flex-col justify-center gap-0.5 rounded-box border px-3.5 py-2 text-left transition-colors duration-(--duration-instant) ${skin} ${onClick === undefined
|
|
26
|
+
? ""
|
|
27
|
+
: "cursor-pointer hover:border-line-strong focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary"}`;
|
|
28
|
+
const content = (_jsxs(_Fragment, { children: [_jsxs("span", { className: `flex items-center gap-1.5 text-title font-semibold tabular-nums ${t.text}`, children: [Icon === undefined ? null : (_jsx(Icon, { className: "size-(--size-icon-md)", strokeWidth: 2, "aria-hidden": true })), typeof value === "number" ? value.toLocaleString(copy.locale) : value] }), _jsx("span", { className: `text-micro ${bySurface(sup, "text-muted", "text-chrome-muted")}`, children: label })] }));
|
|
29
|
+
if (onClick === undefined) {
|
|
30
|
+
return (_jsx("div", { className: box, "aria-label": `${value} ${label}`, children: content }));
|
|
31
|
+
}
|
|
32
|
+
return (_jsx("button", { type: "button", onClick: onClick, "aria-pressed": active, className: box, children: content }));
|
|
33
|
+
}
|
|
34
|
+
/** The row of metrics. It wraps if they don't fit; it doesn't scroll: a hidden
|
|
35
|
+
* metric is a filter nobody finds. */
|
|
36
|
+
export function Metrics({ children }) {
|
|
37
|
+
return _jsx("div", { className: "flex min-w-0 flex-wrap items-stretch gap-2", children: children });
|
|
38
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
declare const SIZE: {
|
|
3
|
+
readonly sm: "size-5 text-micro";
|
|
4
|
+
readonly md: "size-6 text-micro";
|
|
5
|
+
readonly lg: "size-8 text-meta";
|
|
6
|
+
readonly xl: "size-12 text-body";
|
|
7
|
+
};
|
|
8
|
+
export type AvatarSize = keyof typeof SIZE;
|
|
9
|
+
export declare function Avatar({ name, size,
|
|
10
|
+
/** The photo, if there is one. Without it the initials go, which is the normal
|
|
11
|
+
* case here. */
|
|
12
|
+
src, }: {
|
|
13
|
+
name: string;
|
|
14
|
+
size?: AvatarSize;
|
|
15
|
+
src?: string;
|
|
16
|
+
}): import("react").JSX.Element;
|
|
17
|
+
/** The ways to reach a person. Each one that is present becomes a row of the contact card. */
|
|
18
|
+
export type PersonContact = {
|
|
19
|
+
email?: string;
|
|
20
|
+
/** As people write it — «+591 7 123-4567». The `tel:` link keeps only the digits and `+`. */
|
|
21
|
+
phone?: string;
|
|
22
|
+
/** The WhatsApp number with its country code, written any way; `wa.me` gets the digits. */
|
|
23
|
+
whatsapp?: string;
|
|
24
|
+
};
|
|
25
|
+
export type ContactCardProps = {
|
|
26
|
+
name: string;
|
|
27
|
+
/** The photo. Without it, the initials — in the same color the person has everywhere. */
|
|
28
|
+
src?: string;
|
|
29
|
+
/** What the person is here: «Vendedora · Sucursal La Paz». */
|
|
30
|
+
role?: string;
|
|
31
|
+
contact?: PersonContact;
|
|
32
|
+
/** Go to the person's own record — «Ver ficha». Without it the card only reaches them. */
|
|
33
|
+
onOpen?: () => void;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Who a person is and how to reach them: the face, the name, the role, and one row per way
|
|
37
|
+
* of contact — write, call, WhatsApp — then their record.
|
|
38
|
+
*
|
|
39
|
+
* Context: in an ERP the question after «who is the salesperson of this invoice?» is almost
|
|
40
|
+
* always «how do I reach her?». Copying an email out of another screen to paste it into the
|
|
41
|
+
* mail client is the detour this card removes. Each way of contact is a ROW and not a bare
|
|
42
|
+
* icon: the address and the number are drawn, because often what the person wants is to read
|
|
43
|
+
* the number aloud, not to dial it from the browser. Every row is a real link (`mailto:`,
|
|
44
|
+
* `tel:`, `wa.me`), so middle-click, «copy address» and the phone's own dialer work.
|
|
45
|
+
*
|
|
46
|
+
* It is what opens from a `UserChip` with `contact`, and it is exported on its own for the
|
|
47
|
+
* side panel of a record, where the same person is shown without a chip to open.
|
|
48
|
+
*/
|
|
49
|
+
export declare function ContactCard({ name, src, role, contact, onOpen }: ContactCardProps): import("react").JSX.Element;
|
|
50
|
+
export type UserChipProps = {
|
|
51
|
+
name: string;
|
|
52
|
+
size?: AvatarSize;
|
|
53
|
+
src?: string;
|
|
54
|
+
/** What goes next to the name in the chip: a role, an email, «hace 2 min». */
|
|
55
|
+
detail?: ReactNode;
|
|
56
|
+
/** What the person is, shown in the contact card: «Vendedora · Sucursal La Paz». */
|
|
57
|
+
role?: string;
|
|
58
|
+
/** The ways to reach them. Present = the chip opens a contact card. */
|
|
59
|
+
contact?: PersonContact;
|
|
60
|
+
/** Go to the person's record. Present = the chip opens a card with «Ver ficha». */
|
|
61
|
+
onOpen?: () => void;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* The person with their name next to them. For a cell, a list row, a field in read mode or
|
|
65
|
+
* a fact of a record.
|
|
66
|
+
*
|
|
67
|
+
* Context: a chip is a button ONLY when it has somewhere to go. With `contact` or `onOpen` it
|
|
68
|
+
* opens the `ContactCard` (press, Enter or Space — React Aria's `DialogTrigger` through
|
|
69
|
+
* `PopoverTrigger`: focus goes into the card, Escape and a click outside close it, and focus
|
|
70
|
+
* comes back to the chip). Without them it stays text: a button that does nothing is a lie a
|
|
71
|
+
* keyboard user pays for with one more Tab stop on every row.
|
|
72
|
+
*
|
|
73
|
+
* It opens on PRESS and not on hover: the card holds links, and content that has to be
|
|
74
|
+
* reached with the pointer or the keyboard cannot disappear when the pointer leaves the name.
|
|
75
|
+
*/
|
|
76
|
+
export declare function UserChip({ name, size, src, detail, role, contact, onOpen, }: UserChipProps): import("react").JSX.Element;
|
|
77
|
+
/** Several people in little width: the widget of a to-many relation.
|
|
78
|
+
*
|
|
79
|
+
* Context: they overlap and get cut off at `max`. Five avatars in a row are five
|
|
80
|
+
* things; five overlapped with a «+3» are ONE thing that has eight inside, which
|
|
81
|
+
* is what the cell means to say.
|
|
82
|
+
*
|
|
83
|
+
* The `title` carries the whole list: whoever needs the names has them without
|
|
84
|
+
* the column paying for their width. */
|
|
85
|
+
export declare function UserStack({ names, max, size, }: {
|
|
86
|
+
names: readonly string[];
|
|
87
|
+
max?: number;
|
|
88
|
+
size?: AvatarSize;
|
|
89
|
+
}): import("react").JSX.Element;
|
|
90
|
+
export {};
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useUiCopy } from "../../foundations/copy.js";
|
|
4
|
+
import { useSurface } from "../../foundations/surface.js";
|
|
5
|
+
import { Button } from "../button/Button.js";
|
|
6
|
+
import { PopoverTrigger } from "../overlay/PopoverTrigger.js";
|
|
7
|
+
import { ArrowRight, Mail, MessageCircle, Phone } from "lucide-react";
|
|
8
|
+
/* ==================================================================== *
|
|
9
|
+
* WHO — the person as a datum, not as a string of text.
|
|
10
|
+
*
|
|
11
|
+
* «andres.g@sincpro.com.bo» in a cell is a correct and unreadable datum: twelve
|
|
12
|
+
* rows of emails get read letter by letter. An initial in a circle is
|
|
13
|
+
* recognized without reading, which is what is needed when the question is «who
|
|
14
|
+
* does this belong to?» and not «what is their email?».
|
|
15
|
+
*
|
|
16
|
+
* ── The color comes out of the name and nobody picks it ──────────────
|
|
17
|
+
*
|
|
18
|
+
* A hash of the name against the tone palette. It is deterministic —the same
|
|
19
|
+
* person always has the same color, on every screen— and that is why the color
|
|
20
|
+
* gets to mean something. If the consumer picked it, the same person would be
|
|
21
|
+
* green in one list and blue in another, and recognition without reading is lost
|
|
22
|
+
* entirely.
|
|
23
|
+
*
|
|
24
|
+
* The color here is NOT a state: it is identity. That is why it uses the tone
|
|
25
|
+
* palette but never `bad` nor `warn` — a person is not «in error».
|
|
26
|
+
* ==================================================================== */
|
|
27
|
+
const PALETTE = [
|
|
28
|
+
"bg-info/15 text-info",
|
|
29
|
+
"bg-good/15 text-good",
|
|
30
|
+
"bg-accent/25 text-ink",
|
|
31
|
+
"bg-primary/12 text-primary",
|
|
32
|
+
"bg-ink/10 text-ink",
|
|
33
|
+
];
|
|
34
|
+
/** The initials: two, and from the first two WORDS. «Ana María Gutiérrez» gives
|
|
35
|
+
* `AG` and not `AM` — the surname distinguishes more than the middle name. */
|
|
36
|
+
function initials(name) {
|
|
37
|
+
const parts = name
|
|
38
|
+
.trim()
|
|
39
|
+
.split(/\s+/)
|
|
40
|
+
.filter((p) => p.length > 0);
|
|
41
|
+
const first = parts[0]?.[0] ?? "?";
|
|
42
|
+
const last = parts.length > 1 ? parts[parts.length - 1]?.[0] : undefined;
|
|
43
|
+
return `${first}${last ?? ""}`.toUpperCase();
|
|
44
|
+
}
|
|
45
|
+
/* The same five identities on the dark chrome. The canvas palette there composed its tints
|
|
46
|
+
over the navy and left ink letters on a dark disc — 1.61:1, unreadable (measured on the
|
|
47
|
+
application bar). Same index, so a person keeps their color across the two surfaces. */
|
|
48
|
+
const CHROME_PALETTE = [
|
|
49
|
+
"bg-chrome-link/20 text-chrome-link",
|
|
50
|
+
"bg-chrome-good/20 text-chrome-good",
|
|
51
|
+
"bg-chrome-accent/12 text-chrome-accent",
|
|
52
|
+
"bg-chrome-link/14 text-chrome-ink",
|
|
53
|
+
"bg-chrome-ink/14 text-chrome-ink",
|
|
54
|
+
];
|
|
55
|
+
function toneOf(name, surface = "canvas") {
|
|
56
|
+
let h = 0;
|
|
57
|
+
for (const ch of name)
|
|
58
|
+
h = (h * 31 + ch.codePointAt(0)) % 100000;
|
|
59
|
+
const palette = surface === "chrome" ? CHROME_PALETTE : PALETTE;
|
|
60
|
+
return palette[h % palette.length];
|
|
61
|
+
}
|
|
62
|
+
/* The text doesn't go below `micro`: two initials at 10 px can't be read, and an
|
|
63
|
+
unreadable initial is a colored circle — which is exactly what we don't want.
|
|
64
|
+
What changes between sizes is the circle. */
|
|
65
|
+
const SIZE = {
|
|
66
|
+
sm: "size-5 text-micro",
|
|
67
|
+
md: "size-6 text-micro",
|
|
68
|
+
lg: "size-8 text-meta",
|
|
69
|
+
/* The head of a contact card: the face is what the card is about. */
|
|
70
|
+
xl: "size-12 text-body",
|
|
71
|
+
};
|
|
72
|
+
/* Two initials, so the overlap stays short of the second letter. The ring is
|
|
73
|
+
1px: a 2px ring of the surface color reads as a gap between people, which
|
|
74
|
+
is the thing a stack is not. */
|
|
75
|
+
const OVERLAP = {
|
|
76
|
+
sm: "-ml-1.5",
|
|
77
|
+
md: "-ml-2",
|
|
78
|
+
lg: "-ml-2.5",
|
|
79
|
+
xl: "-ml-3.5",
|
|
80
|
+
};
|
|
81
|
+
export function Avatar({ name, size = "md",
|
|
82
|
+
/** The photo, if there is one. Without it the initials go, which is the normal
|
|
83
|
+
* case here. */
|
|
84
|
+
src, }) {
|
|
85
|
+
const surface = useSurface();
|
|
86
|
+
if (src !== undefined) {
|
|
87
|
+
return (
|
|
88
|
+
/* The photo lies over the initials, drawn invisible: an image has no text baseline, so
|
|
89
|
+
a chip with a photo sat on the image's bottom edge in a row aligned by baseline (a
|
|
90
|
+
`DataField`), higher than the label next to it. The hidden initials give the photo
|
|
91
|
+
the same baseline the initials disc has. */
|
|
92
|
+
_jsxs("span", { title: name, className: `relative inline-flex shrink-0 items-center justify-center overflow-hidden rounded-pill ${SIZE[size]}`, children: [_jsx("span", { "aria-hidden": true, className: "invisible", children: initials(name) }), _jsx("img", { src: src, alt: name, className: "absolute inset-0 size-full object-cover" })] }));
|
|
93
|
+
}
|
|
94
|
+
return (_jsx("span", { title: name, "aria-hidden": true, className: `inline-flex shrink-0 items-center justify-center rounded-pill font-medium select-none ${SIZE[size]} ${toneOf(name, surface)}`, children: initials(name) }));
|
|
95
|
+
}
|
|
96
|
+
/** The three contact links, built from what people type. `wa.me` takes digits only — a `+`,
|
|
97
|
+
* a space or a dash breaks it — and `tel:` takes digits and the leading `+`. */
|
|
98
|
+
function contactRows(contact, copy) {
|
|
99
|
+
const rows = [];
|
|
100
|
+
if (contact.email !== undefined && contact.email !== "") {
|
|
101
|
+
rows.push({
|
|
102
|
+
key: "email",
|
|
103
|
+
icon: Mail,
|
|
104
|
+
label: copy.email,
|
|
105
|
+
value: contact.email,
|
|
106
|
+
href: `mailto:${contact.email}`,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
if (contact.phone !== undefined && contact.phone !== "") {
|
|
110
|
+
rows.push({
|
|
111
|
+
key: "call",
|
|
112
|
+
icon: Phone,
|
|
113
|
+
label: copy.call,
|
|
114
|
+
value: contact.phone,
|
|
115
|
+
href: `tel:${contact.phone.replace(/[^\d+]/g, "")}`,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
if (contact.whatsapp !== undefined && contact.whatsapp !== "") {
|
|
119
|
+
rows.push({
|
|
120
|
+
key: "whatsapp",
|
|
121
|
+
icon: MessageCircle,
|
|
122
|
+
label: copy.whatsapp,
|
|
123
|
+
value: contact.whatsapp,
|
|
124
|
+
href: `https://wa.me/${contact.whatsapp.replace(/\D/g, "")}`,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return rows;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Who a person is and how to reach them: the face, the name, the role, and one row per way
|
|
131
|
+
* of contact — write, call, WhatsApp — then their record.
|
|
132
|
+
*
|
|
133
|
+
* Context: in an ERP the question after «who is the salesperson of this invoice?» is almost
|
|
134
|
+
* always «how do I reach her?». Copying an email out of another screen to paste it into the
|
|
135
|
+
* mail client is the detour this card removes. Each way of contact is a ROW and not a bare
|
|
136
|
+
* icon: the address and the number are drawn, because often what the person wants is to read
|
|
137
|
+
* the number aloud, not to dial it from the browser. Every row is a real link (`mailto:`,
|
|
138
|
+
* `tel:`, `wa.me`), so middle-click, «copy address» and the phone's own dialer work.
|
|
139
|
+
*
|
|
140
|
+
* It is what opens from a `UserChip` with `contact`, and it is exported on its own for the
|
|
141
|
+
* side panel of a record, where the same person is shown without a chip to open.
|
|
142
|
+
*/
|
|
143
|
+
export function ContactCard({ name, src, role, contact = {}, onOpen }) {
|
|
144
|
+
const copy = useUiCopy().person;
|
|
145
|
+
const rows = contactRows(contact, copy);
|
|
146
|
+
return (
|
|
147
|
+
/* React events bubble through the popover's portal to the React parents of the chip —
|
|
148
|
+
a table row that opens on click among them. A click inside the card is not a click
|
|
149
|
+
on the row. */
|
|
150
|
+
_jsxs("div", { className: "flex w-64 max-w-full flex-col gap-3", onClick: (e) => e.stopPropagation(), children: [_jsxs("div", { className: "flex min-w-0 items-center gap-3", children: [_jsx("span", { "aria-hidden": true, className: "flex shrink-0", children: _jsx(Avatar, { name: name, size: "xl", ...(src === undefined ? {} : { src }) }) }), _jsxs("div", { className: "flex min-w-0 flex-col", children: [_jsx("span", { className: "truncate text-lead font-semibold text-ink", title: name, children: name }), role === undefined ? null : (_jsx("span", { className: "line-clamp-2 text-meta text-muted", title: role, children: role }))] })] }), rows.length === 0 ? null : (_jsx("ul", { className: "-mx-1 flex flex-col", children: rows.map(({ key, icon: Icon, label, value, href }) => (_jsx("li", { children: _jsxs("a", { href: href, "data-contact": key, ...(key === "whatsapp" ? { target: "_blank", rel: "noreferrer" } : {}), className: "flex min-w-0 items-center gap-2.5 rounded-control px-1 py-1.5 no-underline outline-primary outline-offset-2 hover:bg-ink/6 focus-visible:outline-2 forced-colors:outline-[Highlight]", children: [_jsx(Icon, { "aria-hidden": true, className: "size-(--size-icon-md) shrink-0 text-primary", strokeWidth: 2 }), _jsxs("span", { className: "flex min-w-0 flex-col", children: [_jsx("span", { className: "text-meta font-medium text-ink", children: label }), _jsx("span", { className: "truncate text-micro tabular-nums text-muted", children: value })] })] }) }, key))) })), onOpen === undefined ? null : (_jsx("div", { className: "flex justify-end border-t border-line pt-2", children: _jsx(Button, { role: "ghost", size: "sm", icon: ArrowRight, onClick: onOpen, children: copy.open }) }))] }));
|
|
151
|
+
}
|
|
152
|
+
/** The chip as the opener of its card. A native button that takes what `PopoverTrigger`
|
|
153
|
+
* hands its trigger — `ref`, the press handlers, `expanded` and `haspopup` — the same
|
|
154
|
+
* contract the system's buttons keep. */
|
|
155
|
+
function ChipButton({ expanded, haspopup, children, ref, ...native }) {
|
|
156
|
+
return (_jsx("button", { ...native, ref: ref, type: "button", ...(haspopup === undefined ? {} : { "aria-haspopup": haspopup }), ...(expanded === undefined ? {} : { "aria-expanded": expanded }),
|
|
157
|
+
/* The hover tint is a pseudo-element that spills past the box, not padding: padding
|
|
158
|
+
moved the chip off the column of values, and negative margins to undo it made the
|
|
159
|
+
chip wider than the room its parent measured — and it truncated itself. */
|
|
160
|
+
className: "relative isolate inline-flex max-w-full min-w-0 cursor-pointer items-center gap-2 rounded-control text-left outline-primary outline-offset-4 before:absolute before:-inset-x-1 before:-inset-y-0.5 before:-z-10 before:rounded-control before:transition-colors before:duration-(--duration-instant) hover:before:bg-ink/6 focus-visible:outline-2 aria-expanded:before:bg-ink/6 forced-colors:outline-[Highlight]", children: children }));
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* The person with their name next to them. For a cell, a list row, a field in read mode or
|
|
164
|
+
* a fact of a record.
|
|
165
|
+
*
|
|
166
|
+
* Context: a chip is a button ONLY when it has somewhere to go. With `contact` or `onOpen` it
|
|
167
|
+
* opens the `ContactCard` (press, Enter or Space — React Aria's `DialogTrigger` through
|
|
168
|
+
* `PopoverTrigger`: focus goes into the card, Escape and a click outside close it, and focus
|
|
169
|
+
* comes back to the chip). Without them it stays text: a button that does nothing is a lie a
|
|
170
|
+
* keyboard user pays for with one more Tab stop on every row.
|
|
171
|
+
*
|
|
172
|
+
* It opens on PRESS and not on hover: the card holds links, and content that has to be
|
|
173
|
+
* reached with the pointer or the keyboard cannot disappear when the pointer leaves the name.
|
|
174
|
+
*/
|
|
175
|
+
export function UserChip({ name, size = "md", src, detail, role, contact, onOpen, }) {
|
|
176
|
+
const face = (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": true, className: "flex shrink-0", children: _jsx(Avatar, { name: name, size: size, ...(src === undefined ? {} : { src }) }) }), _jsx("span", { className: "min-w-0 truncate text-meta text-ink", children: name }), detail === undefined ? null : (_jsx("span", { className: "min-w-0 shrink truncate text-micro text-muted", children: detail }))] }));
|
|
177
|
+
const reachable = onOpen !== undefined ||
|
|
178
|
+
(contact !== undefined &&
|
|
179
|
+
Object.values(contact).some((v) => v !== undefined && v !== ""));
|
|
180
|
+
if (!reachable) {
|
|
181
|
+
return _jsx("span", { className: "inline-flex min-w-0 items-center gap-2", children: face });
|
|
182
|
+
}
|
|
183
|
+
return (_jsx(PopoverTrigger, { label: name, trigger: _jsx(ChipButton, { children: face }), children: (close) => (_jsx(ContactCard, { name: name, ...(src === undefined ? {} : { src }), ...(role === undefined ? {} : { role }), ...(contact === undefined ? {} : { contact }), ...(onOpen === undefined
|
|
184
|
+
? {}
|
|
185
|
+
: {
|
|
186
|
+
onOpen: () => {
|
|
187
|
+
close();
|
|
188
|
+
onOpen();
|
|
189
|
+
},
|
|
190
|
+
}) })) }));
|
|
191
|
+
}
|
|
192
|
+
/** Several people in little width: the widget of a to-many relation.
|
|
193
|
+
*
|
|
194
|
+
* Context: they overlap and get cut off at `max`. Five avatars in a row are five
|
|
195
|
+
* things; five overlapped with a «+3» are ONE thing that has eight inside, which
|
|
196
|
+
* is what the cell means to say.
|
|
197
|
+
*
|
|
198
|
+
* The `title` carries the whole list: whoever needs the names has them without
|
|
199
|
+
* the column paying for their width. */
|
|
200
|
+
export function UserStack({ names, max = 4, size = "md", }) {
|
|
201
|
+
const copy = useUiCopy();
|
|
202
|
+
if (names.length === 0) {
|
|
203
|
+
return (_jsx("span", { className: "text-meta text-muted italic", title: copy.person.unassigned, children: copy.person.unassigned }));
|
|
204
|
+
}
|
|
205
|
+
const visible = names.length <= max ? names : names.slice(0, max - 1);
|
|
206
|
+
const rest = names.length - visible.length;
|
|
207
|
+
return (_jsxs("span", { className: "inline-flex items-center", title: names.join(", "), children: [visible.map((n, i) => (_jsx("span", { className: i === 0 ? "" : OVERLAP[size], children: _jsx("span", { className: "block rounded-pill ring-1 ring-surface", children: _jsx(Avatar, { name: n, size: size }) }) }, n))), rest <= 0 ? null : (_jsxs("span", { className: `${OVERLAP[size]} inline-flex items-center justify-center rounded-pill bg-ink/10 font-medium tabular-nums text-muted ring-1 ring-surface ${SIZE[size]}`, children: ["+", rest] }))] }));
|
|
208
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Tone } from "../../foundations/types.js";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
export type ProgressBarProps = {
|
|
4
|
+
/** Where the task is, from `minValue` to `maxValue`. Absent = under way and it cannot say
|
|
5
|
+
* how far: the bar sweeps and draws no figure. */
|
|
6
|
+
value?: number;
|
|
7
|
+
/** What is running: «Importación de clientes», «Cierre de octubre». Always its accessible
|
|
8
|
+
* name; drawn above the bar only with `showLabel`. */
|
|
9
|
+
label: string;
|
|
10
|
+
minValue?: number;
|
|
11
|
+
/** The size of the task — 4000 for an import of 4.000 rows. 100 by default, so a plain
|
|
12
|
+
* percentage needs nothing else. */
|
|
13
|
+
maxValue?: number;
|
|
14
|
+
/** How the figure is written. Absent: the percentage of the task. */
|
|
15
|
+
formatOptions?: Intl.NumberFormatOptions;
|
|
16
|
+
/** The figure written by hand, when a percentage does not say it: «1.240 de 4.000 filas».
|
|
17
|
+
* It is also what a screen reader reads. Indeterminate, it is the only figure drawn —
|
|
18
|
+
* «leyendo el archivo…». */
|
|
19
|
+
valueLabel?: ReactNode;
|
|
20
|
+
/** Draws the label above the bar: the bar a screen is about (an import, the closing of
|
|
21
|
+
* the month). Off in a cell or a row, where the column or the line already says it. */
|
|
22
|
+
showLabel?: boolean;
|
|
23
|
+
/** Off when the figure is already beside it — a cell with its column. */
|
|
24
|
+
showValue?: boolean;
|
|
25
|
+
/** The width of the track (inline) or of the whole piece (with `showLabel`). A bar in a
|
|
26
|
+
* list row is shorter than the bar of an import. */
|
|
27
|
+
width?: string;
|
|
28
|
+
/** The state of the task. `brand` — the gradient — is the long bar of a live process. */
|
|
29
|
+
tone?: Tone | "brand";
|
|
30
|
+
};
|
|
31
|
+
export declare function ProgressBar({ value, label, minValue, maxValue, formatOptions, valueLabel, showLabel, showValue, width, tone, }: ProgressBarProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { AriaLocale } from "../../foundations/aria.js";
|
|
4
|
+
import { cn } from "../../foundations/variants.js";
|
|
5
|
+
import { ProgressBar as AriaProgressBar } from "react-aria-components";
|
|
6
|
+
/**
|
|
7
|
+
* A task under way: the import of 4.000 rows, the closing of the month, a file going up. A
|
|
8
|
+
* real bar, never a spinner — a spinner says «wait» and does not say how much; a bar says both.
|
|
9
|
+
*
|
|
10
|
+
* Context: it looks like `Meter` and it is the opposite thing. A meter is a QUANTITY in a
|
|
11
|
+
* range that just is — the stock against the warehouse, the credit used — and full is often
|
|
12
|
+
* the alarm. A progress bar is something HAPPENING, moving toward done, and full is the good
|
|
13
|
+
* news. That is why their color comes from different places: the meter's is the reading of
|
|
14
|
+
* its value; this one's is the state of the task, said by whoever runs it — `good` finished,
|
|
15
|
+
* `warn` is running slow, `bad` failed — and `brand` by default, the blue→gold gradient of
|
|
16
|
+
* the long bar of a live import. (At 6 px and 62 % in a list row a gradient reads as a
|
|
17
|
+
* broken bar, so a short bar there asks for a flat tone.)
|
|
18
|
+
*
|
|
19
|
+
* ── Determinate and indeterminate ─────────────────────────────────────
|
|
20
|
+
*
|
|
21
|
+
* With a `value` it fills to it, and the figure next to it is in tabular figures so a column
|
|
22
|
+
* of bars keeps its digits aligned. Without one the task is under way and cannot say how far
|
|
23
|
+
* — the server has not counted the rows yet — and the bar SAYS so instead of inventing a
|
|
24
|
+
* number: a segment sweeps along the track and no figure is drawn. That sweep is the only
|
|
25
|
+
* loop of the system (`--animate-progress-sweep`), and it runs behind `motion-safe:`: with
|
|
26
|
+
* `prefers-reduced-motion` the segment stands still, the whole track faded — «busy» without
|
|
27
|
+
* movement, and without a fill that could be read as a percentage.
|
|
28
|
+
*
|
|
29
|
+
* React Aria writes `role="progressbar"` and the `aria-value*` — no `aria-valuenow` while it
|
|
30
|
+
* is indeterminate, which is how a screen reader tells the two apart — and formats the figure
|
|
31
|
+
* in the copy table's locale (`AriaLocale`).
|
|
32
|
+
*
|
|
33
|
+
* The track is the system's `--size-track`: at 3 px it was a line that could not be told
|
|
34
|
+
* from a separator; two bars of different thickness on one screen read as two things.
|
|
35
|
+
*/
|
|
36
|
+
const FILL = {
|
|
37
|
+
neutral: "bg-muted",
|
|
38
|
+
info: "bg-info",
|
|
39
|
+
good: "bg-good",
|
|
40
|
+
warn: "bg-warn",
|
|
41
|
+
bad: "bg-bad",
|
|
42
|
+
accent: "bg-accent",
|
|
43
|
+
};
|
|
44
|
+
export function ProgressBar({ value, label, minValue = 0, maxValue = 100, formatOptions, valueLabel, showLabel = false, showValue = true, width = "w-24", tone = "brand", }) {
|
|
45
|
+
const fill = tone === "brand" ? "bar-brand" : FILL[tone];
|
|
46
|
+
return (_jsx(AriaLocale, { children: _jsx(AriaProgressBar, { "aria-label": label, ...(value === undefined ? {} : { value }), isIndeterminate: value === undefined, minValue: minValue, maxValue: maxValue, formatOptions: formatOptions, valueLabel: valueLabel, className: showLabel
|
|
47
|
+
? `flex max-w-full flex-col gap-1.5 ${width}`
|
|
48
|
+
: "flex min-w-0 items-center gap-2", children: ({ percentage, valueText, isIndeterminate }) => {
|
|
49
|
+
const figure = showValue && valueText !== undefined ? (_jsx("span", { className: "shrink-0 text-meta tabular-nums text-muted", children: valueText })) : null;
|
|
50
|
+
const track = (_jsx("span", { className: cn("relative block h-(--size-track) overflow-hidden rounded-pill bg-ink/10 forced-colors:outline forced-colors:outline-[ButtonBorder]", showLabel ? "w-full" : width), children: isIndeterminate ? (_jsx("span", { "data-indeterminate": true, className: cn("absolute inset-y-0 left-0 w-2/5 rounded-pill forced-colors:bg-[Highlight]", "motion-safe:animate-progress-sweep motion-reduce:w-full motion-reduce:opacity-40", fill) })) : (_jsx("span", { className: cn("block h-full rounded-pill transition-[width] duration-(--duration-panel) ease-(--ease-out) forced-colors:bg-[Highlight]", fill), style: { width: `${percentage ?? 0}%` } })) }));
|
|
51
|
+
return showLabel ? (_jsxs(_Fragment, { children: [_jsxs("span", { className: "flex items-baseline justify-between gap-2", children: [_jsx("span", { className: "truncate text-meta text-ink", children: label }), figure] }), track] })) : (_jsxs(_Fragment, { children: [track, figure] }));
|
|
52
|
+
} }) }));
|
|
53
|
+
}
|