@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,74 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { tv } from "../../foundations/variants.js";
|
|
4
|
+
import { Children, createContext, isValidElement, useContext, } from "react";
|
|
5
|
+
import { composeRenderProps, Focusable, OverlayArrow, Tooltip as AriaTooltip, TooltipTrigger as AriaTooltipTrigger, } from "react-aria-components";
|
|
6
|
+
/**
|
|
7
|
+
* The name of a control that has no visible name — an icon button of a table row, of a
|
|
8
|
+
* toolbar — shown when the pointer rests on it or the keyboard reaches it.
|
|
9
|
+
*
|
|
10
|
+
* Context: it is not the other two ways of putting words next to a mark.
|
|
11
|
+
*
|
|
12
|
+
* - `Hint` is CONTENT: a sentence of help that does not fit on the page, behind a `?`. It
|
|
13
|
+
* opens on press and stays, because it is meant to be read, on a tablet too.
|
|
14
|
+
* - The native `title` is the browser's tooltip: it shows late, unstyled, never on keyboard
|
|
15
|
+
* focus and never on touch — the person who navigates with Tab never learns what the
|
|
16
|
+
* trash can is. That is why icon targets no longer use it.
|
|
17
|
+
*
|
|
18
|
+
* `Tooltip` is the LABEL, shown: a few words, on hover AND on focus, never interactive, never
|
|
19
|
+
* pinned. It is drawn on the chrome (the dark surface of the shell) because it is not part
|
|
20
|
+
* of the page — it is a whisper about one control, and the dark box reads as that against
|
|
21
|
+
* a page of white surfaces. React Aria decides when: its default delay on the first hover,
|
|
22
|
+
* then the next tooltip of the row opens at once (warm-up), and Escape closes it.
|
|
23
|
+
*
|
|
24
|
+
* Every `IconButton` and `IconLink` already draws one with its `label`; write a
|
|
25
|
+
* `TooltipTrigger` by hand only for a control that is not an icon target, or to say more
|
|
26
|
+
* than the label.
|
|
27
|
+
*/
|
|
28
|
+
const tooltip = tv({
|
|
29
|
+
base: "group max-w-64 rounded-control bg-chrome px-2 py-1 text-micro text-chrome-ink shadow-float transition-[opacity,translate] duration-(--duration-quick) ease-(--ease-out) forced-colors:border forced-colors:border-[ButtonBorder] forced-colors:bg-[Canvas] forced-colors:text-[CanvasText]",
|
|
30
|
+
variants: {
|
|
31
|
+
isEntering: {
|
|
32
|
+
true: "opacity-0 placement-top:translate-y-1 placement-bottom:-translate-y-1 placement-left:translate-x-1 placement-right:-translate-x-1",
|
|
33
|
+
},
|
|
34
|
+
isExiting: {
|
|
35
|
+
true: "opacity-0",
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
export function Tooltip({ children, arrow = true, className, ...props }) {
|
|
40
|
+
return (_jsxs(AriaTooltip, { offset: arrow ? 8 : 4, ...props, className: composeRenderProps(className, (own, state) => tooltip({ ...state, className: own })), children: [arrow ? (_jsx(OverlayArrow, { children: _jsx("svg", { viewBox: "0 0 8 8", "aria-hidden": true, className: "block size-2 fill-chrome group-placement-bottom:rotate-180 group-placement-left:-rotate-90 group-placement-right:rotate-90 forced-colors:fill-[Canvas]", children: _jsx("path", { d: "M0 0 L4 4 L8 0" }) }) })) : null, children] }));
|
|
41
|
+
}
|
|
42
|
+
/** True inside the control slot of a `TooltipTrigger`: an `IconButton` written there leaves
|
|
43
|
+
* its own tooltip out, so the consumer's is the only one. */
|
|
44
|
+
const InTooltipTrigger = createContext(false);
|
|
45
|
+
export function useInTooltipTrigger() {
|
|
46
|
+
return useContext(InTooltipTrigger);
|
|
47
|
+
}
|
|
48
|
+
function isDisabledControl(control) {
|
|
49
|
+
if (!isValidElement(control))
|
|
50
|
+
return false;
|
|
51
|
+
return control.props.disabled === true || control.props.isDisabled === true;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Ties a control to its `Tooltip`: the control first, the tooltip second.
|
|
55
|
+
*
|
|
56
|
+
* Context: the timing is React Aria's on purpose — its default delay before the first one,
|
|
57
|
+
* and the warm-up that opens the next one at once while the pointer runs along a row of
|
|
58
|
+
* icons. A tooltip that opens on every pass of the mouse is noise; one that makes you wait
|
|
59
|
+
* on each icon of a row is a toll. `delay` is here for the rare control that needs it, not
|
|
60
|
+
* to tune each screen.
|
|
61
|
+
*
|
|
62
|
+
* ── Why the control is wrapped in `Focusable` here ──
|
|
63
|
+
*
|
|
64
|
+
* React Aria's trigger hands its pointer, focus and `aria-describedby` props to the control
|
|
65
|
+
* through context, and only a React Aria component reads that context. `Focusable` is the
|
|
66
|
+
* adapter for everything else: it clones the control with those props and a `ref`. Doing it
|
|
67
|
+
* here means a consumer writes `<TooltipTrigger><Button …/><Tooltip …/></TooltipTrigger>`
|
|
68
|
+
* with the system's own buttons — they forward `ref` and spread native props — or with a
|
|
69
|
+
* plain `<button>`, and never has to know the adapter exists.
|
|
70
|
+
*/
|
|
71
|
+
export function TooltipTrigger({ children, delay, closeDelay, disabled = false, }) {
|
|
72
|
+
const [control, ...tooltip] = Children.toArray(children);
|
|
73
|
+
return (_jsxs(AriaTooltipTrigger, { delay: delay, closeDelay: closeDelay, isDisabled: disabled, children: [_jsx(InTooltipTrigger.Provider, { value: true, children: _jsx(Focusable, { isDisabled: isDisabledControl(control), children: control }) }), tooltip] }));
|
|
74
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/* LO QUE FLOTA anclado a algo: el calendario de un selector de fecha, la lista de un
|
|
2
|
+
combobox, un formulario chico abierto desde un botón, el nombre de un ícono. */
|
|
3
|
+
export { Popover } from "./Popover.js";
|
|
4
|
+
export { PopoverTrigger, } from "./PopoverTrigger.js";
|
|
5
|
+
export { Tooltip, TooltipTrigger, } from "./Tooltip.js";
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type FormMode } from "../field/mode.js";
|
|
2
|
+
import type { LucideIcon } from "lucide-react";
|
|
3
|
+
import { type ReactNode } from "react";
|
|
4
|
+
export interface PickerItem {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
/** The data that allows choosing well, already formatted: stock on hand, a
|
|
8
|
+
* price, a tax id. In tabular figures, so they line up between options, which
|
|
9
|
+
* is what makes them comparable at a glance. */
|
|
10
|
+
meta?: string;
|
|
11
|
+
tags?: ReactNode;
|
|
12
|
+
icon?: LucideIcon;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
/** Extra text WHERE TO SEARCH that is not drawn: the internal code and the
|
|
15
|
+
* barcode of a product, the NIT of a customer.
|
|
16
|
+
*
|
|
17
|
+
* Context: it is the difference between being able to type the code printed
|
|
18
|
+
* on the box and finding the product, and having to remember what it was
|
|
19
|
+
* called. Drawing every code of every option is not an alternative. */
|
|
20
|
+
haystack?: string;
|
|
21
|
+
}
|
|
22
|
+
export type PickerProps = {
|
|
23
|
+
id?: string;
|
|
24
|
+
items: readonly PickerItem[];
|
|
25
|
+
value?: string;
|
|
26
|
+
onChange?: (id: string) => void;
|
|
27
|
+
placeholder?: string;
|
|
28
|
+
/** What to do with the ones that don't fit. Without this, the footer only
|
|
29
|
+
* informs. */
|
|
30
|
+
onBrowseAll?: () => void;
|
|
31
|
+
/** Open the chosen record. Drawn as a trailing affordance, the way a many2one
|
|
32
|
+
* offers "open" next to "pick". Absent when there is nothing chosen. */
|
|
33
|
+
onOpen?: (id: string) => void;
|
|
34
|
+
emptyText?: string;
|
|
35
|
+
/** Placeholder of the field while it is being typed into. It is NOT a second
|
|
36
|
+
* box — the field itself is the search. */
|
|
37
|
+
searchPlaceholder?: string;
|
|
38
|
+
/** The options are still coming. It gets said, not hidden: an empty picker
|
|
39
|
+
* while loading reads as "there is nothing", which is a different answer. */
|
|
40
|
+
loading?: boolean;
|
|
41
|
+
loadingText?: string;
|
|
42
|
+
describedBy?: string;
|
|
43
|
+
invalid?: boolean;
|
|
44
|
+
disabled?: boolean;
|
|
45
|
+
/** Forces the mode. By default it inherits the form's: in read mode it draws the name. */
|
|
46
|
+
mode?: FormMode;
|
|
47
|
+
};
|
|
48
|
+
export declare function Picker({ mode, ...props }: PickerProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { AriaLocale } from "../../foundations/aria.js";
|
|
4
|
+
import { fillCopy, useUiCopy } from "../../foundations/copy.js";
|
|
5
|
+
import { cn, tv } from "../../foundations/variants.js";
|
|
6
|
+
import { IconButton } from "../button/Button.js";
|
|
7
|
+
import { FieldButton, FieldGroup } from "../field/aria.js";
|
|
8
|
+
import { useFieldIds } from "../field/context.js";
|
|
9
|
+
import { ReadValue } from "../field/Field.js";
|
|
10
|
+
import { useFormMode } from "../field/mode.js";
|
|
11
|
+
import { Popover } from "../overlay/Popover.js";
|
|
12
|
+
import { Check, ChevronDown, SquareArrowOutUpRight } from "lucide-react";
|
|
13
|
+
import { useContext, useMemo, useRef, useState } from "react";
|
|
14
|
+
import { ComboBox as AriaComboBox, ComboBoxStateContext, Input, ListBox, ListBoxItem, Text, useFilter, } from "react-aria-components";
|
|
15
|
+
/** The id of the «see all…» row. Not a record id: it cannot collide with one. */
|
|
16
|
+
const BROWSE_ALL = "\u0000browse-all";
|
|
17
|
+
const CAP = 8;
|
|
18
|
+
/* No focus ring on an option: the focus stays in the input (the list is driven from it),
|
|
19
|
+
and the marked row is said by its tint — a ring inside the list as well as around the
|
|
20
|
+
field is two marks for one place. */
|
|
21
|
+
const option = tv({
|
|
22
|
+
base: "flex w-full min-w-0 cursor-default gap-2 rounded-control px-2 text-left outline-0 select-none forced-color-adjust-none",
|
|
23
|
+
variants: {
|
|
24
|
+
hints: {
|
|
25
|
+
true: "items-start py-1.5",
|
|
26
|
+
false: "items-center py-1",
|
|
27
|
+
},
|
|
28
|
+
isFocused: {
|
|
29
|
+
true: "bg-primary/8 forced-colors:bg-[Highlight] forced-colors:text-[HighlightText]",
|
|
30
|
+
},
|
|
31
|
+
isHovered: {
|
|
32
|
+
true: "bg-ink/4",
|
|
33
|
+
},
|
|
34
|
+
isPressed: {
|
|
35
|
+
true: "bg-primary/12",
|
|
36
|
+
},
|
|
37
|
+
isDisabled: {
|
|
38
|
+
true: "opacity-45 forced-colors:text-[GrayText]",
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
compoundVariants: [{ isFocused: true, isHovered: true, class: "bg-primary/8" }],
|
|
42
|
+
});
|
|
43
|
+
export function Picker({ mode, ...props }) {
|
|
44
|
+
if (useFormMode(mode) === "read") {
|
|
45
|
+
return _jsx(ReadValue, { value: props.items.find((i) => i.id === props.value)?.name ?? null });
|
|
46
|
+
}
|
|
47
|
+
/* The filter reads the locale from React Aria's context, so the box that provides it has
|
|
48
|
+
to be ABOVE the component that asks for it. */
|
|
49
|
+
return (_jsx(AriaLocale, { children: _jsx(EditablePicker, { ...props }) }));
|
|
50
|
+
}
|
|
51
|
+
function EditablePicker({ id: ownId, items, value, onChange, placeholder, onBrowseAll, onOpen, emptyText, searchPlaceholder, loading = false, loadingText, describedBy: ownDescribedBy, invalid: ownInvalid, disabled = false, }) {
|
|
52
|
+
const { id, labelledBy, describedBy, invalid } = useFieldIds({
|
|
53
|
+
id: ownId,
|
|
54
|
+
describedBy: ownDescribedBy,
|
|
55
|
+
invalid: ownInvalid,
|
|
56
|
+
});
|
|
57
|
+
const copy = useUiCopy();
|
|
58
|
+
const { contains } = useFilter({ sensitivity: "base" });
|
|
59
|
+
const shownPlaceholder = placeholder ?? copy.picker.placeholder;
|
|
60
|
+
const shownEmpty = emptyText ?? copy.picker.empty;
|
|
61
|
+
const shownSearch = searchPlaceholder ?? copy.picker.search;
|
|
62
|
+
const shownLoading = loadingText ?? copy.picker.loading;
|
|
63
|
+
const chosen = items.find((i) => i.id === value);
|
|
64
|
+
const chosenName = chosen?.name ?? "";
|
|
65
|
+
/* Both the text and the choice are controlled, so React Aria never rewrites the field
|
|
66
|
+
from its own collection — which is capped and filtered, and may not hold the chosen
|
|
67
|
+
record at all. `query` is what filters: empty when the list opens (the whole list,
|
|
68
|
+
not just the one already chosen), the typed text after that. */
|
|
69
|
+
const [open, setOpen] = useState(false);
|
|
70
|
+
const [text, setText] = useState(chosenName);
|
|
71
|
+
const [query, setQuery] = useState("");
|
|
72
|
+
const [shownFor, setShownFor] = useState(chosenName);
|
|
73
|
+
if (!open && shownFor !== chosenName) {
|
|
74
|
+
setShownFor(chosenName);
|
|
75
|
+
setText(chosenName);
|
|
76
|
+
}
|
|
77
|
+
const filtered = useMemo(() => {
|
|
78
|
+
const q = query.trim();
|
|
79
|
+
if (q === "")
|
|
80
|
+
return items;
|
|
81
|
+
return items.filter((i) => contains(`${i.name} ${i.meta ?? ""} ${i.haystack ?? ""}`, q));
|
|
82
|
+
}, [items, query, contains]);
|
|
83
|
+
const visible = loading ? [] : filtered.slice(0, CAP);
|
|
84
|
+
const remaining = loading ? 0 : filtered.length - visible.length;
|
|
85
|
+
const hasMarks = items.some((i) => i.icon !== undefined);
|
|
86
|
+
const hasHints = visible.some((i) => i.meta !== undefined);
|
|
87
|
+
/* «See all…» is an OPTION at the end of the list and not a button in a footer: the
|
|
88
|
+
arrows reach it like any other row. As a footer button it was out of the keyboard's
|
|
89
|
+
path — Tab leaves the field and closes the list before getting there. */
|
|
90
|
+
const browsable = remaining > 0 && onBrowseAll !== undefined;
|
|
91
|
+
const listed = browsable
|
|
92
|
+
? [...visible, { id: BROWSE_ALL, name: copy.picker.browseAll }]
|
|
93
|
+
: visible;
|
|
94
|
+
const closeList = useRef(null);
|
|
95
|
+
const take = (key) => {
|
|
96
|
+
if (key === null)
|
|
97
|
+
return;
|
|
98
|
+
const next = String(key);
|
|
99
|
+
if (next === BROWSE_ALL) {
|
|
100
|
+
/* The full list opens somewhere else (a dialog, a panel): this one closes and the
|
|
101
|
+
field keeps the chosen name. */
|
|
102
|
+
closeList.current?.();
|
|
103
|
+
setText(chosenName);
|
|
104
|
+
setQuery("");
|
|
105
|
+
onBrowseAll?.();
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
setText(items.find((i) => i.id === next)?.name ?? "");
|
|
109
|
+
setQuery("");
|
|
110
|
+
if (next !== value)
|
|
111
|
+
onChange?.(next);
|
|
112
|
+
};
|
|
113
|
+
return (_jsxs(AriaComboBox, { id: id, "aria-labelledby": labelledBy, "aria-describedby": describedBy, items: listed, value: value ?? null, onChange: take, inputValue: text, onInputChange: (next) => {
|
|
114
|
+
setText(next);
|
|
115
|
+
setQuery(next);
|
|
116
|
+
}, onOpenChange: (isOpen, trigger) => {
|
|
117
|
+
setOpen(isOpen);
|
|
118
|
+
if (isOpen && trigger !== "input")
|
|
119
|
+
setQuery("");
|
|
120
|
+
if (!isOpen) {
|
|
121
|
+
setText(chosenName);
|
|
122
|
+
setQuery("");
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
/* Opening on focus is what a many2one does: you arrive at the field to choose, and
|
|
126
|
+
the options are the answer before the first letter. */
|
|
127
|
+
menuTrigger: "focus",
|
|
128
|
+
/* An empty list stays open to SAY it is empty (or loading) instead of vanishing. */
|
|
129
|
+
allowsEmptyCollection: true, isInvalid: invalid, isDisabled: disabled, className: "w-full min-w-0", children: [_jsx(CloseBridge, { into: closeList }), _jsxs(FieldGroup, { children: [_jsx(Input, { placeholder: loading ? shownLoading : open ? shownSearch : shownPlaceholder, title: shownSearch,
|
|
130
|
+
/* Arriving selects the name, so the first letter typed replaces it. Only when
|
|
131
|
+
ARRIVING: React Aria hands the focus back to the input while the list is open,
|
|
132
|
+
and selecting again then would swallow every second letter. */
|
|
133
|
+
onFocus: (event) => {
|
|
134
|
+
if (!open)
|
|
135
|
+
event.currentTarget.select();
|
|
136
|
+
}, className: "h-full min-w-0 flex-1 bg-transparent px-2.5 text-meta text-ink outline-0 placeholder:text-muted disabled:cursor-not-allowed" }), value !== undefined && value !== "" && onOpen !== undefined ? (_jsx(IconButton, { icon: SquareArrowOutUpRight, label: copy.picker.open, size: "sm", disabled: disabled, onClick: () => onOpen(value) })) : null, _jsx(FieldButton, { className: "mr-1", children: _jsx(ChevronDown, { "aria-hidden": true, className: "size-(--size-icon-md)", strokeWidth: 2 }) })] }), _jsxs(Popover, { placement: "bottom start", className: "flex w-(--trigger-width) flex-col overflow-hidden", children: [_jsx(ListBox, { items: listed, renderEmptyState: () => (_jsx("p", { className: "px-3 py-3 text-center text-meta text-muted", children: loading ? shownLoading : shownEmpty })), className: "min-h-0 flex-1 overflow-y-auto p-1 outline-0", children: (item) => item.id === BROWSE_ALL ? (_jsx(ListBoxItem, { id: BROWSE_ALL, textValue: item.name, className: (state) => `${option({ ...state, hints: false })} mt-1 border-t border-line text-primary`, children: _jsxs("span", { className: "flex w-full items-center justify-between gap-2", children: [_jsx("span", { className: "font-medium", children: item.name }), _jsx("span", { className: "text-micro tabular-nums text-muted", children: fillCopy(copy.picker.more, {
|
|
137
|
+
count: remaining.toLocaleString(copy.locale),
|
|
138
|
+
}) })] }) })) : (_jsx(ListBoxItem, { id: item.id, textValue: item.name, isDisabled: item.disabled, className: (state) => option({ ...state, hints: hasHints }), children: ({ isSelected }) => (_jsx(Row, { item: item, selected: isSelected, marks: hasMarks, hints: hasHints })) })) }), remaining > 0 && !browsable ? (_jsxs("div", { className: "flex shrink-0 items-center gap-2 border-t border-line bg-ground px-2.5 py-1.5", children: [_jsx("span", { className: "text-micro tabular-nums text-muted", children: fillCopy(copy.picker.more, { count: remaining.toLocaleString(copy.locale) }) }), _jsx("span", { className: "text-micro text-muted", children: copy.picker.refine })] })) : null] })] }));
|
|
139
|
+
}
|
|
140
|
+
function Row({ item, selected, marks, hints, }) {
|
|
141
|
+
const Icon = item.icon;
|
|
142
|
+
return (_jsxs(_Fragment, { children: [marks ? (_jsx("span", { className: cn(hints ? "mt-0.5" : "", "flex size-(--size-icon-sm) shrink-0 items-center justify-center"), children: selected ? (_jsx(Check, { "aria-hidden": true, className: "size-(--size-icon-sm) text-primary forced-colors:text-[HighlightText]", strokeWidth: 2.5 })) : Icon !== undefined ? (_jsx(Icon, { "aria-hidden": true, className: "size-(--size-icon-sm) text-muted", strokeWidth: 1.75 })) : null })) : null, _jsxs("span", { className: "flex min-w-0 flex-1 flex-col gap-0.5", children: [_jsxs("span", { className: "flex min-w-0 items-center gap-1.5", children: [_jsx(Text, { slot: "label", className: cn("min-w-0 truncate text-meta text-ink", selected ? "font-medium" : ""), children: item.name }), item.tags] }), item.meta === undefined ? null : (_jsx(Text, { slot: "description", className: "truncate text-micro tabular-nums text-muted", children: item.meta }))] }), !marks && selected ? (_jsx(Check, { "aria-hidden": true, className: "size-(--size-icon-sm) shrink-0 text-primary forced-colors:text-[HighlightText]", strokeWidth: 2.5 })) : null] }));
|
|
143
|
+
}
|
|
144
|
+
/** Hands the combobox's `close` to the code outside it: React Aria owns the open state and
|
|
145
|
+
* offers no controlled `isOpen`, and the «see all…» row has to close the list it lives in. */
|
|
146
|
+
function CloseBridge({ into }) {
|
|
147
|
+
const state = useContext(ComboBoxStateContext);
|
|
148
|
+
into.current = state === null ? null : () => state.close();
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Picker, type PickerItem, type PickerProps, } from "./Picker.js";
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export interface PeekProps {
|
|
3
|
+
open: boolean;
|
|
4
|
+
title: ReactNode;
|
|
5
|
+
subtitle?: ReactNode;
|
|
6
|
+
/** The × button and Escape (while the focus is inside the drawer). */
|
|
7
|
+
onClose: () => void;
|
|
8
|
+
/** Promoting preserves the context: it is the same view changing surface. */
|
|
9
|
+
onPromote?: () => void;
|
|
10
|
+
/** The actions of the record being previewed, next to «open as panel». */
|
|
11
|
+
actions?: ReactNode;
|
|
12
|
+
children: ReactNode;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The drawer that previews a record next to the list it came from.
|
|
16
|
+
*
|
|
17
|
+
* Context: the drawer comes in from the right and does NOT dim what is behind
|
|
18
|
+
* it — that is the property that defines it. The list it came from stays
|
|
19
|
+
* clickable, which is the whole point of a preview; a scrim would turn it into
|
|
20
|
+
* a dialog, and a dialog is for what has to be answered before moving on.
|
|
21
|
+
* Pressing another row replaces what the drawer shows; it never opens a second one.
|
|
22
|
+
*
|
|
23
|
+
* It is anchored to the work area and not to the window: it covers the content
|
|
24
|
+
* without covering the menu or the activity bar, which are still navigation.
|
|
25
|
+
* The left corners are the panel radius. The sheet still spans the work area;
|
|
26
|
+
* only the cut against the list is rounded, so the drawer reads as a panel.
|
|
27
|
+
*
|
|
28
|
+
* ── On a phone it is a bottom sheet ───────────────────────────────────
|
|
29
|
+
*
|
|
30
|
+
* Below `sm` (40rem, `--container-handheld`) there is no «beside the list»: 30rem from
|
|
31
|
+
* the right of a 390 px screen is the whole screen. So the same drawer anchors to the
|
|
32
|
+
* BOTTOM edge of the window — full width, top corners rounded, at most `--size-sheet`
|
|
33
|
+
* (90dvh) tall, its body scrolling inside — and comes in from below. The strip left on
|
|
34
|
+
* top still shows the list, and it is still not dimmed: what defines the drawer survives
|
|
35
|
+
* the change of edge. It is `fixed` there and not `absolute` because on a phone the work
|
|
36
|
+
* area is as tall as its content, and its bottom edge can be three screens down; the
|
|
37
|
+
* screen's bottom edge is where the thumb is. (Like any `fixed`, it is anchored to the
|
|
38
|
+
* window only if no ancestor has a `transform` or containment — mount it next to the
|
|
39
|
+
* list, not inside a `@container`.) CSS only: the focus, Escape and restore below do not
|
|
40
|
+
* change.
|
|
41
|
+
*
|
|
42
|
+
* ── Why it is not React Aria's `ModalOverlay` ─────────────────────────
|
|
43
|
+
*
|
|
44
|
+
* Every overlay React Aria has —`ModalOverlay`, `Sheet`— is modal: it makes the rest of the
|
|
45
|
+
* page `inert`, traps the focus and portals to the window. Each of those breaks a promise
|
|
46
|
+
* above: the list would stop answering, and the drawer would cover the menu. So the drawer
|
|
47
|
+
* is an `<aside>` in the work area (the «preview» landmark) holding a NON-modal React Aria
|
|
48
|
+
* `Dialog`:
|
|
49
|
+
*
|
|
50
|
+
* - `Dialog` — `role="dialog"` without `aria-modal`, named by its title (`Heading
|
|
51
|
+
* slot="title"`), and on open it takes the focus itself — else a keyboard user would have
|
|
52
|
+
* to tab through the whole list to reach it. Focusing the panel and not its first button
|
|
53
|
+
* keeps the close tooltip from popping up on every open.
|
|
54
|
+
* - `FocusScope restoreFocus` — on close the focus goes back to the row that opened it.
|
|
55
|
+
* There is no `contain`: Tab may walk out to the list, which is still live.
|
|
56
|
+
* - `useKeyboard` — Escape closes it while the focus is INSIDE the drawer. A listener on
|
|
57
|
+
* the window closed it from anywhere, competing with every other Escape on the page (a
|
|
58
|
+
* search field clearing, a list dropping its selection).
|
|
59
|
+
*/
|
|
60
|
+
export declare function Peek({ open, title, subtitle, onClose, onPromote, actions, children, }: PeekProps): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useUiCopy } from "../../foundations/copy.js";
|
|
4
|
+
import { DensityProvider } from "../../foundations/density.js";
|
|
5
|
+
import { cn } from "../../foundations/variants.js";
|
|
6
|
+
import { Button, IconButton } from "../button/Button.js";
|
|
7
|
+
import { Tag } from "../tag/Tag.js";
|
|
8
|
+
import { SquareArrowOutUpRight, X } from "lucide-react";
|
|
9
|
+
import { FocusScope, useKeyboard } from "react-aria";
|
|
10
|
+
import { Dialog, Heading } from "react-aria-components";
|
|
11
|
+
/**
|
|
12
|
+
* The drawer that previews a record next to the list it came from.
|
|
13
|
+
*
|
|
14
|
+
* Context: the drawer comes in from the right and does NOT dim what is behind
|
|
15
|
+
* it — that is the property that defines it. The list it came from stays
|
|
16
|
+
* clickable, which is the whole point of a preview; a scrim would turn it into
|
|
17
|
+
* a dialog, and a dialog is for what has to be answered before moving on.
|
|
18
|
+
* Pressing another row replaces what the drawer shows; it never opens a second one.
|
|
19
|
+
*
|
|
20
|
+
* It is anchored to the work area and not to the window: it covers the content
|
|
21
|
+
* without covering the menu or the activity bar, which are still navigation.
|
|
22
|
+
* The left corners are the panel radius. The sheet still spans the work area;
|
|
23
|
+
* only the cut against the list is rounded, so the drawer reads as a panel.
|
|
24
|
+
*
|
|
25
|
+
* ── On a phone it is a bottom sheet ───────────────────────────────────
|
|
26
|
+
*
|
|
27
|
+
* Below `sm` (40rem, `--container-handheld`) there is no «beside the list»: 30rem from
|
|
28
|
+
* the right of a 390 px screen is the whole screen. So the same drawer anchors to the
|
|
29
|
+
* BOTTOM edge of the window — full width, top corners rounded, at most `--size-sheet`
|
|
30
|
+
* (90dvh) tall, its body scrolling inside — and comes in from below. The strip left on
|
|
31
|
+
* top still shows the list, and it is still not dimmed: what defines the drawer survives
|
|
32
|
+
* the change of edge. It is `fixed` there and not `absolute` because on a phone the work
|
|
33
|
+
* area is as tall as its content, and its bottom edge can be three screens down; the
|
|
34
|
+
* screen's bottom edge is where the thumb is. (Like any `fixed`, it is anchored to the
|
|
35
|
+
* window only if no ancestor has a `transform` or containment — mount it next to the
|
|
36
|
+
* list, not inside a `@container`.) CSS only: the focus, Escape and restore below do not
|
|
37
|
+
* change.
|
|
38
|
+
*
|
|
39
|
+
* ── Why it is not React Aria's `ModalOverlay` ─────────────────────────
|
|
40
|
+
*
|
|
41
|
+
* Every overlay React Aria has —`ModalOverlay`, `Sheet`— is modal: it makes the rest of the
|
|
42
|
+
* page `inert`, traps the focus and portals to the window. Each of those breaks a promise
|
|
43
|
+
* above: the list would stop answering, and the drawer would cover the menu. So the drawer
|
|
44
|
+
* is an `<aside>` in the work area (the «preview» landmark) holding a NON-modal React Aria
|
|
45
|
+
* `Dialog`:
|
|
46
|
+
*
|
|
47
|
+
* - `Dialog` — `role="dialog"` without `aria-modal`, named by its title (`Heading
|
|
48
|
+
* slot="title"`), and on open it takes the focus itself — else a keyboard user would have
|
|
49
|
+
* to tab through the whole list to reach it. Focusing the panel and not its first button
|
|
50
|
+
* keeps the close tooltip from popping up on every open.
|
|
51
|
+
* - `FocusScope restoreFocus` — on close the focus goes back to the row that opened it.
|
|
52
|
+
* There is no `contain`: Tab may walk out to the list, which is still live.
|
|
53
|
+
* - `useKeyboard` — Escape closes it while the focus is INSIDE the drawer. A listener on
|
|
54
|
+
* the window closed it from anywhere, competing with every other Escape on the page (a
|
|
55
|
+
* search field clearing, a list dropping its selection).
|
|
56
|
+
*/
|
|
57
|
+
export function Peek({ open, title, subtitle, onClose, onPromote, actions, children, }) {
|
|
58
|
+
const copy = useUiCopy();
|
|
59
|
+
const { keyboardProps } = useKeyboard({
|
|
60
|
+
onKeyDown: (e) => {
|
|
61
|
+
if (e.key === "Escape")
|
|
62
|
+
onClose();
|
|
63
|
+
else
|
|
64
|
+
e.continuePropagation();
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
if (!open)
|
|
68
|
+
return null;
|
|
69
|
+
const hasActions = onPromote !== undefined || actions !== undefined;
|
|
70
|
+
return (_jsx(FocusScope, { restoreFocus: true, children: _jsx("aside", { ...keyboardProps, "aria-label": copy.shell.preview, className: "peek-width absolute top-0 right-0 bottom-0 z-30 overflow-hidden rounded-l-panel border-l border-line-strong bg-surface shadow-window transition-[opacity,translate] duration-(--duration-panel) ease-(--ease-out) starting:translate-x-4 starting:opacity-0 max-sm:fixed max-sm:top-auto max-sm:left-0 max-sm:max-h-(--size-sheet) max-sm:w-full max-sm:rounded-tr-panel max-sm:rounded-bl-none max-sm:border-t max-sm:border-l-0 max-sm:starting:translate-x-0 max-sm:starting:translate-y-4", children: _jsxs(Dialog, { className: "flex h-full max-h-[inherit] flex-col outline-0", children: [_jsxs("header", { className: "shrink-0 border-b border-line", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-2 px-3 py-2", children: [_jsx(Tag, { tone: "info", children: copy.shell.previewTag }), _jsx(Heading, { slot: "title", level: 2, className: "min-w-0 flex-1 truncate text-meta font-medium text-ink", children: title }), _jsx(IconButton, { icon: X, label: copy.shell.closePreview, onClick: onClose })] }), subtitle === undefined ? null : (_jsx("p", { className: "truncate px-3 pb-2 text-micro tabular-nums text-muted", children: subtitle }))] }), hasActions ? (_jsxs("div", { className: "flex min-w-0 shrink-0 flex-wrap items-center gap-1.5 border-b border-line px-3 py-1.5 max-sm:order-last max-sm:border-t max-sm:border-b-0 max-sm:pt-2 max-sm:safe-bottom", children: [onPromote === undefined ? null : (_jsx(Button, { role: "secondary", size: "sm", icon: SquareArrowOutUpRight, onClick: onPromote, children: copy.shell.openAsPanel })), actions] })) : null, _jsx("div", { className: cn("min-h-0 flex-1 overflow-y-auto px-3 py-3", !hasActions && "max-sm:safe-bottom"), children: _jsx(DensityProvider, { value: "compact", children: children }) })] }) }) }));
|
|
71
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Peek, type PeekProps } from "./Peek.js";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* One cell of a `DataTable` that edits in place: its drawing becomes a button that opens
|
|
4
|
+
* `editor` in a popover anchored to the cell.
|
|
5
|
+
*
|
|
6
|
+
* Internal to the table family: a column asks for it with `DataColumn.edit`.
|
|
7
|
+
*/
|
|
8
|
+
export declare function CellEditor({ label, numeric, editor, children, }: {
|
|
9
|
+
/** The verb and the column: «Editar vencimiento». Names the button and the popover. */
|
|
10
|
+
label: string;
|
|
11
|
+
numeric: boolean;
|
|
12
|
+
editor: (done: () => void) => ReactNode;
|
|
13
|
+
children: ReactNode;
|
|
14
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { cn, focusRing } from "../../foundations/variants.js";
|
|
4
|
+
import { Popover } from "../overlay/Popover.js";
|
|
5
|
+
import { Pencil } from "lucide-react";
|
|
6
|
+
import { useEffect, useId, useRef } from "react";
|
|
7
|
+
import { Button as AriaButton, Dialog, DialogTrigger } from "react-aria-components";
|
|
8
|
+
/* ==================================================================== *
|
|
9
|
+
* CELL EDITOR — change one value of a list without leaving the list.
|
|
10
|
+
*
|
|
11
|
+
* Context: the salesperson of an invoice, its due date, its stage. Opening the record to
|
|
12
|
+
* change one field and coming back is three screens for one value, and the person loses
|
|
13
|
+
* the row they were on. Odoo answers it with the editable list; this is the step before it:
|
|
14
|
+
* the list stays a list, and ONE cell opens a small popover with the right control —
|
|
15
|
+
* a `Picker`, a `Calendar`, a `Select` — anchored to the cell, the way a date picker opens
|
|
16
|
+
* its calendar under its field.
|
|
17
|
+
*
|
|
18
|
+
* ── The cell IS the button ──────────────────────────────────────────
|
|
19
|
+
*
|
|
20
|
+
* The whole drawing of the cell is the trigger, not a pencil on its side: the target is
|
|
21
|
+
* the value the eye is already on (Fitts), and a column of twelve pencils is noise. The
|
|
22
|
+
* pencil only appears where the pointer or the keyboard is — on hover and on visible
|
|
23
|
+
* focus — the same rule `RowActions` follows: an affordance without a subject yet is not
|
|
24
|
+
* shown. That is also why an editable column must not draw links: they would be a button
|
|
25
|
+
* inside a button.
|
|
26
|
+
*
|
|
27
|
+
* ── React Aria's dialog trigger ─────────────────────────────────────
|
|
28
|
+
*
|
|
29
|
+
* `DialogTrigger` + `Popover` + `Dialog`: Escape and a click outside close it, and the
|
|
30
|
+
* focus goes back to the cell it came from, so the keyboard continues down the column.
|
|
31
|
+
* What commits is the editor's own gesture (choosing a person, pressing a day), and then
|
|
32
|
+
* it calls `done`. Closing without that gesture changes nothing — Escape is «cancel».
|
|
33
|
+
*
|
|
34
|
+
* The button's name is the verb plus the value («Editar vencimiento 15/11/2026»): the
|
|
35
|
+
* value alone does not say it can be changed, the verb alone does not say what it is.
|
|
36
|
+
* ==================================================================== */
|
|
37
|
+
/** The first control of the editor gets the focus, unless the editor already placed it
|
|
38
|
+
* (a `Calendar` with `autoFocus`). Arriving in a popover with the focus on its frame is one
|
|
39
|
+
* extra Tab before the first key does anything. */
|
|
40
|
+
function useFocusFirst(scope) {
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
const frame = requestAnimationFrame(() => {
|
|
43
|
+
const root = scope.current;
|
|
44
|
+
if (root === null)
|
|
45
|
+
return;
|
|
46
|
+
const active = document.activeElement;
|
|
47
|
+
if (active !== null && active !== root && root.contains(active))
|
|
48
|
+
return;
|
|
49
|
+
for (const selector of [
|
|
50
|
+
"input:not([disabled])",
|
|
51
|
+
/* A calendar: the day the keyboard is on, not the «previous month» arrow. */
|
|
52
|
+
'[role="grid"] [tabindex="0"]',
|
|
53
|
+
"button:not([disabled])",
|
|
54
|
+
'[tabindex="0"]',
|
|
55
|
+
]) {
|
|
56
|
+
const target = root.querySelector(selector);
|
|
57
|
+
if (target !== null) {
|
|
58
|
+
target.focus();
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
return () => cancelAnimationFrame(frame);
|
|
64
|
+
}, [scope]);
|
|
65
|
+
}
|
|
66
|
+
function EditorDialog({ label, editor, }) {
|
|
67
|
+
const ref = useRef(null);
|
|
68
|
+
useFocusFirst(ref);
|
|
69
|
+
return (_jsx(Dialog, { ref: ref, "aria-label": label, className: "max-h-[inherit] max-w-sm min-w-56 overflow-auto p-2 outline-0", children: ({ close }) => editor(close) }));
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* One cell of a `DataTable` that edits in place: its drawing becomes a button that opens
|
|
73
|
+
* `editor` in a popover anchored to the cell.
|
|
74
|
+
*
|
|
75
|
+
* Internal to the table family: a column asks for it with `DataColumn.edit`.
|
|
76
|
+
*/
|
|
77
|
+
export function CellEditor({ label, numeric, editor, children, }) {
|
|
78
|
+
const verb = useId();
|
|
79
|
+
const shown = useId();
|
|
80
|
+
return (
|
|
81
|
+
/* The row may open its record on click; changing one cell must not also open it. */
|
|
82
|
+
_jsx("div", { onClick: (event) => event.stopPropagation(), className: "-mx-1 min-w-0", children: _jsxs(DialogTrigger, { children: [_jsxs(AriaButton, { "aria-labelledby": `${verb} ${shown}`, className: ({ isFocusVisible }) => cn("group/edit flex w-full min-w-0 cursor-pointer items-center gap-1.5 rounded-mark px-1 text-inherit outline-0 transition-colors duration-(--duration-instant) hover:bg-ink/6 pressed:bg-ink/10 aria-expanded:bg-primary/8", numeric ? "flex-row-reverse text-right" : "text-left", focusRing({ isFocusVisible })), children: [_jsx("span", { id: verb, hidden: true, children: label }), _jsx("span", { id: shown, className: "min-w-0 flex-1 truncate", children: children }), _jsx(Pencil, { "aria-hidden": true, strokeWidth: 2, className: "size-(--size-icon-sm) shrink-0 text-muted opacity-0 transition-opacity duration-(--duration-instant) group-hover/edit:opacity-100 group-focus-visible/edit:opacity-100 group-aria-expanded/edit:opacity-100" })] }), _jsx(Popover, { placement: numeric ? "bottom end" : "bottom start", children: _jsx(EditorDialog, { label: label, editor: editor }) })] }) }));
|
|
83
|
+
}
|