@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,80 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { HeaderSlotContext } from "./headerSlot.js";
|
|
4
|
+
import { stickyTop, useStickyLayer } from "../../foundations/sticky.js";
|
|
5
|
+
import { useState } from "react";
|
|
6
|
+
/* ==================================================================== *
|
|
7
|
+
* THE APPLICATION FRAME — the three bands, and nothing between them.
|
|
8
|
+
*
|
|
9
|
+
* ┌───────────────────────────────────────────────────────┐
|
|
10
|
+
* │ menu │ ← sticky
|
|
11
|
+
* ├───────────────────────────────────────────────────────┤ ← no seam
|
|
12
|
+
* │ view │
|
|
13
|
+
* ├───────────────────────────────────────────────────────┤ ← no seam
|
|
14
|
+
* │ status │
|
|
15
|
+
* └───────────────────────────────────────────────────────┘
|
|
16
|
+
*
|
|
17
|
+
* It exists because the frame was handwritten in the application, with a
|
|
18
|
+
* `px-3 py-2.5` here and a `bg-surface` there, and every hole fix opened
|
|
19
|
+
* another one in another view. The decisions below are taken ONCE and hold
|
|
20
|
+
* for every view — list, record, form — which is what makes them look alike
|
|
21
|
+
* without anybody remembering to copy them.
|
|
22
|
+
*
|
|
23
|
+
* ── What this frame decides, and why ────────────────────────────────
|
|
24
|
+
*
|
|
25
|
+
* **The three bands touch.** No padding, no margin, no `gap` between the
|
|
26
|
+
* menu, the view and the status. Any gap here reads as a white border around
|
|
27
|
+
* the content: the dark band of a list stops reading as the continuation of
|
|
28
|
+
* the menu and starts looking like a loose card.
|
|
29
|
+
*
|
|
30
|
+
* **The view does not get padding from the frame.** Inner air is put by the
|
|
31
|
+
* view, which is the only one that knows whether it is drawing a table to
|
|
32
|
+
* both edges or a narrow form. A padding here would impose it on both.
|
|
33
|
+
*
|
|
34
|
+
* **Nothing forces the window height.** A short view ends where its content
|
|
35
|
+
* ends: without cascading `h-full` there is no leftover white under the
|
|
36
|
+
* status, and the page scrolls as a whole like any page.
|
|
37
|
+
*
|
|
38
|
+
* ── The sidebar ─────────────────────────────────────────────────────
|
|
39
|
+
*
|
|
40
|
+
* ┌───────────────────────────────────────────────────────┐
|
|
41
|
+
* │ menu │ ← sticky
|
|
42
|
+
* ├──────────┬────────────────────────────────────────────┤
|
|
43
|
+
* │ sidebar │ view │
|
|
44
|
+
* │ (sticky) │ …scrolls with the page… │
|
|
45
|
+
* ├──────────┴────────────────────────────────────────────┤
|
|
46
|
+
* │ status │
|
|
47
|
+
* └───────────────────────────────────────────────────────┘
|
|
48
|
+
*
|
|
49
|
+
* It sticks UNDER the menu, not at the top of the window: it reads the same
|
|
50
|
+
* measured `--sticky-top` the view's own sticky bands read, so a menu that
|
|
51
|
+
* wraps to two lines pushes the sidebar down with it. It is as tall as what
|
|
52
|
+
* is left of the window and scrolls on its own, so a long module never takes
|
|
53
|
+
* the page with it. The page still scrolls as a whole — only the view moves.
|
|
54
|
+
* Without a `sidebar` the frame is exactly what it was.
|
|
55
|
+
* ==================================================================== */
|
|
56
|
+
export function AppFrame({
|
|
57
|
+
/** The bar at the top. It stays in view while scrolling. */
|
|
58
|
+
menu,
|
|
59
|
+
/** The band at the bottom. It goes AFTER the content, not pinned to the
|
|
60
|
+
* floor of the window — pinning left a hole between it and a short view. */
|
|
61
|
+
status,
|
|
62
|
+
/** The module's navigation, on the left of the view: a `NavigationTree`. */
|
|
63
|
+
sidebar, children, }) {
|
|
64
|
+
/* La barra de arriba MIDE lo que ocupa y se lo pasa a lo de abajo: la barra
|
|
65
|
+
de una lista se pega debajo de ella, y el encabezado de una tabla debajo de
|
|
66
|
+
las dos. Ninguna de las tres tiene alto fijo — ver `system/sticky.tsx`. */
|
|
67
|
+
const bar = useStickyLayer();
|
|
68
|
+
/* The slot for a view's header (see `headerSlot.ts`): full width, stuck under the
|
|
69
|
+
application bar, and it pushes the stack down for the sidebar and the table header. */
|
|
70
|
+
const [slot, setSlot] = useState(null);
|
|
71
|
+
const head = useStickyLayer();
|
|
72
|
+
return (
|
|
73
|
+
/* The frame owns the page: its background and its full height. A screen built inside
|
|
74
|
+
it is only composition — before, every page had to wrap itself in a `div` with the
|
|
75
|
+
ground color and `min-h-dvh`, and the one that forgot ended where its content did. */
|
|
76
|
+
_jsxs("div", { className: "relative flex min-h-dvh min-w-0 flex-col bg-ground", children: [menu === undefined ? null : (_jsx("div", { ref: bar.ref, className: "sticky top-0 z-40", children: menu })), sidebar === undefined ? (_jsx("main", { className: "flex min-w-0 flex-1 flex-col", style: bar.below, children: children })) : (_jsx(HeaderSlotContext, { value: slot, children: _jsxs("div", { className: "flex min-w-0 flex-1 flex-col", style: bar.below, children: [_jsx("div", { ref: (node) => {
|
|
77
|
+
head.ref.current = node;
|
|
78
|
+
setSlot(node);
|
|
79
|
+
}, className: "sticky z-30 min-w-0", style: head.self }), _jsxs("div", { className: "flex min-w-0 flex-1 items-start", style: head.below, children: [_jsx("aside", { className: "sticky w-(--container-sidebar) shrink-0 self-start overflow-y-auto overscroll-contain border-r border-line bg-ground", style: { ...stickyTop, height: "calc(100dvh - var(--sticky-top, 0px))" }, children: sidebar }), _jsx("main", { className: "flex min-w-0 flex-1 flex-col self-stretch", children: children })] })] }) })), status] }));
|
|
80
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { type MenuGroup } from "../../primitives/menu/Menu.js";
|
|
2
|
+
import { type LucideIcon } from "lucide-react";
|
|
3
|
+
import { type ReactNode } from "react";
|
|
4
|
+
export interface AppEntry {
|
|
5
|
+
id: string;
|
|
6
|
+
label: string;
|
|
7
|
+
icon: LucideIcon;
|
|
8
|
+
/** One line: what this app is for. It shows in the switcher. */
|
|
9
|
+
hint?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface TopMenu {
|
|
12
|
+
id: string;
|
|
13
|
+
label: string;
|
|
14
|
+
groups: readonly MenuGroup[];
|
|
15
|
+
}
|
|
16
|
+
export declare function TopBar({ app, apps, currentApp, onAppSelect, menus, onMenuSelect, systray, user, onUserSelect, userGroups, }: {
|
|
17
|
+
/** The product's name, when there is ONE product. It is drawn like the
|
|
18
|
+
* current app of the switcher —same icon, same weight— but without the
|
|
19
|
+
* switcher: nothing to switch to. */
|
|
20
|
+
app?: {
|
|
21
|
+
label: string;
|
|
22
|
+
icon?: LucideIcon;
|
|
23
|
+
};
|
|
24
|
+
/** Everything that exists. Without this there is no switcher — a product with
|
|
25
|
+
* a single app does not need one, and drawing it empty promises something
|
|
26
|
+
* that is not there. */
|
|
27
|
+
apps?: readonly AppEntry[];
|
|
28
|
+
currentApp?: string;
|
|
29
|
+
onAppSelect?: (id: string) => void;
|
|
30
|
+
/** The menus OF the current app. */
|
|
31
|
+
menus?: readonly TopMenu[];
|
|
32
|
+
onMenuSelect?: (menuId: string, optionId: string) => void;
|
|
33
|
+
/** What happened while I was not looking: activity, alerts. Icons with a count. */
|
|
34
|
+
systray?: ReactNode;
|
|
35
|
+
user?: {
|
|
36
|
+
name: string;
|
|
37
|
+
detail?: string;
|
|
38
|
+
};
|
|
39
|
+
userGroups?: readonly MenuGroup[];
|
|
40
|
+
onUserSelect?: (id: string) => void;
|
|
41
|
+
}): import("react").JSX.Element;
|
|
42
|
+
/** The app switcher: a grid, not a list.
|
|
43
|
+
*
|
|
44
|
+
* Context: there are six or eight, each with an icon, and they are picked by
|
|
45
|
+
* recognition rather than by reading. A vertical list forces you to read all
|
|
46
|
+
* six to find the one you already know; the grid shows them at once and the
|
|
47
|
+
* position gets learned. */
|
|
48
|
+
export declare function AppSwitcher({ apps, current, onSelect, }: {
|
|
49
|
+
apps: readonly AppEntry[];
|
|
50
|
+
current?: string;
|
|
51
|
+
onSelect?: (id: string) => void;
|
|
52
|
+
}): import("react").JSX.Element;
|
|
53
|
+
/** A systray icon with its count.
|
|
54
|
+
*
|
|
55
|
+
* Zero is NOT drawn: a bell with a «0» next to it forces you to read a number
|
|
56
|
+
* to discover there is nothing. With no count you can already see there is
|
|
57
|
+
* nothing. */
|
|
58
|
+
export declare function SystrayButton({ icon: Icon, label, count, tone, onClick, active, }: {
|
|
59
|
+
icon: LucideIcon;
|
|
60
|
+
label: string;
|
|
61
|
+
count?: number;
|
|
62
|
+
tone?: "info" | "warn" | "bad";
|
|
63
|
+
onClick?: () => void;
|
|
64
|
+
active?: boolean;
|
|
65
|
+
}): import("react").JSX.Element;
|
|
66
|
+
/** Who I am and how I leave. The name is drawn when there is width; when narrow
|
|
67
|
+
* only the avatar stays, which is what gets recognised without reading. */
|
|
68
|
+
export declare function UserMenu({ name, detail, groups, onSelect, }: {
|
|
69
|
+
name: string;
|
|
70
|
+
detail?: string;
|
|
71
|
+
groups: readonly MenuGroup[];
|
|
72
|
+
onSelect?: (id: string) => void;
|
|
73
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,54 @@
|
|
|
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 { SurfaceProvider } from "../../foundations/surface.js";
|
|
5
|
+
import { Menu } from "../../primitives/menu/Menu.js";
|
|
6
|
+
import { Avatar } from "../../primitives/value/Person.js";
|
|
7
|
+
import { Grid3x3 } from "lucide-react";
|
|
8
|
+
import { useState } from "react";
|
|
9
|
+
export function TopBar({ app, apps, currentApp, onAppSelect, menus, onMenuSelect, systray, user, onUserSelect, userGroups, }) {
|
|
10
|
+
const copy = useUiCopy();
|
|
11
|
+
const actual = apps?.find((a) => a.id === currentApp);
|
|
12
|
+
const brand = app ?? actual;
|
|
13
|
+
return (_jsx(SurfaceProvider, { value: "chrome", children: _jsxs("header", { className: "flex h-(--size-appbar) min-w-0 items-center gap-1 border-b border-chrome-edge bg-chrome px-2", children: [apps === undefined || apps.length === 0 ? null : (_jsx(AppSwitcher, { apps: apps, current: currentApp, ...(onAppSelect === undefined ? {} : { onSelect: onAppSelect }) })), brand === undefined ? null : (_jsxs("span", { className: "flex shrink-0 items-center gap-2 pr-2 pl-1", children: [brand.icon === undefined ? null : (_jsx(brand.icon, { className: "size-(--size-icon-md) text-chrome-link", strokeWidth: 2, "aria-hidden": true })), _jsx("span", { className: "text-lead font-medium text-chrome-ink", children: brand.label })] })), menus === undefined ? null : (_jsx("nav", { className: "flex min-w-0 items-center gap-0.5 overflow-hidden", "aria-label": copy.shell.menus, children: menus.map((m) => (_jsx(Menu, { label: m.label, groups: m.groups, surface: "chrome", variant: "text", chevron: false, onSelect: (id) => onMenuSelect?.(m.id, id) }, m.id))) })), _jsxs("div", { className: "ml-auto flex shrink-0 items-center gap-1 pl-2", children: [systray, user === undefined ? null : (_jsx(UserMenu, { name: user.name, ...(user.detail === undefined ? {} : { detail: user.detail }), groups: userGroups ?? [], ...(onUserSelect === undefined ? {} : { onSelect: onUserSelect }) }))] })] }) }));
|
|
14
|
+
}
|
|
15
|
+
/** The app switcher: a grid, not a list.
|
|
16
|
+
*
|
|
17
|
+
* Context: there are six or eight, each with an icon, and they are picked by
|
|
18
|
+
* recognition rather than by reading. A vertical list forces you to read all
|
|
19
|
+
* six to find the one you already know; the grid shows them at once and the
|
|
20
|
+
* position gets learned. */
|
|
21
|
+
export function AppSwitcher({ apps, current, onSelect, }) {
|
|
22
|
+
const [open, setOpen] = useState(false);
|
|
23
|
+
const copy = useUiCopy();
|
|
24
|
+
return (_jsxs("div", { className: "relative shrink-0", children: [_jsx("button", { type: "button", onClick: () => setOpen((v) => !v), "aria-expanded": open, "aria-haspopup": "menu", "aria-label": copy.shell.modules, title: copy.shell.modules, className: `inline-flex size-(--size-control-md) items-center justify-center rounded-pill transition-colors duration-(--duration-instant) focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-chrome-link ${open
|
|
25
|
+
? "bg-chrome-ink/12 text-chrome-ink"
|
|
26
|
+
: "text-chrome-muted hover:bg-chrome-ink/10 hover:text-chrome-ink"}`, children: _jsx(Grid3x3, { className: "size-(--size-icon-md)", strokeWidth: 2 }) }), !open ? null : (_jsxs(_Fragment, { children: [_jsx("div", { className: "fixed inset-0 z-40", onClick: () => setOpen(false), role: "presentation" }), _jsx("div", { role: "menu", className: "absolute top-full left-0 z-50 mt-1 grid w-(--container-apps) grid-cols-3 gap-1 rounded-panel border border-line bg-surface p-2 shadow-float", children: apps.map((a) => {
|
|
27
|
+
const here = a.id === current;
|
|
28
|
+
return (_jsxs("button", { type: "button", role: "menuitem", title: a.hint, onClick: () => {
|
|
29
|
+
onSelect?.(a.id);
|
|
30
|
+
setOpen(false);
|
|
31
|
+
}, className: `flex flex-col items-center gap-1.5 rounded-box px-2 py-3 text-center transition-colors duration-(--duration-instant) focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-primary ${here ? "bg-primary/8 text-primary" : "text-ink hover:bg-ink/5"}`, children: [_jsx(a.icon, { className: "size-(--size-icon-lg)", strokeWidth: 1.75, "aria-hidden": true }), _jsx("span", { className: "text-micro leading-tight", children: a.label })] }, a.id));
|
|
32
|
+
}) })] }))] }));
|
|
33
|
+
}
|
|
34
|
+
/** A systray icon with its count.
|
|
35
|
+
*
|
|
36
|
+
* Zero is NOT drawn: a bell with a «0» next to it forces you to read a number
|
|
37
|
+
* to discover there is nothing. With no count you can already see there is
|
|
38
|
+
* nothing. */
|
|
39
|
+
export function SystrayButton({ icon: Icon, label, count = 0, tone = "info", onClick, active = false, }) {
|
|
40
|
+
const PUNTO = {
|
|
41
|
+
info: "bg-chrome-link text-chrome",
|
|
42
|
+
warn: "bg-chrome-warn text-chrome",
|
|
43
|
+
bad: "bg-chrome-bad text-chrome",
|
|
44
|
+
};
|
|
45
|
+
return (_jsxs("button", { type: "button", onClick: onClick, title: count > 0 ? `${label} (${count})` : label, "aria-label": count > 0 ? `${label}: ${count}` : label, className: `relative inline-flex size-(--size-control-md) items-center justify-center rounded-pill transition-colors duration-(--duration-instant) focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-chrome-link ${active
|
|
46
|
+
? "bg-chrome-ink/12 text-chrome-ink"
|
|
47
|
+
: "text-chrome-muted hover:bg-chrome-ink/10 hover:text-chrome-ink"}`, children: [_jsx(Icon, { className: "size-(--size-icon-md)", strokeWidth: 2, "aria-hidden": true }), count <= 0 ? null : (_jsx("span", { "aria-hidden": true, className: `absolute -top-0.5 -right-0.5 inline-flex min-w-4 items-center justify-center rounded-pill px-1 text-micro leading-none font-semibold tabular-nums ${PUNTO[tone]}`, children: count > 99 ? "99+" : count }))] }));
|
|
48
|
+
}
|
|
49
|
+
/** Who I am and how I leave. The name is drawn when there is width; when narrow
|
|
50
|
+
* only the avatar stays, which is what gets recognised without reading. */
|
|
51
|
+
export function UserMenu({ name, detail, groups, onSelect, }) {
|
|
52
|
+
const copy = useUiCopy();
|
|
53
|
+
return (_jsxs("span", { className: "flex shrink-0 items-center gap-2 pl-1", children: [_jsx(Avatar, { name: name, size: "md" }), _jsxs("span", { className: "hidden min-w-0 flex-col leading-tight sm:flex", children: [_jsx("span", { className: "truncate text-meta text-chrome-ink", children: name }), detail === undefined ? null : (_jsx("span", { className: "truncate text-micro text-chrome-muted", children: detail }))] }), _jsx(Menu, { label: copy.shell.account, groups: groups, surface: "chrome", variant: "text", align: "end", iconOnly: true, ...(onSelect === undefined ? {} : { onSelect }) })] }));
|
|
54
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context: **where a view's header goes when the frame has a sidebar.**
|
|
3
|
+
*
|
|
4
|
+
* With a sidebar the screen is a grid: the application bar on top, the sidebar on the
|
|
5
|
+
* left, the view on the right. A list's bar (crumbs, search, pagination, actions, sort,
|
|
6
|
+
* presentation switcher) is the header of the SCREEN, not of the column to the right of
|
|
7
|
+
* the sidebar — drawn inside that column it started at the sidebar's edge and the
|
|
8
|
+
* sidebar, taller and darker, won the top-left of the page. The frame offers a full-width
|
|
9
|
+
* slot between the application bar and the sidebar row; a view that has a header renders
|
|
10
|
+
* it into the slot (a portal, so its context — density, surface, copy — comes along).
|
|
11
|
+
*
|
|
12
|
+
* `null` = there is no slot (no sidebar, or the slot has not mounted yet): the view draws
|
|
13
|
+
* its header where it always did. Not exported from the package: it is the contract
|
|
14
|
+
* between `AppFrame` and the views, not something an app writes.
|
|
15
|
+
*/
|
|
16
|
+
export declare const HeaderSlotContext: import("react").Context<HTMLElement | null>;
|
|
17
|
+
export declare function useHeaderSlot(): HTMLElement | null;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { createContext, useContext } from "react";
|
|
3
|
+
/**
|
|
4
|
+
* Context: **where a view's header goes when the frame has a sidebar.**
|
|
5
|
+
*
|
|
6
|
+
* With a sidebar the screen is a grid: the application bar on top, the sidebar on the
|
|
7
|
+
* left, the view on the right. A list's bar (crumbs, search, pagination, actions, sort,
|
|
8
|
+
* presentation switcher) is the header of the SCREEN, not of the column to the right of
|
|
9
|
+
* the sidebar — drawn inside that column it started at the sidebar's edge and the
|
|
10
|
+
* sidebar, taller and darker, won the top-left of the page. The frame offers a full-width
|
|
11
|
+
* slot between the application bar and the sidebar row; a view that has a header renders
|
|
12
|
+
* it into the slot (a portal, so its context — density, surface, copy — comes along).
|
|
13
|
+
*
|
|
14
|
+
* `null` = there is no slot (no sidebar, or the slot has not mounted yet): the view draws
|
|
15
|
+
* its header where it always did. Not exported from the package: it is the contract
|
|
16
|
+
* between `AppFrame` and the views, not something an app writes.
|
|
17
|
+
*/
|
|
18
|
+
export const HeaderSlotContext = createContext(null);
|
|
19
|
+
export function useHeaderSlot() {
|
|
20
|
+
return useContext(HeaderSlotContext);
|
|
21
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* The locale React Aria formats with — month names, the order of day/month/year in a date
|
|
4
|
+
* field, the first day of the week — taken from the copy table, the one locale the whole
|
|
5
|
+
* system speaks.
|
|
6
|
+
*
|
|
7
|
+
* Context: React Aria reads the browser's language when nobody tells it. An app installed
|
|
8
|
+
* in Spanish on a browser in English would show a Spanish form with a date field in
|
|
9
|
+
* month/day/year — the one place in the screen that reads in another language, and the
|
|
10
|
+
* one where a wrong order changes the data.
|
|
11
|
+
*/
|
|
12
|
+
export declare function AriaLocale({ children }: {
|
|
13
|
+
children: ReactNode;
|
|
14
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useUiCopy } from "./copy.js";
|
|
4
|
+
import { I18nProvider } from "react-aria-components";
|
|
5
|
+
/**
|
|
6
|
+
* The locale React Aria formats with — month names, the order of day/month/year in a date
|
|
7
|
+
* field, the first day of the week — taken from the copy table, the one locale the whole
|
|
8
|
+
* system speaks.
|
|
9
|
+
*
|
|
10
|
+
* Context: React Aria reads the browser's language when nobody tells it. An app installed
|
|
11
|
+
* in Spanish on a browser in English would show a Spanish form with a date field in
|
|
12
|
+
* month/day/year — the one place in the screen that reads in another language, and the
|
|
13
|
+
* one where a wrong order changes the data.
|
|
14
|
+
*/
|
|
15
|
+
export function AriaLocale({ children }) {
|
|
16
|
+
const copy = useUiCopy();
|
|
17
|
+
return _jsx(I18nProvider, { locale: copy.locale, children: children });
|
|
18
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { GroupGrain } from "./grouping.js";
|
|
2
|
+
/**
|
|
3
|
+
* Una fecha recortada a la CLAVE de su grano.
|
|
4
|
+
*
|
|
5
|
+
* El texto es el que produce el motor —«2026», «2026-Q3», «2026-09», «2026-W38»,
|
|
6
|
+
* «2026-09-15»—, y que los dos lados lo escriban igual es lo que deja comparar
|
|
7
|
+
* un agrupado hecho en el navegador con uno contado a mil kilómetros. Sin fecha
|
|
8
|
+
* la clave es vacía: un montón que existe —el de los que no tienen— y que se
|
|
9
|
+
* dibuja con las palabras de la copia.
|
|
10
|
+
*/
|
|
11
|
+
export declare function dateKey(when: Date | string | number | null | undefined, grain: GroupGrain): string;
|
|
12
|
+
/**
|
|
13
|
+
* El RÓTULO de una clave de fecha, en el idioma activo.
|
|
14
|
+
*
|
|
15
|
+
* `within` son los granos que los montones de arriba ya dijeron: con el año
|
|
16
|
+
* arriba, un mes es «Septiembre» y no «Septiembre de 2026». Una clave que no
|
|
17
|
+
* tiene la forma de su grano se dibuja TAL CUAL —es lo que hace que un montón
|
|
18
|
+
* contado afuera, con una clave que esta capa no escribió, siga siendo legible
|
|
19
|
+
* en vez de desaparecer detrás de una fecha inventada.
|
|
20
|
+
*/
|
|
21
|
+
export declare function dateLabel(key: string, grain: GroupGrain, within?: readonly GroupGrain[]): string;
|
|
22
|
+
/** El nombre de un mes —1 a 12— en el idioma activo. Lo usa el que ofrece meses
|
|
23
|
+
* sin tener ninguna fecha en la mano, como el recorte por período. */
|
|
24
|
+
export declare function monthName(month: number): string;
|
|
25
|
+
/** Cómo se llama un GRANO —«día», «semana»—, que es distinto de cómo se llama un
|
|
26
|
+
* montón suyo: el grano nombra el corte y el rótulo nombra lo cortado. Sale de
|
|
27
|
+
* la copia, que es donde ya viven las unidades de tiempo en singular. */
|
|
28
|
+
export declare function grainName(grain: GroupGrain): string;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { currentCopy, currentCopyLocale, fillCopy } from "./copy.js";
|
|
2
|
+
/**
|
|
3
|
+
* EL CALENDARIO: de una fecha a una CLAVE, y de una clave a un RÓTULO.
|
|
4
|
+
*
|
|
5
|
+
* Context: un montón de fecha tiene dos textos y no son el mismo. «2026-09» es
|
|
6
|
+
* lo que el motor escribe —lo que identifica al montón, lo que se compara con lo
|
|
7
|
+
* que contó el servidor, lo que se guarda plegado— y «Septiembre» es lo que se
|
|
8
|
+
* lee. Escribir uno solo obliga a elegir cuál se pierde: con la clave a la vista
|
|
9
|
+
* la interfaz habla en máquina, y con el rótulo por identidad dos septiembres de
|
|
10
|
+
* años distintos se funden en un montón que no existe.
|
|
11
|
+
*
|
|
12
|
+
* Por eso son dos funciones y no una:
|
|
13
|
+
*
|
|
14
|
+
* dateKey(when, "month") → «2026-09» la identidad, en un solo idioma
|
|
15
|
+
* dateLabel("2026-09", …) → «Septiembre» el rótulo, en el idioma activo
|
|
16
|
+
*
|
|
17
|
+
* ── El idioma no se traduce: se PIDE ────────────────────────────────
|
|
18
|
+
*
|
|
19
|
+
* Los meses y los días de la semana los dice `Intl` con el `locale` que la copia
|
|
20
|
+
* tiene puesto, así que no hay acá ninguna tabla de doce meses en español ni en
|
|
21
|
+
* inglés — ni la habría que escribir para un tercer idioma. Lo único que sí es
|
|
22
|
+
* copia son la semana y el trimestre, porque ningún calendario los nombra: «W38»
|
|
23
|
+
* no es una palabra y `Intl` no la tiene.
|
|
24
|
+
*
|
|
25
|
+
* ── Un rótulo dice lo que el de arriba NO dijo ──────────────────────
|
|
26
|
+
*
|
|
27
|
+
* Un árbol de fechas se lee de arriba hacia abajo, y repetir el año en cada mes
|
|
28
|
+
* y el mes en cada día llena la sangría de texto que ya está dos renglones más
|
|
29
|
+
* arriba:
|
|
30
|
+
*
|
|
31
|
+
* 2026 ← el año lo dice una vez
|
|
32
|
+
* Septiembre ← no «Septiembre de 2026»
|
|
33
|
+
* Semana 38 ← no «Semana 38 de 2026»
|
|
34
|
+
* Martes ← no «martes, 15 sept 2026»
|
|
35
|
+
*
|
|
36
|
+
* De eso se ocupa `within`: los granos que los montones de arriba —del mismo
|
|
37
|
+
* campo— ya dijeron. Sin nadie arriba, el rótulo se planta solo y dice la fecha
|
|
38
|
+
* entera, porque un «Martes» suelto no es ningún día.
|
|
39
|
+
*/
|
|
40
|
+
/**
|
|
41
|
+
* La forma de la clave de cada grano. Es `GroupGrain` entero: un grano que el
|
|
42
|
+
* calendario no supiera cortar sería un nivel que no parte nada.
|
|
43
|
+
*
|
|
44
|
+
* Los rangos van en la expresión y no en un `if` porque `new Date(2026, 12, 1)`
|
|
45
|
+
* no falla: se corre a enero de 2027. Una clave «2026-13» dibujada como «Enero»
|
|
46
|
+
* es un mes que nadie contó, y eso es peor que un rótulo ilegible.
|
|
47
|
+
*/
|
|
48
|
+
const KEY_SHAPES = {
|
|
49
|
+
year: /^(\d{4})$/,
|
|
50
|
+
quarter: /^(\d{4})-Q([1-4])$/,
|
|
51
|
+
month: /^(\d{4})-(0[1-9]|1[0-2])$/,
|
|
52
|
+
week: /^(\d{4})-W(0[1-9]|[1-4]\d|5[0-3])$/,
|
|
53
|
+
day: /^(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/,
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Una fecha recortada a la CLAVE de su grano.
|
|
57
|
+
*
|
|
58
|
+
* El texto es el que produce el motor —«2026», «2026-Q3», «2026-09», «2026-W38»,
|
|
59
|
+
* «2026-09-15»—, y que los dos lados lo escriban igual es lo que deja comparar
|
|
60
|
+
* un agrupado hecho en el navegador con uno contado a mil kilómetros. Sin fecha
|
|
61
|
+
* la clave es vacía: un montón que existe —el de los que no tienen— y que se
|
|
62
|
+
* dibuja con las palabras de la copia.
|
|
63
|
+
*/
|
|
64
|
+
export function dateKey(when, grain) {
|
|
65
|
+
if (when === null || when === undefined || when === "")
|
|
66
|
+
return "";
|
|
67
|
+
const date = when instanceof Date ? when : new Date(when);
|
|
68
|
+
if (Number.isNaN(date.getTime()))
|
|
69
|
+
return "";
|
|
70
|
+
const year = date.getFullYear();
|
|
71
|
+
const month = date.getMonth() + 1;
|
|
72
|
+
if (grain === "year")
|
|
73
|
+
return String(year);
|
|
74
|
+
if (grain === "quarter")
|
|
75
|
+
return `${year}-Q${Math.floor(date.getMonth() / 3) + 1}`;
|
|
76
|
+
if (grain === "month")
|
|
77
|
+
return `${year}-${pad(month)}`;
|
|
78
|
+
if (grain === "week") {
|
|
79
|
+
const [weekYear, week] = isoWeek(date);
|
|
80
|
+
return `${weekYear}-W${pad(week)}`;
|
|
81
|
+
}
|
|
82
|
+
return `${year}-${pad(month)}-${pad(date.getDate())}`;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* El RÓTULO de una clave de fecha, en el idioma activo.
|
|
86
|
+
*
|
|
87
|
+
* `within` son los granos que los montones de arriba ya dijeron: con el año
|
|
88
|
+
* arriba, un mes es «Septiembre» y no «Septiembre de 2026». Una clave que no
|
|
89
|
+
* tiene la forma de su grano se dibuja TAL CUAL —es lo que hace que un montón
|
|
90
|
+
* contado afuera, con una clave que esta capa no escribió, siga siendo legible
|
|
91
|
+
* en vez de desaparecer detrás de una fecha inventada.
|
|
92
|
+
*/
|
|
93
|
+
export function dateLabel(key, grain, within = []) {
|
|
94
|
+
const copy = currentCopy();
|
|
95
|
+
if (key === "")
|
|
96
|
+
return copy.time.noDate;
|
|
97
|
+
const parts = KEY_SHAPES[grain].exec(key);
|
|
98
|
+
if (parts === null)
|
|
99
|
+
return key;
|
|
100
|
+
const locale = currentCopyLocale();
|
|
101
|
+
const year = Number(parts[1]);
|
|
102
|
+
const said = (one) => within.includes(one);
|
|
103
|
+
if (grain === "year")
|
|
104
|
+
return String(year);
|
|
105
|
+
if (grain === "quarter") {
|
|
106
|
+
const quarter = Number(parts[2]);
|
|
107
|
+
return said("year")
|
|
108
|
+
? fillCopy(copy.calendar.quarter, { n: quarter })
|
|
109
|
+
: fillCopy(copy.calendar.quarterOfYear, { n: quarter, year });
|
|
110
|
+
}
|
|
111
|
+
if (grain === "week") {
|
|
112
|
+
const week = Number(parts[2]);
|
|
113
|
+
/* El mes también alcanza: una semana adentro de septiembre ya sabe de qué
|
|
114
|
+
año es, porque el mes lo dijo. */
|
|
115
|
+
return said("year") || said("quarter") || said("month")
|
|
116
|
+
? fillCopy(copy.calendar.week, { n: week })
|
|
117
|
+
: fillCopy(copy.calendar.weekOfYear, { n: week, year });
|
|
118
|
+
}
|
|
119
|
+
if (grain === "month") {
|
|
120
|
+
const when = new Date(year, Number(parts[2]) - 1, 1);
|
|
121
|
+
return capitalized(said("year")
|
|
122
|
+
? format(locale, { month: "long" }).format(when)
|
|
123
|
+
: format(locale, { month: "long", year: "numeric" }).format(when));
|
|
124
|
+
}
|
|
125
|
+
const day = Number(parts[3]);
|
|
126
|
+
const when = new Date(year, Number(parts[2]) - 1, day);
|
|
127
|
+
/* El 31 de febrero tiene la forma de un día y no es ninguno: `Date` lo corre
|
|
128
|
+
al 3 de marzo en vez de quejarse, así que se pregunta si volvió el mismo. */
|
|
129
|
+
if (when.getDate() !== day)
|
|
130
|
+
return key;
|
|
131
|
+
/* Adentro de una semana, el día es su NOMBRE: son siete y ninguno se repite,
|
|
132
|
+
así que «Martes» alcanza y el número de día sería ruido. Afuera no: un
|
|
133
|
+
«Martes» suelto no dice cuál de los cincuenta y dos. */
|
|
134
|
+
if (said("week"))
|
|
135
|
+
return capitalized(format(locale, { weekday: "long" }).format(when));
|
|
136
|
+
return capitalized(format(locale, {
|
|
137
|
+
weekday: "long",
|
|
138
|
+
day: "numeric",
|
|
139
|
+
month: "short",
|
|
140
|
+
...(said("year") ? {} : { year: "numeric" }),
|
|
141
|
+
}).format(when));
|
|
142
|
+
}
|
|
143
|
+
/** El nombre de un mes —1 a 12— en el idioma activo. Lo usa el que ofrece meses
|
|
144
|
+
* sin tener ninguna fecha en la mano, como el recorte por período. */
|
|
145
|
+
export function monthName(month) {
|
|
146
|
+
return capitalized(format(currentCopyLocale(), { month: "long" }).format(new Date(2000, month - 1, 1)));
|
|
147
|
+
}
|
|
148
|
+
/** Cómo se llama un GRANO —«día», «semana»—, que es distinto de cómo se llama un
|
|
149
|
+
* montón suyo: el grano nombra el corte y el rótulo nombra lo cortado. Sale de
|
|
150
|
+
* la copia, que es donde ya viven las unidades de tiempo en singular. */
|
|
151
|
+
export function grainName(grain) {
|
|
152
|
+
return capitalized(currentCopy().dates[grain]);
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* El año y la semana ISO de una fecha.
|
|
156
|
+
*
|
|
157
|
+
* La semana 1 es la que tiene el primer jueves del año, así que se calcula con
|
|
158
|
+
* el jueves de la semana del dato: es él quien decide a qué año pertenece, y por
|
|
159
|
+
* eso una semana de fin de diciembre puede ser la 1 del año siguiente. El año
|
|
160
|
+
* que sale de acá es el de ESA semana y no el de la fecha.
|
|
161
|
+
*/
|
|
162
|
+
function isoWeek(date) {
|
|
163
|
+
const thursday = new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
164
|
+
thursday.setDate(thursday.getDate() + 3 - ((thursday.getDay() + 6) % 7));
|
|
165
|
+
const first = new Date(thursday.getFullYear(), 0, 4);
|
|
166
|
+
const week = 1 +
|
|
167
|
+
Math.round(((thursday.getTime() - first.getTime()) / 86_400_000 - 3 + ((first.getDay() + 6) % 7)) /
|
|
168
|
+
7);
|
|
169
|
+
return [thursday.getFullYear(), week];
|
|
170
|
+
}
|
|
171
|
+
function format(locale, options) {
|
|
172
|
+
return new Intl.DateTimeFormat(locale, options);
|
|
173
|
+
}
|
|
174
|
+
/** Un rótulo encabeza un montón, y en español `Intl` devuelve «septiembre» con
|
|
175
|
+
* minúscula. Se sube la primera letra con el idioma activo, que es el que sabe
|
|
176
|
+
* cuál es la mayúscula de esa letra. */
|
|
177
|
+
function capitalized(text) {
|
|
178
|
+
const first = text.charAt(0);
|
|
179
|
+
return first === "" ? text : first.toLocaleUpperCase(currentCopyLocale()) + text.slice(1);
|
|
180
|
+
}
|
|
181
|
+
function pad(value) {
|
|
182
|
+
return String(value).padStart(2, "0");
|
|
183
|
+
}
|