@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,110 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { parseDate } from "@internationalized/date";
|
|
4
|
+
import { AriaLocale } from "../../foundations/aria.js";
|
|
5
|
+
import { focusRing, tv } from "../../foundations/variants.js";
|
|
6
|
+
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
7
|
+
import { Button as AriaButton, Calendar as AriaCalendar, CalendarCell, CalendarGrid, CalendarGridBody, CalendarGridHeader, CalendarHeaderCell, CalendarHeading, RangeCalendar as AriaRangeCalendar, useLocale, } from "react-aria-components";
|
|
8
|
+
/**
|
|
9
|
+
* The month grid — on its own, or inside the popover of a date picker.
|
|
10
|
+
*
|
|
11
|
+
* Context: keyboard and screen reader behave like a native calendar because React Aria
|
|
12
|
+
* implements the grid pattern (arrows move a day, PageUp/PageDown a month, Home/End the
|
|
13
|
+
* week). The selected day is the system's `primary`, not the gold: the gold is for the ONE
|
|
14
|
+
* action of a screen, and a calendar is not an action.
|
|
15
|
+
*/
|
|
16
|
+
const navButton = tv({
|
|
17
|
+
extend: focusRing,
|
|
18
|
+
base: "inline-flex size-(--size-control-sm) items-center justify-center rounded-control text-muted transition-colors duration-(--duration-instant) hover:bg-ink/6 hover:text-ink pressed:bg-ink/10 disabled:text-line-strong",
|
|
19
|
+
});
|
|
20
|
+
function Header() {
|
|
21
|
+
const { direction } = useLocale();
|
|
22
|
+
const [Prev, Next] = direction === "rtl" ? [ChevronRight, ChevronLeft] : [ChevronLeft, ChevronRight];
|
|
23
|
+
return (_jsxs("header", { className: "mb-2 flex items-center gap-1", children: [_jsx(AriaButton, { slot: "previous", className: navButton, children: _jsx(Prev, { "aria-hidden": true, className: "size-(--size-icon-md)", strokeWidth: 2 }) }), _jsx(CalendarHeading, { className: "flex-1 text-center text-body font-semibold text-ink first-letter:uppercase" }), _jsx(AriaButton, { slot: "next", className: navButton, children: _jsx(Next, { "aria-hidden": true, className: "size-(--size-icon-md)", strokeWidth: 2 }) })] }));
|
|
24
|
+
}
|
|
25
|
+
function WeekDays() {
|
|
26
|
+
return (_jsx(CalendarGridHeader, { children: (day) => (_jsx(CalendarHeaderCell, { className: "pb-1 text-micro font-medium text-muted", children: day })) }));
|
|
27
|
+
}
|
|
28
|
+
const dayCell = tv({
|
|
29
|
+
extend: focusRing,
|
|
30
|
+
base: "flex size-(--size-control-md) cursor-default items-center justify-center rounded-control text-meta tabular-nums outside-month:text-line-strong forced-color-adjust-none",
|
|
31
|
+
variants: {
|
|
32
|
+
isSelected: {
|
|
33
|
+
false: "text-ink hover:bg-ink/6 pressed:bg-ink/10",
|
|
34
|
+
true: "bg-primary font-medium text-surface invalid:bg-danger forced-colors:bg-[Highlight] forced-colors:text-[HighlightText]",
|
|
35
|
+
},
|
|
36
|
+
isDisabled: {
|
|
37
|
+
true: "text-line-strong hover:bg-transparent forced-colors:text-[GrayText]",
|
|
38
|
+
},
|
|
39
|
+
isToday: {
|
|
40
|
+
true: "font-semibold underline decoration-2 underline-offset-4",
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
/* ISO in, ISO out — the same contract as `DatePicker`. Inside a picker none of these are
|
|
45
|
+
passed and the calendar reads the picker's state from React Aria's context; only the
|
|
46
|
+
props that ARE given are forwarded, so an `undefined` never shadows that context. */
|
|
47
|
+
function iso(value) {
|
|
48
|
+
if (value === undefined)
|
|
49
|
+
return undefined;
|
|
50
|
+
if (value === null || value === "")
|
|
51
|
+
return null;
|
|
52
|
+
try {
|
|
53
|
+
return parseDate(value);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function defined(props) {
|
|
60
|
+
return Object.fromEntries(Object.entries(props).filter(([, v]) => v !== undefined));
|
|
61
|
+
}
|
|
62
|
+
/** One month, to pick one day. */
|
|
63
|
+
export function Calendar({ value, onChange, min, max, isUnavailable, ...rest }) {
|
|
64
|
+
const props = defined({
|
|
65
|
+
value: iso(value),
|
|
66
|
+
onChange: onChange === undefined ? undefined : (next) => onChange(next.toString()),
|
|
67
|
+
minValue: iso(min) ?? undefined,
|
|
68
|
+
maxValue: iso(max) ?? undefined,
|
|
69
|
+
isDateUnavailable: isUnavailable === undefined
|
|
70
|
+
? undefined
|
|
71
|
+
: (date) => isUnavailable(date.toString()),
|
|
72
|
+
});
|
|
73
|
+
return (_jsx(AriaLocale, { children: _jsxs(AriaCalendar, { ...rest, ...props, className: "w-fit", children: [_jsx(Header, {}), _jsxs(CalendarGrid, { className: "border-separate border-spacing-0.5", children: [_jsx(WeekDays, {}), _jsx(CalendarGridBody, { children: (date) => _jsx(CalendarCell, { date: date, className: dayCell }) })] })] }) }));
|
|
74
|
+
}
|
|
75
|
+
const rangeDay = tv({
|
|
76
|
+
extend: focusRing,
|
|
77
|
+
base: "flex size-full items-center justify-center rounded-control forced-color-adjust-none",
|
|
78
|
+
variants: {
|
|
79
|
+
position: {
|
|
80
|
+
none: "text-ink group-hover:bg-ink/6 group-pressed:bg-ink/10",
|
|
81
|
+
middle: "text-ink group-hover:bg-primary/18",
|
|
82
|
+
cap: "bg-primary font-medium text-surface group-invalid:bg-danger forced-colors:bg-[Highlight] forced-colors:text-[HighlightText]",
|
|
83
|
+
},
|
|
84
|
+
isDisabled: {
|
|
85
|
+
true: "text-line-strong forced-colors:text-[GrayText]",
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
/** One or two months, to pick a span: the first click opens it, the second closes it. */
|
|
90
|
+
export function RangeCalendar({ months = 1, value, onChange, min, max, ...rest }) {
|
|
91
|
+
const start = iso(value?.start);
|
|
92
|
+
const end = iso(value?.end);
|
|
93
|
+
const props = defined({
|
|
94
|
+
value: value === undefined ? undefined : start && end ? { start, end } : null,
|
|
95
|
+
onChange: onChange === undefined
|
|
96
|
+
? undefined
|
|
97
|
+
: (next) => onChange({ start: next.start.toString(), end: next.end.toString() }),
|
|
98
|
+
minValue: iso(min) ?? undefined,
|
|
99
|
+
maxValue: iso(max) ?? undefined,
|
|
100
|
+
});
|
|
101
|
+
return (_jsx(AriaLocale, { children: _jsxs(AriaRangeCalendar, { ...rest, ...props, visibleDuration: { months }, className: "w-fit", children: [_jsx(Header, {}), _jsx("div", { className: "flex gap-4", children: Array.from({ length: months }, (_, i) => (_jsxs(CalendarGrid, { offset: { months: i }, className: "border-separate border-spacing-y-0.5", children: [_jsx(WeekDays, {}), _jsx(CalendarGridBody, { children: (date) => (_jsx(CalendarCell, { date: date, className: "group size-(--size-control-md) cursor-default text-meta tabular-nums outside-month:invisible selected:bg-primary/10 selection-start:rounded-s-control selection-end:rounded-e-control", children: ({ formattedDate, isSelected, isSelectionStart, isSelectionEnd, isFocusVisible, isDisabled, }) => (_jsx("span", { className: rangeDay({
|
|
102
|
+
position: isSelected && (isSelectionStart || isSelectionEnd)
|
|
103
|
+
? "cap"
|
|
104
|
+
: isSelected
|
|
105
|
+
? "middle"
|
|
106
|
+
: "none",
|
|
107
|
+
isDisabled,
|
|
108
|
+
isFocusVisible,
|
|
109
|
+
}), children: formattedDate })) })) })] }, i))) })] }) }));
|
|
110
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type FormMode } from "../field/mode.js";
|
|
2
|
+
type Shared = {
|
|
3
|
+
id?: string;
|
|
4
|
+
describedBy?: string;
|
|
5
|
+
invalid?: boolean;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
/** The earliest and latest day that can be chosen, as ISO dates. */
|
|
8
|
+
min?: string;
|
|
9
|
+
max?: string;
|
|
10
|
+
/** Forces the mode. By default it inherits the form's: in read mode it draws the value. */
|
|
11
|
+
mode?: FormMode;
|
|
12
|
+
};
|
|
13
|
+
/** A date typed by segments, without a calendar: for a form where dates are known by heart
|
|
14
|
+
* (a birth date, a contract date). */
|
|
15
|
+
export declare function DateField({ value, onChange, id: ownId, describedBy: ownDescribedBy, invalid: ownInvalid, disabled, min, max, mode, }: Shared & {
|
|
16
|
+
value: string | null;
|
|
17
|
+
onChange?: (next: string | null) => void;
|
|
18
|
+
}): import("react").JSX.Element;
|
|
19
|
+
/** A date typed by segments, or picked on a calendar from the button. */
|
|
20
|
+
export declare function DatePicker({ value, onChange, id: ownId, describedBy: ownDescribedBy, invalid: ownInvalid, disabled, min, max, mode, }: Shared & {
|
|
21
|
+
value: string | null;
|
|
22
|
+
onChange?: (next: string | null) => void;
|
|
23
|
+
}): import("react").JSX.Element;
|
|
24
|
+
export type DateRangeValue = {
|
|
25
|
+
start: string;
|
|
26
|
+
end: string;
|
|
27
|
+
};
|
|
28
|
+
/** A span of days — a period of a report, the validity of a contract — typed or picked on
|
|
29
|
+
* two months at once. */
|
|
30
|
+
export declare function DateRangePicker({ value, onChange, id: ownId, describedBy: ownDescribedBy, invalid: ownInvalid, disabled, min, max, mode, }: Shared & {
|
|
31
|
+
value: DateRangeValue | null;
|
|
32
|
+
onChange?: (next: DateRangeValue | null) => void;
|
|
33
|
+
}): import("react").JSX.Element;
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { DateFormatter, parseDate } from "@internationalized/date";
|
|
4
|
+
import { AriaLocale } from "../../foundations/aria.js";
|
|
5
|
+
import { useUiCopy } from "../../foundations/copy.js";
|
|
6
|
+
import { tv } from "../../foundations/variants.js";
|
|
7
|
+
import { Calendar, RangeCalendar } from "./Calendar.js";
|
|
8
|
+
import { FieldButton, FieldGroup } from "../field/aria.js";
|
|
9
|
+
import { useFieldIds } from "../field/context.js";
|
|
10
|
+
import { ReadValue } from "../field/Field.js";
|
|
11
|
+
import { useFormMode } from "../field/mode.js";
|
|
12
|
+
import { Popover } from "../overlay/Popover.js";
|
|
13
|
+
import { CalendarDays } from "lucide-react";
|
|
14
|
+
import { DateField as AriaDateField, DateInput, DatePicker as AriaDatePicker, DateRangePicker as AriaDateRangePicker, DateSegment, Dialog, } from "react-aria-components";
|
|
15
|
+
/**
|
|
16
|
+
* Dates as an ERP stores them — ISO strings, `"2026-10-30"` — typed segment by segment or
|
|
17
|
+
* picked on a calendar.
|
|
18
|
+
*
|
|
19
|
+
* Context: the value is a string and not React Aria's `CalendarDate` because that is what
|
|
20
|
+
* every backend sends and every other control of the system takes. The conversion happens
|
|
21
|
+
* here, once, instead of at each of the hundred places an app reads a date.
|
|
22
|
+
*
|
|
23
|
+
* The field is SEGMENTED, not a free text box: the person types the day, the month and the
|
|
24
|
+
* year in the order of their locale (day/month/year in `es-BO`), the arrows step each one,
|
|
25
|
+
* and an impossible date cannot be written. That order comes from the copy table, never
|
|
26
|
+
* from the browser — see `AriaLocale`.
|
|
27
|
+
*/
|
|
28
|
+
const segment = tv({
|
|
29
|
+
base: "rounded-mark px-0.5 tabular-nums caret-transparent outline-0 type-literal:px-0 forced-color-adjust-none",
|
|
30
|
+
variants: {
|
|
31
|
+
isPlaceholder: { true: "text-muted" },
|
|
32
|
+
isDisabled: { true: "text-muted forced-colors:text-[GrayText]" },
|
|
33
|
+
isFocused: {
|
|
34
|
+
true: "bg-primary text-surface forced-colors:bg-[Highlight] forced-colors:text-[HighlightText]",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
function Segments({ slot }) {
|
|
39
|
+
return (_jsx(DateInput, { slot: slot, className: "flex min-w-0 items-center px-2.5 whitespace-nowrap", children: (part) => _jsx(DateSegment, { segment: part, className: segment }) }));
|
|
40
|
+
}
|
|
41
|
+
function parse(iso) {
|
|
42
|
+
if (iso === null || iso === undefined || iso === "")
|
|
43
|
+
return null;
|
|
44
|
+
try {
|
|
45
|
+
return parseDate(iso);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function useReadDate() {
|
|
52
|
+
const copy = useUiCopy();
|
|
53
|
+
const format = new DateFormatter(copy.locale, { dateStyle: "medium" });
|
|
54
|
+
return (iso) => {
|
|
55
|
+
const date = parse(iso);
|
|
56
|
+
return date === null ? "" : format.format(date.toDate("UTC"));
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/** A date typed by segments, without a calendar: for a form where dates are known by heart
|
|
60
|
+
* (a birth date, a contract date). */
|
|
61
|
+
export function DateField({ value, onChange, id: ownId, describedBy: ownDescribedBy, invalid: ownInvalid, disabled = false, min, max, mode, }) {
|
|
62
|
+
const { id, labelledBy, describedBy, invalid } = useFieldIds({
|
|
63
|
+
id: ownId,
|
|
64
|
+
describedBy: ownDescribedBy,
|
|
65
|
+
invalid: ownInvalid,
|
|
66
|
+
});
|
|
67
|
+
const read = useReadDate();
|
|
68
|
+
if (useFormMode(mode) === "read")
|
|
69
|
+
return _jsx(ReadValue, { value: read(value) });
|
|
70
|
+
return (_jsx(AriaLocale, { children: _jsx(AriaDateField, { id: id, "aria-labelledby": labelledBy, "aria-describedby": describedBy, value: parse(value), onChange: (next) => onChange?.(next === null ? null : next.toString()), minValue: parse(min) ?? undefined, maxValue: parse(max) ?? undefined, isInvalid: invalid, isDisabled: disabled, className: "w-full", children: _jsx(FieldGroup, { children: _jsx(Segments, {}) }) }) }));
|
|
71
|
+
}
|
|
72
|
+
/** A date typed by segments, or picked on a calendar from the button. */
|
|
73
|
+
export function DatePicker({ value, onChange, id: ownId, describedBy: ownDescribedBy, invalid: ownInvalid, disabled = false, min, max, mode, }) {
|
|
74
|
+
const { id, labelledBy, describedBy, invalid } = useFieldIds({
|
|
75
|
+
id: ownId,
|
|
76
|
+
describedBy: ownDescribedBy,
|
|
77
|
+
invalid: ownInvalid,
|
|
78
|
+
});
|
|
79
|
+
const read = useReadDate();
|
|
80
|
+
if (useFormMode(mode) === "read")
|
|
81
|
+
return _jsx(ReadValue, { value: read(value) });
|
|
82
|
+
return (_jsx(AriaLocale, { children: _jsxs(AriaDatePicker, { id: id, "aria-labelledby": labelledBy, "aria-describedby": describedBy, value: parse(value), onChange: (next) => onChange?.(next === null ? null : next.toString()), minValue: parse(min) ?? undefined, maxValue: parse(max) ?? undefined, isInvalid: invalid, isDisabled: disabled, className: "w-full", children: [_jsxs(FieldGroup, { children: [_jsx(Segments, {}), _jsx(FieldButton, { className: "mr-1 ml-auto", children: _jsx(CalendarDays, { "aria-hidden": true, className: "size-(--size-icon-md)", strokeWidth: 2 }) })] }), _jsx(Popover, { placement: "bottom end", children: _jsx(Dialog, { className: "p-3 outline-0", children: _jsx(Calendar, {}) }) })] }) }));
|
|
83
|
+
}
|
|
84
|
+
/** A span of days — a period of a report, the validity of a contract — typed or picked on
|
|
85
|
+
* two months at once. */
|
|
86
|
+
export function DateRangePicker({ value, onChange, id: ownId, describedBy: ownDescribedBy, invalid: ownInvalid, disabled = false, min, max, mode, }) {
|
|
87
|
+
const { id, labelledBy, describedBy, invalid } = useFieldIds({
|
|
88
|
+
id: ownId,
|
|
89
|
+
describedBy: ownDescribedBy,
|
|
90
|
+
invalid: ownInvalid,
|
|
91
|
+
});
|
|
92
|
+
const read = useReadDate();
|
|
93
|
+
if (useFormMode(mode) === "read") {
|
|
94
|
+
return (_jsx(ReadValue, { value: value === null ? "" : `${read(value.start)} – ${read(value.end)}` }));
|
|
95
|
+
}
|
|
96
|
+
const start = parse(value?.start);
|
|
97
|
+
const end = parse(value?.end);
|
|
98
|
+
return (_jsx(AriaLocale, { children: _jsxs(AriaDateRangePicker, { id: id, "aria-labelledby": labelledBy, "aria-describedby": describedBy, value: start === null || end === null ? null : { start, end }, onChange: (next) => onChange?.(next === null ? null : { start: next.start.toString(), end: next.end.toString() }), minValue: parse(min) ?? undefined, maxValue: parse(max) ?? undefined, isInvalid: invalid, isDisabled: disabled, className: "w-full", children: [_jsxs(FieldGroup, { children: [_jsx(Segments, { slot: "start" }), _jsx("span", { "aria-hidden": true, className: "text-muted", children: "\u2013" }), _jsx(Segments, { slot: "end" }), _jsx(FieldButton, { className: "mr-1 ml-auto", children: _jsx(CalendarDays, { "aria-hidden": true, className: "size-(--size-icon-md)", strokeWidth: 2 }) })] }), _jsx(Popover, { placement: "bottom end", children: _jsx(Dialog, { className: "p-3 outline-0", children: _jsx(RangeCalendar, { months: 2 }) }) })] }) }));
|
|
99
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/* LAS FECHAS — escritas por segmentos o elegidas en un calendario, con el orden del idioma de
|
|
2
|
+
la copia. El valor es un ISO (`"2026-10-30"`), lo que manda cualquier backend. */
|
|
3
|
+
export { Calendar, RangeCalendar, } from "./Calendar.js";
|
|
4
|
+
export { DateField, DatePicker, DateRangePicker, } from "./DatePicker.js";
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
/** Where this dialog came from. It is drawn in the header, small. */
|
|
3
|
+
export interface DialogContext {
|
|
4
|
+
/** The category, in small caps: «factura», «cliente». It answers *what kind of
|
|
5
|
+
* thing is this about?* before saying which one. */
|
|
6
|
+
kind?: string;
|
|
7
|
+
/** Which one. The name of the resource. */
|
|
8
|
+
label: string;
|
|
9
|
+
/** What describes it without opening it: «saldo Bs. 12.450,00 · vence 15/11». */
|
|
10
|
+
detail?: string;
|
|
11
|
+
}
|
|
12
|
+
/** The scrim and the window recipes. Internal — exported only so a test can read the
|
|
13
|
+
* classes that make the bottom sheet: the modal is a portal and draws nothing on the
|
|
14
|
+
* server. */
|
|
15
|
+
export declare const dialogStyles: {
|
|
16
|
+
overlay: import("tailwind-variants/config").TVReturnType<{
|
|
17
|
+
isEntering: {
|
|
18
|
+
true: string;
|
|
19
|
+
};
|
|
20
|
+
isExiting: {
|
|
21
|
+
true: string;
|
|
22
|
+
};
|
|
23
|
+
}, undefined, "fixed inset-0 z-50 flex items-center justify-center bg-scrim/40 p-4 transition-opacity duration-(--duration-quick) ease-(--ease-out) max-sm:items-end max-sm:p-0 sm:p-8", undefined, undefined, undefined>;
|
|
24
|
+
modal: import("tailwind-variants/config").TVReturnType<{
|
|
25
|
+
width: {
|
|
26
|
+
sm: string;
|
|
27
|
+
md: string;
|
|
28
|
+
lg: string;
|
|
29
|
+
};
|
|
30
|
+
isEntering: {
|
|
31
|
+
true: string;
|
|
32
|
+
};
|
|
33
|
+
isExiting: {
|
|
34
|
+
true: string;
|
|
35
|
+
};
|
|
36
|
+
}, undefined, "flex max-h-full w-full flex-col overflow-hidden rounded-panel border border-line-strong bg-surface text-ink shadow-window outline-0 transition-[opacity,translate] duration-(--duration-panel) ease-(--ease-out) forced-colors:bg-[Canvas] max-sm:max-h-(--size-sheet) max-sm:max-w-none max-sm:rounded-b-none max-sm:border-x-0 max-sm:border-b-0", undefined, undefined, undefined>;
|
|
37
|
+
};
|
|
38
|
+
export type DialogWidth = "sm" | "md" | "lg";
|
|
39
|
+
export interface DialogProps {
|
|
40
|
+
open: boolean;
|
|
41
|
+
title: string;
|
|
42
|
+
/** One sentence under the title. It is also the dialog's description for assistive
|
|
43
|
+
* technology: what is read after the name, before the first field. */
|
|
44
|
+
subtitle?: string;
|
|
45
|
+
context?: DialogContext;
|
|
46
|
+
width?: DialogWidth;
|
|
47
|
+
/** Every way out goes through here: the × button, Escape and a press on the scrim. */
|
|
48
|
+
onClose: () => void;
|
|
49
|
+
/** Dock into the workspace. Absent when there is nowhere to dock. */
|
|
50
|
+
onExpand?: () => void;
|
|
51
|
+
/** Where it expands to, when «into a panel» says nothing: «open the catalog».
|
|
52
|
+
* It is the tooltip and the accessible name, and it is not drawn. */
|
|
53
|
+
expandLabel?: string;
|
|
54
|
+
footer?: ReactNode;
|
|
55
|
+
children: ReactNode;
|
|
56
|
+
}
|
|
57
|
+
/** The inside of the window: React Aria's `Dialog` with the header, body and footer.
|
|
58
|
+
*
|
|
59
|
+
* Internal — exported only so a test can read its markup, which the modal (a portal)
|
|
60
|
+
* does not produce on the server. */
|
|
61
|
+
export declare function DialogSurface({ title, subtitle, context, onClose, onExpand, expandLabel, footer, children, }: Omit<DialogProps, "open" | "width">): import("react").JSX.Element;
|
|
62
|
+
/**
|
|
63
|
+
* What has to be answered before moving on: a short form that blocks the page and keeps
|
|
64
|
+
* saying what it is about.
|
|
65
|
+
*
|
|
66
|
+
* Context: controlled by `open` + `onClose` — the screen that opens it owns the decision of
|
|
67
|
+
* when it is open, and every way out (×, Escape, the scrim) reports through `onClose`. It
|
|
68
|
+
* stays mounted for the exit transition after `open` turns `false`, so the window fades
|
|
69
|
+
* out instead of vanishing.
|
|
70
|
+
*/
|
|
71
|
+
export declare function Dialog({ open, width, onClose, ...surface }: DialogProps): import("react").JSX.Element;
|
|
72
|
+
export interface WizardStep {
|
|
73
|
+
id: string;
|
|
74
|
+
label: string;
|
|
75
|
+
/** What is missing before you can move on. If it is present, «next» is turned
|
|
76
|
+
* off and this is drawn — a disabled button with no reason is a dead end. */
|
|
77
|
+
blocked?: string;
|
|
78
|
+
}
|
|
79
|
+
export interface WizardProps {
|
|
80
|
+
steps: readonly WizardStep[];
|
|
81
|
+
current: string;
|
|
82
|
+
/** A completed step was pressed: going back to review it. */
|
|
83
|
+
onStepChange?: (id: string) => void;
|
|
84
|
+
children: ReactNode;
|
|
85
|
+
onCancel?: () => void;
|
|
86
|
+
onBack?: () => void;
|
|
87
|
+
onNext?: () => void;
|
|
88
|
+
/** On the last step it stops being «next» and starts naming what is going to
|
|
89
|
+
* happen: «Crear orden». A button that says «next» on the last step hides that
|
|
90
|
+
* something gets executed there. */
|
|
91
|
+
nextLabel?: string;
|
|
92
|
+
busy?: boolean;
|
|
93
|
+
}
|
|
94
|
+
/** A dialog with steps, for what CANNOT be answered in one go.
|
|
95
|
+
*
|
|
96
|
+
* ── When a form deserves to be a wizard ─────────────────────────────
|
|
97
|
+
*
|
|
98
|
+
* Only when **one answer changes the following questions**. If the fields are
|
|
99
|
+
* independent, splitting them into steps simplifies nothing: it adds clicks and
|
|
100
|
+
* hides half the form, and whoever wants to review what they entered has to
|
|
101
|
+
* navigate backwards. A long but complete form is filled in faster than three
|
|
102
|
+
* short steps.
|
|
103
|
+
*
|
|
104
|
+
* The case that does justify it: a purchase order. Whether it is a local purchase
|
|
105
|
+
* or an import changes which conditions exist — an import asks for currency and
|
|
106
|
+
* incoterm, a local one for payment terms. There, step 2 cannot even be drawn
|
|
107
|
+
* until step 1 has been answered.
|
|
108
|
+
*
|
|
109
|
+
* ── The steps already completed can be clicked ──────────────────────
|
|
110
|
+
*
|
|
111
|
+
* Going back is not a mistake: it is reviewing. What you cannot do is jump
|
|
112
|
+
* ahead, because the next step does not know yet what to ask.
|
|
113
|
+
*
|
|
114
|
+
* ── It is the CONTENT of a `Dialog`, not a second one ────────────────
|
|
115
|
+
*
|
|
116
|
+
* The focus trap, Escape and the return of focus are the dialog's. The wizard
|
|
117
|
+
* only moves between steps, so it has no overlay behaviour of its own. */
|
|
118
|
+
export declare function Wizard({ steps, current, onStepChange, children, onCancel, onBack, onNext, nextLabel, busy, }: WizardProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useUiCopy } from "../../foundations/copy.js";
|
|
4
|
+
import { cn, tv } from "../../foundations/variants.js";
|
|
5
|
+
import { Button, IconButton } from "../button/Button.js";
|
|
6
|
+
import { Maximize2, X } from "lucide-react";
|
|
7
|
+
import { useId } from "react";
|
|
8
|
+
import { Dialog as AriaDialog, Heading, Modal, ModalOverlay } from "react-aria-components";
|
|
9
|
+
/** The scrim. The page stays visible under it — the dialog is ABOUT something on it.
|
|
10
|
+
* On a phone it drops the padding and pushes the window to the bottom edge. */
|
|
11
|
+
const overlay = tv({
|
|
12
|
+
base: "fixed inset-0 z-50 flex items-center justify-center bg-scrim/40 p-4 transition-opacity duration-(--duration-quick) ease-(--ease-out) max-sm:items-end max-sm:p-0 sm:p-8",
|
|
13
|
+
variants: {
|
|
14
|
+
isEntering: { true: "opacity-0" },
|
|
15
|
+
isExiting: { true: "opacity-0" },
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
/** The window. It never grows past the scrim's padding: when the form is long the BODY
|
|
19
|
+
* scrolls and the header (what it is about) and the footer (what to do) stay in view.
|
|
20
|
+
*
|
|
21
|
+
* On a phone it is a bottom sheet: every width is the full width, only the top corners
|
|
22
|
+
* are rounded (the bottom ones sit on the screen's edge), it stops at `--size-sheet`,
|
|
23
|
+
* and it enters from below — the motion says where it lives. */
|
|
24
|
+
const modal = tv({
|
|
25
|
+
base: "flex max-h-full w-full flex-col overflow-hidden rounded-panel border border-line-strong bg-surface text-ink shadow-window outline-0 transition-[opacity,translate] duration-(--duration-panel) ease-(--ease-out) forced-colors:bg-[Canvas] max-sm:max-h-(--size-sheet) max-sm:max-w-none max-sm:rounded-b-none max-sm:border-x-0 max-sm:border-b-0",
|
|
26
|
+
variants: {
|
|
27
|
+
width: { sm: "max-w-md", md: "max-w-2xl", lg: "max-w-4xl" },
|
|
28
|
+
isEntering: { true: "translate-y-2 opacity-0 max-sm:translate-y-full" },
|
|
29
|
+
isExiting: {
|
|
30
|
+
true: "translate-y-1 opacity-0 duration-(--duration-quick) max-sm:translate-y-full",
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
/** The scrim and the window recipes. Internal — exported only so a test can read the
|
|
35
|
+
* classes that make the bottom sheet: the modal is a portal and draws nothing on the
|
|
36
|
+
* server. */
|
|
37
|
+
export const dialogStyles = { overlay, modal };
|
|
38
|
+
/** The inside of the window: React Aria's `Dialog` with the header, body and footer.
|
|
39
|
+
*
|
|
40
|
+
* Internal — exported only so a test can read its markup, which the modal (a portal)
|
|
41
|
+
* does not produce on the server. */
|
|
42
|
+
export function DialogSurface({ title, subtitle, context, onClose, onExpand, expandLabel, footer, children, }) {
|
|
43
|
+
const copy = useUiCopy();
|
|
44
|
+
const subtitleId = useId();
|
|
45
|
+
const expand = expandLabel ?? copy.dialog.expand;
|
|
46
|
+
return (_jsxs(AriaDialog, { "aria-describedby": subtitle === undefined ? undefined : subtitleId, className: "flex max-h-[inherit] min-h-0 flex-1 flex-col outline-0", children: [_jsxs("header", { className: "flex shrink-0 items-start gap-3 border-b border-line px-4 py-3", children: [_jsxs("div", { className: "min-w-0 flex-1", children: [context === undefined ? null : (
|
|
47
|
+
/* The context, on top and small. It is what keeps the first field
|
|
48
|
+
from being «about what?». Same voice as a field label, not a
|
|
49
|
+
console kicker: mono uppercase next to a form title was a
|
|
50
|
+
second chrome fighting the first. */
|
|
51
|
+
_jsxs("p", { className: "mb-0.5 flex min-w-0 flex-wrap items-baseline gap-x-2 gap-y-0.5 text-micro text-muted", children: [context.kind === undefined ? null : (_jsx("span", { className: "font-medium", children: context.kind })), _jsx("span", { className: "min-w-0 truncate text-ink", children: context.label }), context.detail === undefined ? null : (_jsx("span", { className: "min-w-0 truncate tabular-nums", children: context.detail }))] })), _jsx(Heading, { slot: "title", className: "text-title font-semibold text-ink", children: title }), subtitle === undefined ? null : (_jsx("p", { id: subtitleId, className: "mt-1 max-w-prose text-meta text-muted", children: subtitle }))] }), _jsxs("div", { className: "flex shrink-0 items-center gap-1", children: [onExpand === undefined ? null : (_jsx(IconButton, { icon: Maximize2, label: expand, onClick: onExpand })), _jsx(IconButton, { icon: X, label: copy.shell.close, onClick: onClose })] })] }), _jsx("div", { className: cn("min-h-0 flex-1 overflow-y-auto px-4 py-4", footer === undefined && "max-sm:safe-bottom"), children: children }), footer === undefined ? null : (_jsx("footer", { className: "flex shrink-0 flex-wrap items-center justify-end gap-2 border-t border-line bg-ground px-4 py-3 max-sm:safe-bottom", children: footer }))] }));
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* What has to be answered before moving on: a short form that blocks the page and keeps
|
|
55
|
+
* saying what it is about.
|
|
56
|
+
*
|
|
57
|
+
* Context: controlled by `open` + `onClose` — the screen that opens it owns the decision of
|
|
58
|
+
* when it is open, and every way out (×, Escape, the scrim) reports through `onClose`. It
|
|
59
|
+
* stays mounted for the exit transition after `open` turns `false`, so the window fades
|
|
60
|
+
* out instead of vanishing.
|
|
61
|
+
*/
|
|
62
|
+
export function Dialog({ open, width = "md", onClose, ...surface }) {
|
|
63
|
+
return (_jsx(ModalOverlay, { isOpen: open, onOpenChange: (next) => {
|
|
64
|
+
if (!next)
|
|
65
|
+
onClose();
|
|
66
|
+
}, isDismissable: true, className: overlay, children: _jsx(Modal, { className: (state) => modal({ ...state, width }), children: _jsx(DialogSurface, { onClose: onClose, ...surface }) }) }));
|
|
67
|
+
}
|
|
68
|
+
/** A dialog with steps, for what CANNOT be answered in one go.
|
|
69
|
+
*
|
|
70
|
+
* ── When a form deserves to be a wizard ─────────────────────────────
|
|
71
|
+
*
|
|
72
|
+
* Only when **one answer changes the following questions**. If the fields are
|
|
73
|
+
* independent, splitting them into steps simplifies nothing: it adds clicks and
|
|
74
|
+
* hides half the form, and whoever wants to review what they entered has to
|
|
75
|
+
* navigate backwards. A long but complete form is filled in faster than three
|
|
76
|
+
* short steps.
|
|
77
|
+
*
|
|
78
|
+
* The case that does justify it: a purchase order. Whether it is a local purchase
|
|
79
|
+
* or an import changes which conditions exist — an import asks for currency and
|
|
80
|
+
* incoterm, a local one for payment terms. There, step 2 cannot even be drawn
|
|
81
|
+
* until step 1 has been answered.
|
|
82
|
+
*
|
|
83
|
+
* ── The steps already completed can be clicked ──────────────────────
|
|
84
|
+
*
|
|
85
|
+
* Going back is not a mistake: it is reviewing. What you cannot do is jump
|
|
86
|
+
* ahead, because the next step does not know yet what to ask.
|
|
87
|
+
*
|
|
88
|
+
* ── It is the CONTENT of a `Dialog`, not a second one ────────────────
|
|
89
|
+
*
|
|
90
|
+
* The focus trap, Escape and the return of focus are the dialog's. The wizard
|
|
91
|
+
* only moves between steps, so it has no overlay behaviour of its own. */
|
|
92
|
+
export function Wizard({ steps, current, onStepChange, children, onCancel, onBack, onNext, nextLabel, busy = false, }) {
|
|
93
|
+
/* The words of the wizard's own buttons come from the copy table. */
|
|
94
|
+
const words = useUiCopy().dialog;
|
|
95
|
+
const at = steps.findIndex((s) => s.id === current);
|
|
96
|
+
const step = steps[at];
|
|
97
|
+
const last = at === steps.length - 1;
|
|
98
|
+
const blocked = step?.blocked;
|
|
99
|
+
return (_jsxs("div", { className: "flex min-w-0 flex-col gap-3", children: [_jsx("ol", { className: "flex min-w-0 flex-wrap items-center gap-0.5 rounded-box bg-ink/4 p-1", children: steps.map((s, i) => {
|
|
100
|
+
const done = i < at;
|
|
101
|
+
const here = i === at;
|
|
102
|
+
return (_jsx("li", { className: "flex items-center", children: _jsxs("button", { type: "button", disabled: i > at, onClick: () => {
|
|
103
|
+
if (!here)
|
|
104
|
+
onStepChange?.(s.id);
|
|
105
|
+
}, "aria-current": here ? "step" : undefined, "data-state": here ? "current" : done ? "done" : "pending", className: `inline-flex items-center gap-1.5 rounded-control px-2 py-1 text-micro transition-colors duration-(--duration-instant) disabled:pointer-events-none focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-primary ${here
|
|
106
|
+
? "bg-surface font-medium text-ink"
|
|
107
|
+
: done
|
|
108
|
+
? "text-ink hover:bg-surface/70"
|
|
109
|
+
: "text-muted"}`, children: [_jsx("span", { className: `inline-flex size-5 shrink-0 items-center justify-center rounded-pill text-micro tabular-nums ${here
|
|
110
|
+
? "bg-ink text-surface"
|
|
111
|
+
: done
|
|
112
|
+
? "bg-ink/10 text-ink"
|
|
113
|
+
: "bg-ink/8 text-muted"}`, children: i + 1 }), s.label] }) }, s.id));
|
|
114
|
+
}) }), _jsx("div", { className: "min-w-0", children: children }), _jsxs("div", { className: "flex flex-wrap items-center gap-2 border-t border-line pt-3", children: [blocked === undefined ? null : (
|
|
115
|
+
/* The reason, next to the disabled button. A grey «next» with no
|
|
116
|
+
explanation is a dead end: the user tries, nothing happens, and
|
|
117
|
+
there is no way to know what is missing. */
|
|
118
|
+
_jsx("span", { className: "mr-auto min-w-0 text-micro text-warn", children: blocked })), _jsxs("span", { className: blocked === undefined
|
|
119
|
+
? "ml-auto flex items-center gap-2"
|
|
120
|
+
: "flex items-center gap-2", children: [onCancel === undefined ? null : (_jsx(Button, { role: "ghost", onClick: onCancel, children: words.cancel })), at > 0 ? (_jsx(Button, { role: "secondary", onClick: onBack, children: words.back })) : null, _jsx(Button, { role: "primary", onClick: onNext, disabled: blocked !== undefined || busy, "aria-busy": busy || undefined, children: busy ? words.busy : (nextLabel ?? (last ? words.finish : words.next)) })] })] })] }));
|
|
121
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Dialog, type DialogContext, type DialogProps, type DialogWidth, Wizard, type WizardProps, type WizardStep, } from "./Dialog.js";
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
export type DisclosureProps = {
|
|
3
|
+
/** The name of what folds. */
|
|
4
|
+
title: ReactNode;
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
/** Its key inside an `Accordion`. Needed only when the accordion is controlled or has a
|
|
7
|
+
* `defaultExpanded`. */
|
|
8
|
+
id?: string;
|
|
9
|
+
/** Something about the content, at the right of the row: «3 líneas», «Bs 1.240,00». It is
|
|
10
|
+
* what lets a closed section be read without opening it. */
|
|
11
|
+
detail?: ReactNode;
|
|
12
|
+
defaultExpanded?: boolean;
|
|
13
|
+
expanded?: boolean;
|
|
14
|
+
onExpandedChange?: (expanded: boolean) => void;
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
/** The heading level of the title, so the outline of the page stays true. */
|
|
17
|
+
level?: 2 | 3 | 4 | 5 | 6;
|
|
18
|
+
};
|
|
19
|
+
export declare function Disclosure({ title, children, id, detail, defaultExpanded, expanded, onExpandedChange, disabled, level, }: DisclosureProps): import("react").JSX.Element;
|
|
20
|
+
/**
|
|
21
|
+
* Several `Disclosure`s that work as one: by default opening one closes the other.
|
|
22
|
+
*
|
|
23
|
+
* Context: a FAQ or the groups of a long form read better one at a time — the open section
|
|
24
|
+
* is the one being read and the rest stay as an index. `allowsMultipleExpanded` is for when
|
|
25
|
+
* the sections are compared, not read (the lines of two groups side by side). The value is
|
|
26
|
+
* a list of the sections' `id`s, not React Aria's `Set<Key>`, like every other control.
|
|
27
|
+
*/
|
|
28
|
+
export declare function Accordion({ children, allowsMultipleExpanded, defaultExpanded, expanded, onExpandedChange, disabled, }: {
|
|
29
|
+
/** `Disclosure`s, each with its `id`. */
|
|
30
|
+
children: ReactNode;
|
|
31
|
+
allowsMultipleExpanded?: boolean;
|
|
32
|
+
defaultExpanded?: string[];
|
|
33
|
+
expanded?: string[];
|
|
34
|
+
onExpandedChange?: (ids: string[]) => void;
|
|
35
|
+
disabled?: boolean;
|
|
36
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { cn, focusRing, tv } from "../../foundations/variants.js";
|
|
4
|
+
import { ChevronRight } from "lucide-react";
|
|
5
|
+
import { useContext } from "react";
|
|
6
|
+
import { Button as AriaButton, Disclosure as AriaDisclosure, DisclosureGroup as AriaDisclosureGroup, DisclosureGroupStateContext, DisclosurePanel, DisclosureStateContext, Heading, } from "react-aria-components";
|
|
7
|
+
/**
|
|
8
|
+
* A section that folds: the advanced options of a form, a question of a FAQ, the lines of a
|
|
9
|
+
* group.
|
|
10
|
+
*
|
|
11
|
+
* Context: it is a HEADING ROW, not a card. What folds is part of the page it is on — the
|
|
12
|
+
* rest of the form, the rest of the list — so it is separated by hairlines and not boxed:
|
|
13
|
+
* a box would say «this is another thing», and the advanced options of an invoice are still
|
|
14
|
+
* the invoice. The title is set like a field label (`text-meta`, medium) because that is
|
|
15
|
+
* what it is: the name of what is underneath.
|
|
16
|
+
*
|
|
17
|
+
* The chevron points right when closed and turns down when open — the one mark everybody
|
|
18
|
+
* already reads as «there is more here». React Aria owns the rest: the row is a real button
|
|
19
|
+
* with `aria-expanded`, the panel is `hidden="until-found"` when closed (the browser's
|
|
20
|
+
* find-in-page still opens it), and the height animates from the measure it publishes.
|
|
21
|
+
*/
|
|
22
|
+
const row = tv({
|
|
23
|
+
extend: focusRing,
|
|
24
|
+
base: "flex w-full items-center gap-2 rounded-control px-2 py-2 text-left text-meta font-medium text-ink transition-colors duration-(--duration-instant) hover:bg-ink/6 pressed:bg-ink/10",
|
|
25
|
+
variants: {
|
|
26
|
+
isDisabled: {
|
|
27
|
+
true: "cursor-not-allowed text-muted hover:bg-transparent forced-colors:text-[GrayText]",
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
function Header({ title, detail, level, }) {
|
|
32
|
+
const isExpanded = useContext(DisclosureStateContext)?.isExpanded ?? false;
|
|
33
|
+
return (_jsx(Heading, { level: level, className: "m-0", children: _jsxs(AriaButton, { slot: "trigger", className: (state) => row(state), children: [_jsx(ChevronRight, { "aria-hidden": true, strokeWidth: 2, className: cn("size-(--size-icon-md) shrink-0 text-muted transition-transform duration-(--duration-quick) ease-(--ease-out)", isExpanded && "rotate-90") }), _jsx("span", { className: "min-w-0 flex-1 truncate", children: title }), detail === undefined ? null : (_jsx("span", { className: "shrink-0 text-micro font-normal text-muted tabular-nums", children: detail }))] }) }));
|
|
34
|
+
}
|
|
35
|
+
export function Disclosure({ title, children, id, detail, defaultExpanded, expanded, onExpandedChange, disabled, level = 3, }) {
|
|
36
|
+
// Inside an accordion the group draws the line above; alone, it closes itself on both sides.
|
|
37
|
+
const inGroup = useContext(DisclosureGroupStateContext) !== null;
|
|
38
|
+
return (_jsxs(AriaDisclosure, { id: id, defaultExpanded: defaultExpanded, isExpanded: expanded, onExpandedChange: onExpandedChange, isDisabled: disabled, className: cn("border-line py-1", inGroup ? "border-b" : "border-y"), children: [_jsx(Header, { title: title, detail: detail, level: level }), _jsx(DisclosurePanel, { className: "h-(--disclosure-panel-height) overflow-clip motion-safe:transition-[height] motion-safe:duration-(--duration-panel) motion-safe:ease-(--ease-out)", children: _jsx("div", { className: "px-2 pt-1 pb-3 text-meta text-ink", children: children }) })] }));
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Several `Disclosure`s that work as one: by default opening one closes the other.
|
|
42
|
+
*
|
|
43
|
+
* Context: a FAQ or the groups of a long form read better one at a time — the open section
|
|
44
|
+
* is the one being read and the rest stay as an index. `allowsMultipleExpanded` is for when
|
|
45
|
+
* the sections are compared, not read (the lines of two groups side by side). The value is
|
|
46
|
+
* a list of the sections' `id`s, not React Aria's `Set<Key>`, like every other control.
|
|
47
|
+
*/
|
|
48
|
+
export function Accordion({ children, allowsMultipleExpanded = false, defaultExpanded, expanded, onExpandedChange, disabled, }) {
|
|
49
|
+
return (_jsx(AriaDisclosureGroup, { allowsMultipleExpanded: allowsMultipleExpanded, defaultExpandedKeys: defaultExpanded, expandedKeys: expanded, onExpandedChange: onExpandedChange === undefined
|
|
50
|
+
? undefined
|
|
51
|
+
: (keys) => onExpandedChange([...keys].map(String)), isDisabled: disabled, className: "border-t border-line", children: children }));
|
|
52
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Accordion, Disclosure, type DisclosureProps, } from "./Disclosure.js";
|