@sincpro/web-ui 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +157 -2
- package/dist/components/assist/AssistMark.d.ts +64 -0
- package/dist/components/assist/AssistMark.js +108 -0
- package/dist/components/assist/AssistPending.d.ts +24 -0
- package/dist/components/assist/AssistPending.js +26 -0
- package/dist/components/assist/AssistSuggestion.d.ts +42 -0
- package/dist/components/assist/AssistSuggestion.js +55 -0
- package/dist/components/assist/ChangeReview.d.ts +56 -0
- package/dist/components/assist/ChangeReview.js +121 -0
- package/dist/components/assist/assist.d.ts +77 -0
- package/dist/components/assist/assist.js +87 -0
- package/dist/components/assist/index.d.ts +5 -0
- package/dist/components/assist/index.js +7 -0
- package/dist/components/chatter/Chatter.d.ts +161 -0
- package/dist/components/chatter/Chatter.js +232 -0
- package/dist/components/chatter/Messages.d.ts +66 -0
- package/dist/components/chatter/Messages.js +43 -0
- package/dist/components/chatter/Timeline.d.ts +21 -0
- package/dist/components/chatter/Timeline.js +30 -0
- package/dist/components/chatter/index.d.ts +3 -0
- package/dist/components/chatter/index.js +4 -0
- package/dist/components/form/Form.d.ts +52 -0
- package/dist/components/form/Form.js +49 -0
- package/dist/components/form/Sheet.d.ts +43 -0
- package/dist/components/form/Sheet.js +83 -0
- package/dist/components/form/index.d.ts +2 -0
- package/dist/components/form/index.js +4 -0
- package/dist/components/frame/Chart.d.ts +13 -0
- package/dist/components/frame/Chart.js +24 -0
- package/dist/components/frame/FrameView.d.ts +33 -0
- package/dist/components/frame/FrameView.js +125 -0
- package/dist/components/frame/index.d.ts +3 -0
- package/dist/components/frame/index.js +3 -0
- package/dist/components/frame/model.d.ts +43 -0
- package/dist/components/frame/model.js +101 -0
- package/dist/components/index.d.ts +10 -0
- package/dist/components/index.js +11 -0
- package/dist/components/lines/LineItems.d.ts +74 -0
- package/dist/components/lines/LineItems.js +229 -0
- package/dist/components/lines/index.d.ts +1 -0
- package/dist/components/lines/index.js +2 -0
- package/dist/components/lines/lines.d.ts +23 -0
- package/dist/components/lines/lines.js +26 -0
- package/dist/components/listing/ColumnsMenu.d.ts +20 -0
- package/dist/components/listing/ColumnsMenu.js +46 -0
- package/dist/components/listing/GroupingLevels.d.ts +35 -0
- package/dist/components/listing/GroupingLevels.js +45 -0
- package/dist/components/listing/Listing.d.ts +27 -0
- package/dist/components/listing/Listing.js +86 -0
- package/dist/components/listing/ListingBar.d.ts +73 -0
- package/dist/components/listing/ListingBar.js +105 -0
- package/dist/components/listing/Pagination.d.ts +36 -0
- package/dist/components/listing/Pagination.js +69 -0
- package/dist/components/listing/RecordCard.d.ts +33 -0
- package/dist/components/listing/RecordCard.js +22 -0
- package/dist/components/listing/RecordLink.d.ts +7 -0
- package/dist/components/listing/RecordLink.js +22 -0
- package/dist/components/listing/Row.d.ts +106 -0
- package/dist/components/listing/Row.js +122 -0
- package/dist/components/listing/SortMenu.d.ts +62 -0
- package/dist/components/listing/SortMenu.js +105 -0
- package/dist/components/listing/ViewSwitcher.d.ts +24 -0
- package/dist/components/listing/ViewSwitcher.js +16 -0
- package/dist/components/listing/columns.d.ts +47 -0
- package/dist/components/listing/columns.js +72 -0
- package/dist/components/listing/grouping.d.ts +35 -0
- package/dist/components/listing/grouping.js +47 -0
- package/dist/components/listing/index.d.ts +17 -0
- package/dist/components/listing/index.js +20 -0
- package/dist/components/listing/modelColumns.d.ts +50 -0
- package/dist/components/listing/modelColumns.js +103 -0
- package/dist/components/listing/pages.d.ts +78 -0
- package/dist/components/listing/pages.js +135 -0
- package/dist/components/listing/selection.d.ts +21 -0
- package/dist/components/listing/selection.js +43 -0
- package/dist/components/listing/set.d.ts +48 -0
- package/dist/components/listing/set.js +24 -0
- package/dist/components/listing/tree.d.ts +28 -0
- package/dist/components/listing/tree.js +23 -0
- package/dist/components/navigation/CommandPalette.d.ts +40 -0
- package/dist/components/navigation/CommandPalette.js +114 -0
- package/dist/components/navigation/NavigationTree.d.ts +31 -0
- package/dist/components/navigation/NavigationTree.js +79 -0
- package/dist/components/navigation/commands.d.ts +30 -0
- package/dist/components/navigation/commands.js +30 -0
- package/dist/components/navigation/index.d.ts +3 -0
- package/dist/components/navigation/index.js +4 -0
- package/dist/components/record/Breadcrumb.d.ts +42 -0
- package/dist/components/record/Breadcrumb.js +98 -0
- package/dist/components/record/DirtyActions.d.ts +26 -0
- package/dist/components/record/DirtyActions.js +22 -0
- package/dist/components/record/Pager.d.ts +18 -0
- package/dist/components/record/Pager.js +33 -0
- package/dist/components/record/RecordHeader.d.ts +71 -0
- package/dist/components/record/RecordHeader.js +72 -0
- package/dist/components/record/RecordSkeleton.d.ts +3 -0
- package/dist/components/record/RecordSkeleton.js +21 -0
- package/dist/components/record/SmartButtons.d.ts +35 -0
- package/dist/components/record/SmartButtons.js +38 -0
- package/dist/components/record/StatusBar.d.ts +51 -0
- package/dist/components/record/StatusBar.js +168 -0
- package/dist/components/record/ThumbBar.d.ts +36 -0
- package/dist/components/record/ThumbBar.js +86 -0
- package/dist/components/record/index.d.ts +6 -0
- package/dist/components/record/index.js +8 -0
- package/dist/components/search/CriteriaBuilder.d.ts +58 -0
- package/dist/components/search/CriteriaBuilder.js +278 -0
- package/dist/components/search/CriteriaChips.d.ts +22 -0
- package/dist/components/search/CriteriaChips.js +64 -0
- package/dist/components/search/FilterBuilder.d.ts +142 -0
- package/dist/components/search/FilterBuilder.js +421 -0
- package/dist/components/search/JoinWord.d.ts +8 -0
- package/dist/components/search/JoinWord.js +47 -0
- package/dist/components/search/JsonEditor.d.ts +19 -0
- package/dist/components/search/JsonEditor.js +78 -0
- package/dist/components/search/SearchBar.d.ts +78 -0
- package/dist/components/search/SearchBar.js +387 -0
- package/dist/components/search/criteria.d.ts +116 -0
- package/dist/components/search/criteria.js +159 -0
- package/dist/components/search/datePeriods.d.ts +122 -0
- package/dist/components/search/datePeriods.js +134 -0
- package/dist/components/search/dateRanges.d.ts +54 -0
- package/dist/components/search/dateRanges.js +89 -0
- package/dist/components/search/index.d.ts +9 -0
- package/dist/components/search/index.js +20 -0
- package/dist/components/search/model.d.ts +54 -0
- package/dist/components/search/model.js +37 -0
- package/dist/components/search/query.d.ts +166 -0
- package/dist/components/search/query.js +436 -0
- package/dist/components/shell/AppFrame.d.ts +14 -0
- package/dist/components/shell/AppFrame.js +80 -0
- package/dist/components/shell/TopBar.d.ts +73 -0
- package/dist/components/shell/TopBar.js +54 -0
- package/dist/components/shell/headerSlot.d.ts +17 -0
- package/dist/components/shell/headerSlot.js +21 -0
- package/dist/components/shell/index.d.ts +2 -0
- package/dist/components/shell/index.js +3 -0
- package/dist/foundations/aria.d.ts +14 -0
- package/dist/foundations/aria.js +18 -0
- package/dist/foundations/calendar.d.ts +28 -0
- package/dist/foundations/calendar.js +183 -0
- package/dist/foundations/copy.d.ts +619 -0
- package/dist/foundations/copy.es.d.ts +2 -0
- package/dist/foundations/copy.es.js +377 -0
- package/dist/foundations/copy.js +472 -0
- package/dist/foundations/density.d.ts +16 -0
- package/dist/foundations/density.js +21 -0
- package/dist/foundations/grouping.d.ts +281 -0
- package/dist/foundations/grouping.js +0 -0
- package/dist/foundations/index.d.ts +14 -0
- package/dist/foundations/index.js +30 -0
- package/dist/foundations/layout.d.ts +9 -0
- package/dist/foundations/layout.js +9 -0
- package/dist/foundations/pivot.d.ts +222 -0
- package/dist/foundations/pivot.js +198 -0
- package/dist/foundations/pivotSheet.d.ts +61 -0
- package/dist/foundations/pivotSheet.js +0 -0
- package/dist/foundations/slots.d.ts +67 -0
- package/dist/foundations/slots.js +63 -0
- package/dist/foundations/sticky.d.ts +53 -0
- package/dist/foundations/sticky.js +150 -0
- package/dist/foundations/surface.d.ts +9 -0
- package/dist/foundations/surface.js +26 -0
- package/dist/foundations/theme.d.ts +42 -0
- package/dist/foundations/theme.js +88 -0
- package/dist/foundations/types.d.ts +23 -0
- package/dist/foundations/types.js +50 -0
- package/dist/foundations/variants.d.ts +19 -0
- package/dist/foundations/variants.js +48 -0
- package/dist/foundations/xlsx.d.ts +86 -0
- package/dist/foundations/xlsx.js +0 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +19 -0
- package/dist/primitives/block/Block.d.ts +66 -0
- package/dist/primitives/block/Block.js +34 -0
- package/dist/primitives/block/Line.d.ts +18 -0
- package/dist/primitives/block/Line.js +45 -0
- package/dist/primitives/block/SectionGrid.d.ts +30 -0
- package/dist/primitives/block/SectionGrid.js +29 -0
- package/dist/primitives/block/Steps.d.ts +76 -0
- package/dist/primitives/block/Steps.js +43 -0
- package/dist/primitives/block/index.d.ts +4 -0
- package/dist/primitives/block/index.js +7 -0
- package/dist/primitives/button/Button.d.ts +137 -0
- package/dist/primitives/button/Button.js +234 -0
- package/dist/primitives/button/index.d.ts +1 -0
- package/dist/primitives/button/index.js +2 -0
- package/dist/primitives/cell/CellStack.d.ts +47 -0
- package/dist/primitives/cell/CellStack.js +33 -0
- package/dist/primitives/cell/index.d.ts +1 -0
- package/dist/primitives/cell/index.js +3 -0
- package/dist/primitives/choice/ComboBox.d.ts +16 -0
- package/dist/primitives/choice/ComboBox.js +52 -0
- package/dist/primitives/choice/RadioGroup.d.ts +18 -0
- package/dist/primitives/choice/RadioGroup.js +50 -0
- package/dist/primitives/choice/SearchField.d.ts +24 -0
- package/dist/primitives/choice/SearchField.js +27 -0
- package/dist/primitives/choice/SegmentedControl.d.ts +16 -0
- package/dist/primitives/choice/SegmentedControl.js +69 -0
- package/dist/primitives/choice/Slider.d.ts +16 -0
- package/dist/primitives/choice/Slider.js +46 -0
- package/dist/primitives/choice/Switch.d.ts +14 -0
- package/dist/primitives/choice/Switch.js +65 -0
- package/dist/primitives/choice/ThemeSwitcher.d.ts +10 -0
- package/dist/primitives/choice/ThemeSwitcher.js +25 -0
- package/dist/primitives/choice/index.d.ts +8 -0
- package/dist/primitives/choice/index.js +11 -0
- package/dist/primitives/choice/options.d.ts +28 -0
- package/dist/primitives/choice/options.js +6 -0
- package/dist/primitives/date/Calendar.d.ts +31 -0
- package/dist/primitives/date/Calendar.js +110 -0
- package/dist/primitives/date/DatePicker.d.ts +34 -0
- package/dist/primitives/date/DatePicker.js +99 -0
- package/dist/primitives/date/index.d.ts +2 -0
- package/dist/primitives/date/index.js +4 -0
- package/dist/primitives/dialog/Dialog.d.ts +118 -0
- package/dist/primitives/dialog/Dialog.js +121 -0
- package/dist/primitives/dialog/index.d.ts +1 -0
- package/dist/primitives/dialog/index.js +2 -0
- package/dist/primitives/disclosure/Disclosure.d.ts +36 -0
- package/dist/primitives/disclosure/Disclosure.js +52 -0
- package/dist/primitives/disclosure/index.d.ts +1 -0
- package/dist/primitives/disclosure/index.js +2 -0
- package/dist/primitives/field/Checks.d.ts +57 -0
- package/dist/primitives/field/Checks.js +72 -0
- package/dist/primitives/field/DropZone.d.ts +42 -0
- package/dist/primitives/field/DropZone.js +52 -0
- package/dist/primitives/field/Field.d.ts +166 -0
- package/dist/primitives/field/Field.js +240 -0
- package/dist/primitives/field/Suggest.d.ts +48 -0
- package/dist/primitives/field/Suggest.js +43 -0
- package/dist/primitives/field/aria.d.ts +61 -0
- package/dist/primitives/field/aria.js +70 -0
- package/dist/primitives/field/context.d.ts +30 -0
- package/dist/primitives/field/context.js +13 -0
- package/dist/primitives/field/index.d.ts +7 -0
- package/dist/primitives/field/index.js +11 -0
- package/dist/primitives/field/layout.d.ts +24 -0
- package/dist/primitives/field/layout.js +10 -0
- package/dist/primitives/field/mode.d.ts +9 -0
- package/dist/primitives/field/mode.js +13 -0
- package/dist/primitives/hint/Hint.d.ts +31 -0
- package/dist/primitives/hint/Hint.js +42 -0
- package/dist/primitives/hint/index.d.ts +1 -0
- package/dist/primitives/hint/index.js +1 -0
- package/dist/primitives/index.d.ts +19 -0
- package/dist/primitives/index.js +20 -0
- package/dist/primitives/menu/Menu.d.ts +100 -0
- package/dist/primitives/menu/Menu.js +59 -0
- package/dist/primitives/menu/index.d.ts +1 -0
- package/dist/primitives/menu/index.js +2 -0
- package/dist/primitives/notice/Callout.d.ts +49 -0
- package/dist/primitives/notice/Callout.js +28 -0
- package/dist/primitives/notice/Notice.d.ts +45 -0
- package/dist/primitives/notice/Notice.js +61 -0
- package/dist/primitives/notice/Toast.d.ts +55 -0
- package/dist/primitives/notice/Toast.js +142 -0
- package/dist/primitives/notice/index.d.ts +3 -0
- package/dist/primitives/notice/index.js +5 -0
- package/dist/primitives/overlay/Popover.d.ts +6 -0
- package/dist/primitives/overlay/Popover.js +27 -0
- package/dist/primitives/overlay/PopoverTrigger.d.ts +55 -0
- package/dist/primitives/overlay/PopoverTrigger.js +38 -0
- package/dist/primitives/overlay/Tooltip.d.ts +38 -0
- package/dist/primitives/overlay/Tooltip.js +74 -0
- package/dist/primitives/overlay/index.d.ts +3 -0
- package/dist/primitives/overlay/index.js +5 -0
- package/dist/primitives/picker/Picker.d.ts +48 -0
- package/dist/primitives/picker/Picker.js +150 -0
- package/dist/primitives/picker/index.d.ts +1 -0
- package/dist/primitives/picker/index.js +2 -0
- package/dist/primitives/surface/Peek.d.ts +60 -0
- package/dist/primitives/surface/Peek.js +71 -0
- package/dist/primitives/surface/index.d.ts +1 -0
- package/dist/primitives/surface/index.js +2 -0
- package/dist/primitives/table/CellEditor.d.ts +14 -0
- package/dist/primitives/table/CellEditor.js +83 -0
- package/dist/primitives/table/DataTable.d.ts +135 -0
- package/dist/primitives/table/DataTable.js +244 -0
- package/dist/primitives/table/EditableTable.d.ts +96 -0
- package/dist/primitives/table/EditableTable.js +423 -0
- package/dist/primitives/table/PivotTable.d.ts +14 -0
- package/dist/primitives/table/PivotTable.js +0 -0
- package/dist/primitives/table/ReportTable.d.ts +38 -0
- package/dist/primitives/table/ReportTable.js +106 -0
- package/dist/primitives/table/RowActions.d.ts +16 -0
- package/dist/primitives/table/RowActions.js +28 -0
- package/dist/primitives/table/SheetTable.d.ts +70 -0
- package/dist/primitives/table/SheetTable.js +197 -0
- package/dist/primitives/table/Table.d.ts +106 -0
- package/dist/primitives/table/Table.js +116 -0
- package/dist/primitives/table/index.d.ts +7 -0
- package/dist/primitives/table/index.js +9 -0
- package/dist/primitives/tabs/Tabs.d.ts +91 -0
- package/dist/primitives/tabs/Tabs.js +56 -0
- package/dist/primitives/tabs/index.d.ts +1 -0
- package/dist/primitives/tabs/index.js +1 -0
- package/dist/primitives/tag/Tag.d.ts +36 -0
- package/dist/primitives/tag/Tag.js +68 -0
- package/dist/primitives/tag/Tags.d.ts +57 -0
- package/dist/primitives/tag/Tags.js +104 -0
- package/dist/primitives/tag/TagsField.d.ts +32 -0
- package/dist/primitives/tag/TagsField.js +182 -0
- package/dist/primitives/tag/choices.d.ts +38 -0
- package/dist/primitives/tag/choices.js +26 -0
- package/dist/primitives/tag/index.d.ts +4 -0
- package/dist/primitives/tag/index.js +8 -0
- package/dist/primitives/toolbar/Toolbar.d.ts +13 -0
- package/dist/primitives/toolbar/Toolbar.js +36 -0
- package/dist/primitives/toolbar/index.d.ts +1 -0
- package/dist/primitives/toolbar/index.js +2 -0
- package/dist/primitives/value/Copyable.d.ts +37 -0
- package/dist/primitives/value/Copyable.js +50 -0
- package/dist/primitives/value/Fecha.d.ts +12 -0
- package/dist/primitives/value/Fecha.js +80 -0
- package/dist/primitives/value/Meter.d.ts +29 -0
- package/dist/primitives/value/Meter.js +46 -0
- package/dist/primitives/value/Metric.d.ts +42 -0
- package/dist/primitives/value/Metric.js +38 -0
- package/dist/primitives/value/Person.d.ts +90 -0
- package/dist/primitives/value/Person.js +208 -0
- package/dist/primitives/value/Relation.d.ts +69 -0
- package/dist/primitives/value/Relation.js +57 -0
- package/dist/primitives/value/Totals.d.ts +26 -0
- package/dist/primitives/value/Totals.js +4 -0
- package/dist/primitives/value/Value.d.ts +77 -0
- package/dist/primitives/value/Value.js +119 -0
- package/dist/primitives/value/index.d.ts +8 -0
- package/dist/primitives/value/index.js +9 -0
- package/dist/tokens.css +1025 -0
- package/dist/views/index.d.ts +2 -0
- package/dist/views/index.js +3 -0
- package/dist/views/list/List.d.ts +113 -0
- package/dist/views/list/List.js +97 -0
- package/dist/views/list/ListView.d.ts +6 -0
- package/dist/views/list/ListView.js +269 -0
- package/dist/views/list/columns.d.ts +18 -0
- package/dist/views/list/columns.js +27 -0
- package/dist/views/list/derive.d.ts +76 -0
- package/dist/views/list/derive.js +181 -0
- package/dist/views/list/index.d.ts +2 -0
- package/dist/views/list/index.js +4 -0
- package/dist/views/list/presentations.d.ts +39 -0
- package/dist/views/list/presentations.js +103 -0
- package/dist/views/list/types.d.ts +272 -0
- package/dist/views/list/types.js +1 -0
- package/dist/views/list/useNarrowList.d.ts +17 -0
- package/dist/views/list/useNarrowList.js +32 -0
- package/dist/views/record/Record.d.ts +126 -0
- package/dist/views/record/Record.js +141 -0
- package/dist/views/record/index.d.ts +1 -0
- package/dist/views/record/index.js +2 -0
- package/package.json +109 -4
|
@@ -0,0 +1,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,69 @@
|
|
|
1
|
+
import type { Tone } from "../../foundations/types.js";
|
|
2
|
+
import { type FormMode } from "../field/mode.js";
|
|
3
|
+
import type { LucideIcon } from "lucide-react";
|
|
4
|
+
import { type ReactNode } from "react";
|
|
5
|
+
/** What the floating summary of the related record says. */
|
|
6
|
+
export type RelationSummary = {
|
|
7
|
+
/** The title of the card. The relation's name when absent. */
|
|
8
|
+
title?: string;
|
|
9
|
+
/** The state of the related record — «al día», «con deuda». */
|
|
10
|
+
tag?: {
|
|
11
|
+
label: string;
|
|
12
|
+
tone?: Tone;
|
|
13
|
+
};
|
|
14
|
+
/** Two to four facts that answer the usual question about it: the debt, the credit
|
|
15
|
+
* limit, the city, the phone. More is a record, not a summary. */
|
|
16
|
+
facts?: readonly {
|
|
17
|
+
label: string;
|
|
18
|
+
value: ReactNode;
|
|
19
|
+
}[];
|
|
20
|
+
};
|
|
21
|
+
type RelationMedia = {
|
|
22
|
+
src?: string;
|
|
23
|
+
alt?: string;
|
|
24
|
+
icon?: LucideIcon;
|
|
25
|
+
};
|
|
26
|
+
export type RelationCardProps = {
|
|
27
|
+
name: string;
|
|
28
|
+
secondary?: ReactNode;
|
|
29
|
+
media?: RelationMedia;
|
|
30
|
+
summary?: RelationSummary;
|
|
31
|
+
/** «Abrir»: go to the related record. */
|
|
32
|
+
onOpen?: () => void;
|
|
33
|
+
/** The record's address, when it has one: «Abrir» becomes a real link. */
|
|
34
|
+
href?: string;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* The summary of a related record: picture, title, its tag, a handful of facts and «Abrir».
|
|
38
|
+
*
|
|
39
|
+
* Context: it answers the question a person has about the customer of an invoice without
|
|
40
|
+
* leaving the invoice — «does it owe us?», «what is its NIT?» — and, when the answer is
|
|
41
|
+
* «I need more», one press takes them to the record. It is what floats from a
|
|
42
|
+
* `RelationValue`, and it is exported on its own for a side panel.
|
|
43
|
+
*/
|
|
44
|
+
export declare function RelationCard({ name, secondary, media, summary, onOpen, href, }: RelationCardProps): import("react").JSX.Element;
|
|
45
|
+
export type RelationValueProps = {
|
|
46
|
+
/** The related record's name. Empty, `null` or absent = no record: said, in muted. */
|
|
47
|
+
name?: string | null;
|
|
48
|
+
/** What tells it apart: a NIT, a code, a city. Small and quiet, under the name. */
|
|
49
|
+
secondary?: ReactNode;
|
|
50
|
+
/** Its picture or icon, on the left. */
|
|
51
|
+
media?: RelationMedia;
|
|
52
|
+
/** Go to the related record. The name becomes a link. */
|
|
53
|
+
onOpen?: () => void;
|
|
54
|
+
/** The related record's address: the name becomes a real `<a>` (middle-click, copy). */
|
|
55
|
+
href?: string;
|
|
56
|
+
/** Present = an info button next to the name opens the `RelationCard`. */
|
|
57
|
+
summary?: RelationSummary;
|
|
58
|
+
/** The control that EDITS this relation — a `Picker` — drawn instead in edit mode. */
|
|
59
|
+
editor?: ReactNode;
|
|
60
|
+
/** Forces the mode; by default the form's. Only matters with `editor`. */
|
|
61
|
+
mode?: FormMode;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* A many2one as a value: the related record's name as a link, with its secondary line and
|
|
65
|
+
* picture, and its summary one press away. Works in a table cell, a list row, a `Record.Facts`
|
|
66
|
+
* value and a form field (read mode; pass the `Picker` as `editor` for edit mode).
|
|
67
|
+
*/
|
|
68
|
+
export declare function RelationValue({ name, secondary, media, onOpen, href, summary, editor, mode, }: RelationValueProps): string | number | bigint | boolean | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | import("react").JSX.Element | null | undefined;
|
|
69
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { fillCopy, useUiCopy } from "../../foundations/copy.js";
|
|
4
|
+
import { cn, focusRing } from "../../foundations/variants.js";
|
|
5
|
+
import { Button, ButtonLink, IconButton } from "../button/Button.js";
|
|
6
|
+
import { CellStack, Thumb } from "../cell/CellStack.js";
|
|
7
|
+
import { FieldIdsContext } from "../field/context.js";
|
|
8
|
+
import { useFormMode } from "../field/mode.js";
|
|
9
|
+
import { PopoverTrigger } from "../overlay/PopoverTrigger.js";
|
|
10
|
+
import { Tag } from "../tag/Tag.js";
|
|
11
|
+
import { ArrowRight, Info } from "lucide-react";
|
|
12
|
+
import { useContext } from "react";
|
|
13
|
+
import { Link } from "react-aria-components";
|
|
14
|
+
/**
|
|
15
|
+
* The summary of a related record: picture, title, its tag, a handful of facts and «Abrir».
|
|
16
|
+
*
|
|
17
|
+
* Context: it answers the question a person has about the customer of an invoice without
|
|
18
|
+
* leaving the invoice — «does it owe us?», «what is its NIT?» — and, when the answer is
|
|
19
|
+
* «I need more», one press takes them to the record. It is what floats from a
|
|
20
|
+
* `RelationValue`, and it is exported on its own for a side panel.
|
|
21
|
+
*/
|
|
22
|
+
export function RelationCard({ name, secondary, media, summary = {}, onOpen, href, }) {
|
|
23
|
+
const copy = useUiCopy();
|
|
24
|
+
const title = summary.title ?? name;
|
|
25
|
+
return (
|
|
26
|
+
/* React events bubble through the popover's portal to the React parents of the link —
|
|
27
|
+
a table row that opens on click among them. A click inside the card stays here. */
|
|
28
|
+
_jsxs("div", { className: "flex w-72 max-w-full flex-col gap-3", onClick: (e) => e.stopPropagation(), children: [_jsxs("div", { className: "flex min-w-0 items-start gap-3", children: [media === undefined ? null : (_jsx(Thumb, { size: "lg", ...(media.src === undefined ? {} : { src: media.src }), ...(media.alt === undefined ? {} : { alt: media.alt }), ...(media.icon === undefined ? {} : { icon: media.icon }) })), _jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-1", children: [_jsx("span", { className: "text-lead leading-tight font-semibold text-ink", children: title }), secondary === undefined ? null : (_jsx("span", { className: "truncate text-micro tabular-nums text-muted", children: secondary })), summary.tag === undefined ? null : (_jsx("span", { className: "flex", children: _jsx(Tag, { tone: summary.tag.tone ?? "neutral", children: summary.tag.label }) }))] })] }), summary.facts === undefined || summary.facts.length === 0 ? null : (_jsx("dl", { className: "grid grid-cols-2 gap-x-4 gap-y-2 border-t border-line pt-3", children: summary.facts.map((fact) => (_jsxs("div", { className: "flex min-w-0 flex-col", children: [_jsx("dt", { className: "truncate text-micro text-muted", children: fact.label }), _jsx("dd", { className: "truncate text-meta tabular-nums text-ink", children: fact.value })] }, fact.label))) })), href !== undefined ? (_jsx("div", { className: "flex justify-end border-t border-line pt-2", children: _jsx(ButtonLink, { href: href, role: "ghost", size: "sm", icon: ArrowRight, ...(onOpen === undefined ? {} : { onClick: onOpen }), children: copy.relation.open }) })) : onOpen !== undefined ? (_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.relation.open }) })) : null] }));
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* A many2one as a value: the related record's name as a link, with its secondary line and
|
|
32
|
+
* picture, and its summary one press away. Works in a table cell, a list row, a `Record.Facts`
|
|
33
|
+
* value and a form field (read mode; pass the `Picker` as `editor` for edit mode).
|
|
34
|
+
*/
|
|
35
|
+
export function RelationValue({ name, secondary, media, onOpen, href, summary, editor, mode, }) {
|
|
36
|
+
const copy = useUiCopy();
|
|
37
|
+
const formMode = useFormMode(mode);
|
|
38
|
+
/* Inside a `Field` the value takes the control's height, so a form that goes from
|
|
39
|
+
editing to reading does not move (the same rule as `ReadValue`). */
|
|
40
|
+
const inField = useContext(FieldIdsContext) !== null;
|
|
41
|
+
if (editor !== undefined && formMode === "edit")
|
|
42
|
+
return _jsx(_Fragment, { children: editor });
|
|
43
|
+
const frame = (child) => inField ? (_jsx("span", { className: "flex min-h-(--size-control-md) min-w-0 items-center", children: child })) : (child);
|
|
44
|
+
if (name === undefined || name === null || name === "") {
|
|
45
|
+
return frame(_jsx("span", { className: "text-meta text-muted italic", children: copy.relation.none }));
|
|
46
|
+
}
|
|
47
|
+
const goes = href !== undefined || onOpen !== undefined;
|
|
48
|
+
const primary = goes ? (_jsx(Link, { ...(href === undefined ? {} : { href }), ...(onOpen === undefined ? {} : { onPress: onOpen }), className: ({ isFocusVisible }) => cn("cursor-pointer rounded-mark text-primary no-underline underline-offset-2 hover:underline", focusRing({ isFocusVisible })), children: name })) : (name);
|
|
49
|
+
return frame(_jsxs("span", { className: "inline-flex max-w-full min-w-0 items-center gap-1", children: [_jsx(CellStack, { primary: primary, ...(secondary === undefined ? {} : { secondary }), ...(media === undefined ? {} : { media }) }), summary === undefined ? null : (_jsx(PopoverTrigger, { label: fillCopy(copy.relationCard.summary, { name }), trigger: _jsx(IconButton, { icon: Info, label: fillCopy(copy.relationCard.summary, { name }), size: "sm" }), children: (close) => (_jsx(RelationCard, { name: name, summary: summary, ...(secondary === undefined ? {} : { secondary }), ...(media === undefined ? {} : { media }), ...(href === undefined ? {} : { href }), ...(onOpen === undefined
|
|
50
|
+
? {}
|
|
51
|
+
: {
|
|
52
|
+
onOpen: () => {
|
|
53
|
+
close();
|
|
54
|
+
onOpen();
|
|
55
|
+
},
|
|
56
|
+
}) })) }))] }));
|
|
57
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* The money summary of a document — subtotal, taxes, discounts and the TOTAL — under its
|
|
4
|
+
* lines, aligned to the right.
|
|
5
|
+
*
|
|
6
|
+
* Context: written as two more rows of the lines table, «IVA» and «Total» were the same size
|
|
7
|
+
* and weight as a product line, and the one number the document exists for had to be found.
|
|
8
|
+
* Every invoice people have ever read puts the summary apart, on the right, with the total
|
|
9
|
+
* last and largest: following that convention is reading the document the way it is already
|
|
10
|
+
* read (Jakob's law).
|
|
11
|
+
*
|
|
12
|
+
* Labels left and figures right of a narrow column, both in tabular figures, so the amounts
|
|
13
|
+
* line up digit by digit. The `total` row closes the block: a rule above it, and the figure
|
|
14
|
+
* at the title size.
|
|
15
|
+
*/
|
|
16
|
+
export type TotalsRow = {
|
|
17
|
+
label: ReactNode;
|
|
18
|
+
value: ReactNode;
|
|
19
|
+
/** A note under the label — «incluido en el precio», «sobre Bs. 3.601,80». */
|
|
20
|
+
detail?: ReactNode;
|
|
21
|
+
/** The row the block exists for. Usually one, and last. */
|
|
22
|
+
total?: boolean;
|
|
23
|
+
};
|
|
24
|
+
export declare function Totals({ rows }: {
|
|
25
|
+
rows: readonly TotalsRow[];
|
|
26
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
export function Totals({ rows }) {
|
|
3
|
+
return (_jsx("dl", { className: "ml-auto flex w-full max-w-(--container-search) flex-col gap-1.5", children: rows.map((row, i) => row.total === true ? (_jsxs("div", { className: "mt-1 flex items-baseline justify-between gap-4 border-t border-line-strong pt-2.5", children: [_jsx("dt", { className: "text-body font-semibold text-ink", children: row.label }), _jsx("dd", { className: "text-title font-semibold tabular-nums text-ink", children: row.value })] }, i)) : (_jsxs("div", { className: "flex items-baseline justify-between gap-4", children: [_jsxs("dt", { className: "flex flex-col text-meta text-muted", children: [row.label, row.detail === undefined ? null : (_jsx("span", { className: "text-micro text-muted", children: row.detail }))] }), _jsx("dd", { className: "text-meta tabular-nums text-ink", children: row.value })] }, i))) }));
|
|
4
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { Tone } from "../../foundations/types.js";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
export declare const formatCount: (value: number, locale?: string) => string;
|
|
4
|
+
export declare const formatNumber: (value: number, locale?: string) => string;
|
|
5
|
+
export declare const formatDistance: (value: number, locale?: string) => string;
|
|
6
|
+
/** A label with its number, for the glance at the top of a view. */
|
|
7
|
+
export declare function KeyValue({ label, value }: {
|
|
8
|
+
label: string;
|
|
9
|
+
value: ReactNode;
|
|
10
|
+
}): import("react").JSX.Element;
|
|
11
|
+
/**
|
|
12
|
+
* A number with its unit: right aligned, with tabular figures.
|
|
13
|
+
*
|
|
14
|
+
* Context: this is the detail a table's look depends on. Tabular figures give every digit
|
|
15
|
+
* the same width, so `32.561` lines up across rows exactly as it did in monospace — without
|
|
16
|
+
* making a business report look like a terminal. The unit stays in the same face so the
|
|
17
|
+
* digits keep their column.
|
|
18
|
+
*/
|
|
19
|
+
export declare function Numeric({ value, unit, format, }: {
|
|
20
|
+
value: number;
|
|
21
|
+
unit?: string;
|
|
22
|
+
format?: "integer" | "decimal" | "distance";
|
|
23
|
+
}): import("react").JSX.Element;
|
|
24
|
+
/**
|
|
25
|
+
* A real bar, never a spinner.
|
|
26
|
+
*
|
|
27
|
+
* Context: a spinner says «wait» and doesn't say how much; a bar says both
|
|
28
|
+
* things.
|
|
29
|
+
*
|
|
30
|
+
* ── Why the fill is FLAT and the gradient is asked for ──────────────
|
|
31
|
+
*
|
|
32
|
+
* The bar always lived with `bar-brand`, the blue→gold gradient. At 3 px and at
|
|
33
|
+
* 62 % that doesn't read as a brand: it reads as a blue bar with a broken tip.
|
|
34
|
+
* A gradient needs length to be a gradient, and a bar inside a list row doesn't
|
|
35
|
+
* have it.
|
|
36
|
+
*
|
|
37
|
+
* Now the fill is a flat tone —and the tone SAYS something: `good` finished,
|
|
38
|
+
* `warn` is running slow, `bad` failed— and the gradient is asked for with
|
|
39
|
+
* `tone="brand"`, which is the long bar of a live import: there it does have room
|
|
40
|
+
* to be read.
|
|
41
|
+
*
|
|
42
|
+
* And it got fatter, from 3 px to 6 px. At 3 px it isn't a bar, it's a line: it
|
|
43
|
+
* can't be told apart from a separator and the fill isn't visible unless it goes
|
|
44
|
+
* past 20 %.
|
|
45
|
+
*/
|
|
46
|
+
export declare function ProgressBar({ value, label, width, tone,
|
|
47
|
+
/** The percentage next to it. It is turned off when the bar lives inside
|
|
48
|
+
* something that already says the figure —a cell with its column— and it
|
|
49
|
+
* would only mean reading it twice. */
|
|
50
|
+
showValue, }: {
|
|
51
|
+
/** 0 to 100. */
|
|
52
|
+
value: number;
|
|
53
|
+
label: string;
|
|
54
|
+
/** The activity bar wants to be longer than a list row; there is no reason for
|
|
55
|
+
* it to be the same number on both sides. */
|
|
56
|
+
width?: string;
|
|
57
|
+
tone?: Tone | "brand";
|
|
58
|
+
showValue?: boolean;
|
|
59
|
+
}): import("react").JSX.Element;
|
|
60
|
+
/**
|
|
61
|
+
* The content of a data cell, with what the system decides and the view does not.
|
|
62
|
+
*
|
|
63
|
+
* Context: `""` is a TRULY EMPTY cell and not a missing datum — they are two
|
|
64
|
+
* different things and the engine tells them apart, so the interface does too. A
|
|
65
|
+
* missing one is passed as `null` and drawn with its mark; an empty string is a
|
|
66
|
+
* value somebody typed, and it gets one too.
|
|
67
|
+
*
|
|
68
|
+
* It used to live in the application, where it couldn't serve the other five
|
|
69
|
+
* tables.
|
|
70
|
+
*/
|
|
71
|
+
export declare function Cell({ value,
|
|
72
|
+
/** From how many characters it gets truncated. A 64 character identifier
|
|
73
|
+
* breaks the width of the whole table for a column nobody reads in full. */
|
|
74
|
+
max, }: {
|
|
75
|
+
value: string | number | null | undefined;
|
|
76
|
+
max?: number;
|
|
77
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { currentCopyLocale, useUiCopy } from "../../foundations/copy.js";
|
|
4
|
+
/**
|
|
5
|
+
* Context: the mono/sans rule decides everything in here. The label goes in
|
|
6
|
+
* PROPORTIONAL —«orígenes», «mediana» are words, not data— and the value in
|
|
7
|
+
* tabular monospace, which is what makes four of these in a row keep their
|
|
8
|
+
* digits in a column.
|
|
9
|
+
*
|
|
10
|
+
* Formats follow the session locale, not a baked-in Spanish `Intl`.
|
|
11
|
+
*/
|
|
12
|
+
function integerFormat(locale) {
|
|
13
|
+
return new Intl.NumberFormat(locale, { maximumFractionDigits: 0 });
|
|
14
|
+
}
|
|
15
|
+
function decimalFormat(locale) {
|
|
16
|
+
return new Intl.NumberFormat(locale, { maximumFractionDigits: 2 });
|
|
17
|
+
}
|
|
18
|
+
/** Four decimals and not two for distances — they live near zero
|
|
19
|
+
* (`0.0696`, `0.0149`) and with two they turn into `0.07` and `0.01`, which is
|
|
20
|
+
* losing exactly the part that tells one column from another. */
|
|
21
|
+
function distanceFormat(locale) {
|
|
22
|
+
return new Intl.NumberFormat(locale, {
|
|
23
|
+
minimumFractionDigits: 4,
|
|
24
|
+
maximumFractionDigits: 4,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
export const formatCount = (value, locale = currentCopyLocale()) => integerFormat(locale).format(value);
|
|
28
|
+
export const formatNumber = (value, locale = currentCopyLocale()) => decimalFormat(locale).format(value);
|
|
29
|
+
export const formatDistance = (value, locale = currentCopyLocale()) => distanceFormat(locale).format(value);
|
|
30
|
+
/** A label with its number, for the glance at the top of a view. */
|
|
31
|
+
export function KeyValue({ label, value }) {
|
|
32
|
+
return (_jsxs("div", { className: "min-w-0", children: [_jsx("dt", { className: "text-micro text-muted", children: label }), _jsx("dd", { className: "mt-0.5 truncate text-lead tabular-nums text-ink", children: value })] }));
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* A number with its unit: right aligned, with tabular figures.
|
|
36
|
+
*
|
|
37
|
+
* Context: this is the detail a table's look depends on. Tabular figures give every digit
|
|
38
|
+
* the same width, so `32.561` lines up across rows exactly as it did in monospace — without
|
|
39
|
+
* making a business report look like a terminal. The unit stays in the same face so the
|
|
40
|
+
* digits keep their column.
|
|
41
|
+
*/
|
|
42
|
+
export function Numeric({ value, unit, format = "integer", }) {
|
|
43
|
+
const text = format === "distance"
|
|
44
|
+
? formatDistance(value)
|
|
45
|
+
: format === "decimal"
|
|
46
|
+
? formatNumber(value)
|
|
47
|
+
: formatCount(value);
|
|
48
|
+
return (_jsxs("span", { className: "block text-right text-body tabular-nums whitespace-nowrap text-ink", children: [_jsx("span", { className: "tabular-nums", children: text }), unit === undefined ? null : _jsx("span", { className: "ml-1 text-muted", children: unit })] }));
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* A real bar, never a spinner.
|
|
52
|
+
*
|
|
53
|
+
* Context: a spinner says «wait» and doesn't say how much; a bar says both
|
|
54
|
+
* things.
|
|
55
|
+
*
|
|
56
|
+
* ── Why the fill is FLAT and the gradient is asked for ──────────────
|
|
57
|
+
*
|
|
58
|
+
* The bar always lived with `bar-brand`, the blue→gold gradient. At 3 px and at
|
|
59
|
+
* 62 % that doesn't read as a brand: it reads as a blue bar with a broken tip.
|
|
60
|
+
* A gradient needs length to be a gradient, and a bar inside a list row doesn't
|
|
61
|
+
* have it.
|
|
62
|
+
*
|
|
63
|
+
* Now the fill is a flat tone —and the tone SAYS something: `good` finished,
|
|
64
|
+
* `warn` is running slow, `bad` failed— and the gradient is asked for with
|
|
65
|
+
* `tone="brand"`, which is the long bar of a live import: there it does have room
|
|
66
|
+
* to be read.
|
|
67
|
+
*
|
|
68
|
+
* And it got fatter, from 3 px to 6 px. At 3 px it isn't a bar, it's a line: it
|
|
69
|
+
* can't be told apart from a separator and the fill isn't visible unless it goes
|
|
70
|
+
* past 20 %.
|
|
71
|
+
*/
|
|
72
|
+
export function ProgressBar({ value, label, width = "w-24", tone = "brand",
|
|
73
|
+
/** The percentage next to it. It is turned off when the bar lives inside
|
|
74
|
+
* something that already says the figure —a cell with its column— and it
|
|
75
|
+
* would only mean reading it twice. */
|
|
76
|
+
showValue = true, }) {
|
|
77
|
+
const clamped = Math.max(0, Math.min(100, Math.round(value)));
|
|
78
|
+
const fill = tone === "brand" ? "bar-brand" : FILL[tone];
|
|
79
|
+
return (_jsxs("span", { className: "flex items-center gap-2", children: [_jsx("span", { className: `h-(--size-track) overflow-hidden rounded-pill bg-ink/10 ${width}`, role: "progressbar", "aria-valuenow": clamped, "aria-valuemin": 0, "aria-valuemax": 100, "aria-label": label, children: _jsx("span", { className: `block h-full rounded-pill transition-[width] duration-(--duration-panel) ease-out ${fill}`, style: { width: `${clamped}%` } }) }), showValue ? (_jsxs("span", { className: "text-meta tabular-nums text-muted", children: [clamped, "%"] })) : null] }));
|
|
80
|
+
}
|
|
81
|
+
const FILL = {
|
|
82
|
+
neutral: "bg-muted",
|
|
83
|
+
info: "bg-info",
|
|
84
|
+
good: "bg-good",
|
|
85
|
+
warn: "bg-warn",
|
|
86
|
+
bad: "bg-bad",
|
|
87
|
+
accent: "bg-accent",
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* The content of a data cell, with what the system decides and the view does not.
|
|
91
|
+
*
|
|
92
|
+
* Context: `""` is a TRULY EMPTY cell and not a missing datum — they are two
|
|
93
|
+
* different things and the engine tells them apart, so the interface does too. A
|
|
94
|
+
* missing one is passed as `null` and drawn with its mark; an empty string is a
|
|
95
|
+
* value somebody typed, and it gets one too.
|
|
96
|
+
*
|
|
97
|
+
* It used to live in the application, where it couldn't serve the other five
|
|
98
|
+
* tables.
|
|
99
|
+
*/
|
|
100
|
+
export function Cell({ value,
|
|
101
|
+
/** From how many characters it gets truncated. A 64 character identifier
|
|
102
|
+
* breaks the width of the whole table for a column nobody reads in full. */
|
|
103
|
+
max = 48, }) {
|
|
104
|
+
const copy = useUiCopy();
|
|
105
|
+
if (value === null || value === undefined) {
|
|
106
|
+
return (_jsx("span", { className: "text-muted italic", title: copy.cell.missing, children: copy.cell.missing }));
|
|
107
|
+
}
|
|
108
|
+
if (typeof value === "number")
|
|
109
|
+
return _jsx(Numeric, { value: value, format: "decimal" });
|
|
110
|
+
if (value === "") {
|
|
111
|
+
return (_jsx("span", { className: "text-line-strong", title: copy.cell.empty, children: "\u00B7" }));
|
|
112
|
+
}
|
|
113
|
+
if (value.length <= max)
|
|
114
|
+
return _jsx(_Fragment, { children: value });
|
|
115
|
+
/* It is truncated with the whole value in the `title`: whoever needs the full
|
|
116
|
+
id has it a hover away, and whoever is reading the column next to it doesn't
|
|
117
|
+
pay for its width. */
|
|
118
|
+
return (_jsxs("span", { title: value, className: "truncate", children: [value.slice(0, max), "\u2026"] }));
|
|
119
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { Copyable, type CopyableProps } from "./Copyable.js";
|
|
2
|
+
export { DateValue, formatDateTime, formatRelative, } from "./Fecha.js";
|
|
3
|
+
export { Meter, type MeterLevel, meterLevel, type MeterProps, type MeterThresholds, } from "./Meter.js";
|
|
4
|
+
export { Metric, type MetricProps, Metrics } from "./Metric.js";
|
|
5
|
+
export { Avatar, type AvatarSize, ContactCard, type ContactCardProps, type PersonContact, UserChip, type UserChipProps, UserStack, } from "./Person.js";
|
|
6
|
+
export { RelationCard, type RelationCardProps, type RelationSummary, RelationValue, type RelationValueProps, } from "./Relation.js";
|
|
7
|
+
export { Totals, type TotalsRow } from "./Totals.js";
|
|
8
|
+
export { Cell, formatCount, formatDistance, formatNumber, KeyValue, Numeric, ProgressBar, } from "./Value.js";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/* Show a number or a datum so that it reads as such and not as text. */
|
|
2
|
+
export { Copyable } from "./Copyable.js";
|
|
3
|
+
export { DateValue, formatDateTime, formatRelative, } from "./Fecha.js";
|
|
4
|
+
export { Meter, meterLevel, } from "./Meter.js";
|
|
5
|
+
export { Metric, Metrics } from "./Metric.js";
|
|
6
|
+
export { Avatar, ContactCard, UserChip, UserStack, } from "./Person.js";
|
|
7
|
+
export { RelationCard, RelationValue, } from "./Relation.js";
|
|
8
|
+
export { Totals } from "./Totals.js";
|
|
9
|
+
export { Cell, formatCount, formatDistance, formatNumber, KeyValue, Numeric, ProgressBar, } from "./Value.js";
|