@sincpro/web-ui 0.2.0 → 1.0.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/README.md +3 -3
- package/dist/components/activity/ActivityBadge.d.ts +16 -0
- package/dist/components/activity/ActivityBadge.js +29 -0
- package/dist/components/activity/index.d.ts +1 -0
- package/dist/components/activity/index.js +1 -0
- package/dist/components/calendar/RecordCalendar.d.ts +48 -0
- package/dist/components/calendar/RecordCalendar.js +101 -0
- package/dist/components/calendar/index.d.ts +1 -0
- package/dist/components/calendar/index.js +1 -0
- package/dist/components/form/Sheet.js +5 -1
- package/dist/components/gantt/RecordGantt.d.ts +44 -0
- package/dist/components/gantt/RecordGantt.js +110 -0
- package/dist/components/gantt/index.d.ts +1 -0
- package/dist/components/gantt/index.js +1 -0
- package/dist/components/index.d.ts +3 -0
- package/dist/components/index.js +3 -0
- package/dist/components/lines/LineItems.d.ts +5 -2
- package/dist/components/lines/LineItems.js +3 -3
- package/dist/components/listing/BoardColumns.d.ts +39 -0
- package/dist/components/listing/BoardColumns.js +138 -0
- package/dist/components/listing/ListingBar.d.ts +5 -0
- package/dist/components/listing/ListingBar.js +5 -2
- package/dist/components/listing/PivotExplorer.d.ts +34 -0
- package/dist/components/listing/PivotExplorer.js +86 -0
- package/dist/components/listing/RecordCard.d.ts +6 -8
- package/dist/components/listing/RecordCard.js +13 -7
- package/dist/components/listing/Row.d.ts +21 -2
- package/dist/components/listing/Row.js +29 -13
- package/dist/components/listing/index.d.ts +2 -0
- package/dist/components/listing/index.js +2 -0
- package/dist/components/listing/set.d.ts +1 -1
- package/dist/components/navigation/index.d.ts +0 -2
- package/dist/components/navigation/index.js +2 -3
- package/dist/components/record/RecordAudit.d.ts +17 -0
- package/dist/components/record/RecordAudit.js +27 -0
- package/dist/components/record/RecordHeader.d.ts +7 -1
- package/dist/components/record/RecordHeader.js +30 -18
- package/dist/components/record/RecordRibbon.d.ts +13 -0
- package/dist/components/record/RecordRibbon.js +22 -0
- package/dist/components/record/index.d.ts +2 -0
- package/dist/components/record/index.js +2 -0
- package/dist/components/search/CriteriaChips.d.ts +6 -1
- package/dist/components/search/CriteriaChips.js +71 -20
- package/dist/components/search/SearchBar.d.ts +4 -1
- package/dist/components/search/SearchBar.js +15 -7
- package/dist/components/timeline/RecordTimeline.d.ts +60 -0
- package/dist/components/timeline/RecordTimeline.js +214 -0
- package/dist/components/timeline/TimelineToolbar.d.ts +30 -0
- package/dist/components/timeline/TimelineToolbar.js +31 -0
- package/dist/components/timeline/index.d.ts +3 -0
- package/dist/components/timeline/index.js +3 -0
- package/dist/components/timeline/time.d.ts +43 -0
- package/dist/components/timeline/time.js +135 -0
- package/dist/foundations/copy.d.ts +106 -14
- package/dist/foundations/copy.es.js +83 -9
- package/dist/foundations/copy.js +83 -9
- package/dist/foundations/index.d.ts +1 -1
- package/dist/foundations/match.d.ts +23 -0
- package/dist/foundations/match.js +63 -0
- package/dist/foundations/slots.d.ts +1 -1
- package/dist/foundations/slots.js +8 -3
- package/dist/foundations/types.d.ts +7 -0
- package/dist/foundations/types.js +0 -36
- package/dist/primitives/block/index.d.ts +1 -1
- package/dist/primitives/block/index.js +1 -1
- package/dist/primitives/cell/CellStack.d.ts +7 -1
- package/dist/primitives/cell/CellStack.js +5 -2
- package/dist/primitives/choice/ComboBox.js +8 -4
- package/dist/primitives/choice/KanbanState.d.ts +14 -0
- package/dist/primitives/choice/KanbanState.js +37 -0
- package/dist/primitives/choice/Priority.d.ts +20 -0
- package/dist/primitives/choice/Priority.js +41 -0
- package/dist/primitives/choice/Slider.d.ts +6 -1
- package/dist/primitives/choice/Slider.js +2 -2
- package/dist/primitives/choice/index.d.ts +2 -0
- package/dist/primitives/choice/index.js +2 -0
- package/dist/primitives/dialog/Dialog.d.ts +9 -3
- package/dist/primitives/dialog/Dialog.js +41 -5
- package/dist/primitives/dialog/index.d.ts +0 -1
- package/dist/primitives/dialog/index.js +0 -2
- package/dist/primitives/notice/index.d.ts +1 -1
- package/dist/primitives/notice/index.js +1 -1
- package/dist/primitives/overlay/Preview.js +13 -4
- package/dist/primitives/picker/MatchedText.d.ts +10 -0
- package/dist/primitives/picker/MatchedText.js +12 -0
- package/dist/primitives/picker/Picker.d.ts +29 -7
- package/dist/primitives/picker/Picker.js +88 -23
- package/dist/primitives/table/EditableTable.d.ts +6 -1
- package/dist/primitives/table/EditableTable.js +70 -9
- package/dist/primitives/table/ReportTable.js +2 -1
- package/dist/primitives/table/RowActions.js +7 -2
- package/dist/primitives/table/Table.d.ts +5 -0
- package/dist/primitives/table/Table.js +17 -8
- package/dist/primitives/tag/Tags.d.ts +2 -0
- package/dist/primitives/tag/Tags.js +2 -1
- package/dist/primitives/tag/TagsField.d.ts +10 -0
- package/dist/primitives/tag/TagsField.js +34 -9
- package/dist/primitives/tag/choices.d.ts +2 -0
- package/dist/primitives/value/Avatar.d.ts +16 -0
- package/dist/primitives/value/Avatar.js +91 -0
- package/dist/primitives/value/Person.d.ts +6 -51
- package/dist/primitives/value/Person.js +9 -148
- package/dist/primitives/value/Relation.d.ts +21 -9
- package/dist/primitives/value/Relation.js +35 -24
- package/dist/primitives/value/contact.d.ts +30 -0
- package/dist/primitives/value/contact.js +51 -0
- package/dist/primitives/value/index.d.ts +1 -1
- package/dist/primitives/value/index.js +1 -1
- package/dist/tokens.css +136 -1
- package/dist/views/list/List.d.ts +25 -2
- package/dist/views/list/List.js +48 -8
- package/dist/views/list/ListView.d.ts +1 -1
- package/dist/views/list/ListView.js +39 -8
- package/dist/views/list/columns.js +8 -1
- package/dist/views/list/presentations.d.ts +5 -2
- package/dist/views/list/presentations.js +32 -2
- package/dist/views/list/types.d.ts +44 -0
- package/dist/views/list/useNarrowList.d.ts +10 -0
- package/dist/views/list/useNarrowList.js +20 -6
- package/dist/views/record/Record.d.ts +14 -0
- package/dist/views/record/Record.js +22 -7
- package/package.json +11 -4
- package/dist/components/navigation/CommandPalette.d.ts +0 -40
- package/dist/components/navigation/CommandPalette.js +0 -114
- package/dist/components/navigation/commands.d.ts +0 -30
- package/dist/components/navigation/commands.js +0 -30
- package/dist/primitives/dialog/Drill.d.ts +0 -90
- package/dist/primitives/dialog/Drill.js +0 -172
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { commandMatches, groupCommands, } from "./commands.js";
|
|
4
|
-
import { AriaLocale } from "../../foundations/aria.js";
|
|
5
|
-
import { useUiCopy } from "../../foundations/copy.js";
|
|
6
|
-
import { tv } from "../../foundations/variants.js";
|
|
7
|
-
import { Search, X } from "lucide-react";
|
|
8
|
-
import { useEffect, useMemo } from "react";
|
|
9
|
-
import { Autocomplete, Button as AriaButton, Dialog, Header, Input, Keyboard, Menu, MenuItem, MenuSection, Modal, ModalOverlay, SearchField, useFilter, } from "react-aria-components";
|
|
10
|
-
/* ==================================================================== *
|
|
11
|
-
* COMMAND PALETTE — ⌘K, type three letters, Enter.
|
|
12
|
-
*
|
|
13
|
-
* An ERP has a hundred screens and twenty «new» buttons, and the people who
|
|
14
|
-
* use it all day stop wanting to look for any of them: they know the name of
|
|
15
|
-
* what they want. The palette is the shortest path from knowing the name to
|
|
16
|
-
* being there — one key to open, a few letters, Enter — without leaving the
|
|
17
|
-
* keyboard and without knowing in which menu of which module it lives.
|
|
18
|
-
*
|
|
19
|
-
* ┌──────────────────────────────────────────────┐
|
|
20
|
-
* │ 🔍 fact ✕ │
|
|
21
|
-
* ├──────────────────────────────────────────────┤
|
|
22
|
-
* │ IR A… │
|
|
23
|
-
* │ ▌🧾 Facturas │ ← Enter runs it
|
|
24
|
-
* │ CREAR… │
|
|
25
|
-
* │ + Nueva factura ⌘N │
|
|
26
|
-
* └──────────────────────────────────────────────┘
|
|
27
|
-
*
|
|
28
|
-
* React Aria's `Autocomplete` drives it: focus stays in the search box while
|
|
29
|
-
* the arrows walk the list (virtual focus), the first match is highlighted as
|
|
30
|
-
* you type so Enter always does something, and the modal traps focus, closes
|
|
31
|
-
* on Escape and on a click outside, and gives the focus back.
|
|
32
|
-
*
|
|
33
|
-
* The match is the locale's (`useFilter` inside `AriaLocale`): «facturacion»
|
|
34
|
-
* finds «Facturación» because nobody types the accent when they are in a hurry.
|
|
35
|
-
* ==================================================================== */
|
|
36
|
-
const overlay = tv({
|
|
37
|
-
base: "fixed inset-0 z-50 flex items-start justify-center overflow-y-auto bg-scrim/40 px-4 pt-24 transition-opacity duration-(--duration-quick) ease-(--ease-out)",
|
|
38
|
-
variants: {
|
|
39
|
-
isEntering: { true: "opacity-0" },
|
|
40
|
-
isExiting: { true: "opacity-0" },
|
|
41
|
-
},
|
|
42
|
-
});
|
|
43
|
-
const surface = tv({
|
|
44
|
-
base: "w-full max-w-xl overflow-hidden rounded-panel border border-line-strong bg-surface text-ink shadow-window outline-0 transition-[opacity,translate] duration-(--duration-quick) ease-(--ease-out) forced-colors:bg-[Canvas]",
|
|
45
|
-
variants: {
|
|
46
|
-
isEntering: { true: "-translate-y-1 opacity-0" },
|
|
47
|
-
isExiting: { true: "opacity-0" },
|
|
48
|
-
},
|
|
49
|
-
});
|
|
50
|
-
const item = tv({
|
|
51
|
-
base: "flex cursor-default items-center gap-2 rounded-control px-2 py-1.5 text-meta text-ink outline-0 select-none",
|
|
52
|
-
variants: {
|
|
53
|
-
isFocused: {
|
|
54
|
-
true: "bg-primary/8 forced-colors:bg-[Highlight] forced-colors:text-[HighlightText]",
|
|
55
|
-
},
|
|
56
|
-
isPressed: { true: "bg-primary/12" },
|
|
57
|
-
isDisabled: { true: "text-muted forced-colors:text-[GrayText]" },
|
|
58
|
-
},
|
|
59
|
-
});
|
|
60
|
-
/**
|
|
61
|
-
* The search box and the list, without the modal around them.
|
|
62
|
-
*
|
|
63
|
-
* Context: split out so the filtering can be rendered on its own — in a test, with a query
|
|
64
|
-
* already typed — since the modal is a portal that only exists in a browser.
|
|
65
|
-
*/
|
|
66
|
-
export function CommandSearch({ commands, onRun, query, onQueryChange, placeholder, }) {
|
|
67
|
-
const copy = useUiCopy();
|
|
68
|
-
const { contains } = useFilter({ sensitivity: "base" });
|
|
69
|
-
const byId = useMemo(() => new Map(commands.map((c) => [c.id, c])), [commands]);
|
|
70
|
-
const groups = useMemo(() => groupCommands(commands), [commands]);
|
|
71
|
-
const hint = placeholder ?? copy.search.placeholder;
|
|
72
|
-
return (_jsxs(Autocomplete, { ...(query === undefined ? {} : { inputValue: query }), ...(onQueryChange === undefined ? {} : { onInputChange: onQueryChange }), filter: (_text, input, node) => {
|
|
73
|
-
const command = byId.get(String(node.key));
|
|
74
|
-
return command === undefined || commandMatches(command, input, contains);
|
|
75
|
-
}, children: [_jsxs(SearchField, { "aria-label": hint, autoFocus: true, className: "group flex items-center gap-2 border-b border-line px-3", children: [_jsx(Search, { className: "size-(--size-icon-md) shrink-0 text-muted", strokeWidth: 2, "aria-hidden": true }), _jsx(Input, { placeholder: hint, className: "h-(--size-control-lg) min-w-0 flex-1 bg-transparent text-body text-ink outline-0 placeholder:text-muted [&::-webkit-search-cancel-button]:hidden" }), _jsx(AriaButton, { className: "inline-flex shrink-0 items-center justify-center rounded-mark p-1 text-muted transition-colors duration-(--duration-instant) group-empty:invisible hover:bg-ink/6 hover:text-ink pressed:bg-ink/10", children: _jsx(X, { className: "size-(--size-icon-md)", strokeWidth: 2, "aria-hidden": true }) })] }), _jsx(Menu, { "aria-label": hint, onAction: (key) => onRun(String(key)), renderEmptyState: () => (_jsx("p", { className: "px-3 py-6 text-center text-meta text-muted italic", children: copy.menu.empty })), className: "max-h-96 overflow-y-auto p-1 outline-0", children: groups.map((group) => (_jsxs(MenuSection, { id: group.section ?? "-", className: "not-first:mt-1", children: [group.section === undefined ? null : (_jsx(Header, { className: "px-2 pt-2 pb-1 text-micro font-medium text-muted", children: group.section })), group.commands.map((command) => {
|
|
76
|
-
const Icon = command.icon;
|
|
77
|
-
return (_jsxs(MenuItem, { id: command.id, textValue: command.label, className: item, children: [_jsx("span", { className: "flex size-(--size-icon-md) shrink-0 items-center justify-center text-muted", children: Icon === undefined ? null : (_jsx(Icon, { className: "size-(--size-icon-md)", strokeWidth: 2, "aria-hidden": true })) }), _jsx("span", { className: "min-w-0 flex-1 truncate", children: command.label }), command.shortcut === undefined ? null : (_jsx(Keyboard, { className: "shrink-0 rounded-mark border border-line px-1.5 font-sans text-micro text-muted tabular-nums", children: command.shortcut }))] }, command.id));
|
|
78
|
-
})] }, group.section ?? ""))) })] }));
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* A modal search over everything the app can do — go to a screen, create a record — opened
|
|
82
|
-
* with ⌘K (Ctrl+K) and run with Enter.
|
|
83
|
-
*
|
|
84
|
-
* Context: it is controlled (`open`, `onOpenChange`) and does not bind the shortcut itself,
|
|
85
|
-
* because an app has ONE palette and many screens: the key is bound once, where the palette
|
|
86
|
-
* lives, with `useCommandPaletteShortcut`. Running a command closes it — the palette is a
|
|
87
|
-
* path, not a place to stay.
|
|
88
|
-
*/
|
|
89
|
-
export function CommandPalette({ open, onOpenChange, commands, onRun, placeholder, }) {
|
|
90
|
-
const copy = useUiCopy();
|
|
91
|
-
return (_jsx(AriaLocale, { children: _jsx(ModalOverlay, { isOpen: open, onOpenChange: onOpenChange, isDismissable: true, className: overlay, children: _jsx(Modal, { className: surface, children: _jsx(Dialog, { "aria-label": placeholder ?? copy.search.placeholder, className: "outline-0", children: _jsx(CommandSearch, { commands: commands, onRun: (id) => {
|
|
92
|
-
onOpenChange(false);
|
|
93
|
-
onRun(id);
|
|
94
|
-
}, ...(placeholder === undefined ? {} : { placeholder }) }) }) }) }) }));
|
|
95
|
-
}
|
|
96
|
-
/**
|
|
97
|
-
* Binds ⌘K (Ctrl+K on Windows and Linux) to open the palette, anywhere in the page.
|
|
98
|
-
*
|
|
99
|
-
* Context: both modifiers are accepted on every platform. Telling them apart by the user
|
|
100
|
-
* agent guesses wrong on a Mac with a PC keyboard, and the cost of accepting both is that
|
|
101
|
-
* Ctrl+K on a Mac — «delete to the end of the line» in a text box — opens the palette.
|
|
102
|
-
*/
|
|
103
|
-
export function useCommandPaletteShortcut(setOpen) {
|
|
104
|
-
useEffect(() => {
|
|
105
|
-
const onKey = (e) => {
|
|
106
|
-
if (e.key.toLowerCase() !== "k" || !(e.metaKey || e.ctrlKey) || e.altKey)
|
|
107
|
-
return;
|
|
108
|
-
e.preventDefault();
|
|
109
|
-
setOpen(true);
|
|
110
|
-
};
|
|
111
|
-
document.addEventListener("keydown", onKey);
|
|
112
|
-
return () => document.removeEventListener("keydown", onKey);
|
|
113
|
-
}, [setOpen]);
|
|
114
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import type { LucideIcon } from "lucide-react";
|
|
2
|
-
/** Something the palette can run: go to a screen, create a record, open a report. */
|
|
3
|
-
export interface PaletteCommand {
|
|
4
|
-
id: string;
|
|
5
|
-
label: string;
|
|
6
|
-
/** The heading it is listed under: «Ir a…», «Crear…». Commands without one go first,
|
|
7
|
-
* without a heading. */
|
|
8
|
-
section?: string;
|
|
9
|
-
icon?: LucideIcon;
|
|
10
|
-
/** The key that runs it without the palette, as it should be read: «⌘N», «Alt+F». It is
|
|
11
|
-
* only drawn — the palette is where the shortcut gets learned, not where it is bound. */
|
|
12
|
-
shortcut?: string;
|
|
13
|
-
/** Other words that should find it: «factura» finds «Nueva venta» when the people who use
|
|
14
|
-
* it call it that. */
|
|
15
|
-
keywords?: readonly string[];
|
|
16
|
-
}
|
|
17
|
-
/** The commands under each heading, headings in the order they first appear. */
|
|
18
|
-
export declare function groupCommands(commands: readonly PaletteCommand[]): {
|
|
19
|
-
section: string | undefined;
|
|
20
|
-
commands: PaletteCommand[];
|
|
21
|
-
}[];
|
|
22
|
-
/**
|
|
23
|
-
* Whether a command answers a query. `contains` is the locale's comparison — React Aria's
|
|
24
|
-
* `useFilter` in the palette — so «facturacion» finds «Facturación» and an uppercase query
|
|
25
|
-
* finds a lowercase label.
|
|
26
|
-
*
|
|
27
|
-
* Every word of the query has to appear, in any order, in the label or the keywords:
|
|
28
|
-
* «nueva fac» finds «Nueva factura», and so does «factura nueva».
|
|
29
|
-
*/
|
|
30
|
-
export declare function commandMatches(command: PaletteCommand, query: string, contains: (text: string, part: string) => boolean): boolean;
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
/** The commands under each heading, headings in the order they first appear. */
|
|
2
|
-
export function groupCommands(commands) {
|
|
3
|
-
const groups = new Map();
|
|
4
|
-
/* The commands with no heading are listed first, wherever they were written. */
|
|
5
|
-
if (commands.some((c) => c.section === undefined))
|
|
6
|
-
groups.set(undefined, []);
|
|
7
|
-
for (const command of commands) {
|
|
8
|
-
const group = groups.get(command.section);
|
|
9
|
-
if (group === undefined)
|
|
10
|
-
groups.set(command.section, [command]);
|
|
11
|
-
else
|
|
12
|
-
group.push(command);
|
|
13
|
-
}
|
|
14
|
-
return [...groups].map(([section, list]) => ({ section, commands: list }));
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Whether a command answers a query. `contains` is the locale's comparison — React Aria's
|
|
18
|
-
* `useFilter` in the palette — so «facturacion» finds «Facturación» and an uppercase query
|
|
19
|
-
* finds a lowercase label.
|
|
20
|
-
*
|
|
21
|
-
* Every word of the query has to appear, in any order, in the label or the keywords:
|
|
22
|
-
* «nueva fac» finds «Nueva factura», and so does «factura nueva».
|
|
23
|
-
*/
|
|
24
|
-
export function commandMatches(command, query, contains) {
|
|
25
|
-
const words = query.trim().split(/\s+/).filter(Boolean);
|
|
26
|
-
if (words.length === 0)
|
|
27
|
-
return true;
|
|
28
|
-
const haystack = [command.label, ...(command.keywords ?? [])].join(" ");
|
|
29
|
-
return words.every((word) => contains(haystack, word));
|
|
30
|
-
}
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import { type DialogContext, type DialogWidth } from "./Dialog.js";
|
|
2
|
-
import { type ReactNode, type RefObject } from "react";
|
|
3
|
-
/** Where the drill was opened from: the page's record. The first crumb of the trail. */
|
|
4
|
-
export type DrillOrigin = DialogContext;
|
|
5
|
-
/** Which contexts are open, in order, and the ways to move between them. */
|
|
6
|
-
export interface DrillState {
|
|
7
|
-
/** The ids of the open contexts, from the first opened to the one in front. */
|
|
8
|
-
readonly path: readonly string[];
|
|
9
|
-
/** One level deeper. An id already on the path goes back to it instead: a product
|
|
10
|
-
* that leads to its supplier that leads to the same product is a loop, not depth. */
|
|
11
|
-
open: (id: string) => void;
|
|
12
|
-
/** Up one level. On the first one, it closes. */
|
|
13
|
-
back: () => void;
|
|
14
|
-
/** Back to the context with this id, closing the ones in front of it. */
|
|
15
|
-
to: (id: string) => void;
|
|
16
|
-
/** Close every level: back to the page. */
|
|
17
|
-
close: () => void;
|
|
18
|
-
/** Internal: the control that opened each level, where focus returns when the level
|
|
19
|
-
* above it closes. Written by `open`, read by `Drill`. */
|
|
20
|
-
readonly focusMarks: RefObject<(HTMLElement | null)[]>;
|
|
21
|
-
}
|
|
22
|
-
/** One level deeper: push the id — or, when it is already open, go back to it. Internal,
|
|
23
|
-
* exported for the test. */
|
|
24
|
-
export declare function openPath(path: readonly string[], id: string): readonly string[];
|
|
25
|
-
/** Back to the context with this id; an id not on the path changes nothing. */
|
|
26
|
-
export declare function toPath(path: readonly string[], id: string): readonly string[];
|
|
27
|
-
/** The written contexts that are open, in the order of the path. A context on the path
|
|
28
|
-
* that is not written is skipped: there is nothing to draw for it. */
|
|
29
|
-
export declare function framesOf(children: ReactNode, path: readonly string[]): DrillContextProps[];
|
|
30
|
-
/** The state of a `Drill`: the path of open contexts. Pass it to `<Drill state={…}>` and
|
|
31
|
-
* call `open(id)` from wherever the context is opened — a relation's «Abrir», a
|
|
32
|
-
* `Picker`'s open button, a rail action, a line's cell. */
|
|
33
|
-
export declare function useDrill(initial?: readonly string[]): DrillState;
|
|
34
|
-
export interface DrillContextProps {
|
|
35
|
-
/** What `open(id)` names. Unique inside the drill. */
|
|
36
|
-
id: string;
|
|
37
|
-
/** The context's name: the title while it is in front, its crumb when it is behind.
|
|
38
|
-
* Short — «Línea 3», «Impresora térmica 80 mm», «Registrar pago». */
|
|
39
|
-
title: string;
|
|
40
|
-
/** One sentence under the title; also the dialog's description while in front. */
|
|
41
|
-
subtitle?: string;
|
|
42
|
-
/** This context's actions — Cancel and ONE primary. Drawn only while it is in front. */
|
|
43
|
-
footer?: ReactNode;
|
|
44
|
-
/** No padding of its own: for a child that is already a surface with its own margins
|
|
45
|
-
* (a `Record`). A form or a wizard keeps the padding. */
|
|
46
|
-
flush?: boolean;
|
|
47
|
-
children: ReactNode;
|
|
48
|
-
}
|
|
49
|
-
export interface DrillProps {
|
|
50
|
-
state: DrillState;
|
|
51
|
-
/** The page's record, as the first crumb: «pedido · S00142». Pressing it closes. */
|
|
52
|
-
origin?: DrillOrigin;
|
|
53
|
-
/** One width for every level: a window that changes width at each step jumps under the
|
|
54
|
-
* pointer. `lg` by default — a record with pages and lines needs it. */
|
|
55
|
-
width?: DialogWidth;
|
|
56
|
-
/** The `Drill.Context`s. Which ones are open is the state's path. */
|
|
57
|
-
children: ReactNode;
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* The inside of the window: the trail, the context in front (and the ones behind it,
|
|
61
|
-
* hidden), and the footer of the one in front.
|
|
62
|
-
*
|
|
63
|
-
* Internal — exported only so a test can read its markup, which the modal (a portal)
|
|
64
|
-
* does not produce on the server.
|
|
65
|
-
*/
|
|
66
|
-
export declare function DrillSurface({ frames, origin, onBack, onTo, onClose, focusMarks, }: {
|
|
67
|
-
frames: readonly DrillContextProps[];
|
|
68
|
-
origin?: DrillOrigin;
|
|
69
|
-
onBack: () => void;
|
|
70
|
-
onTo: (id: string) => void;
|
|
71
|
-
onClose: () => void;
|
|
72
|
-
focusMarks?: RefObject<(HTMLElement | null)[]>;
|
|
73
|
-
}): import("react").JSX.Element | null;
|
|
74
|
-
/**
|
|
75
|
-
* ONE dialog that holds a stack of contexts opened from a record — and from each other —
|
|
76
|
-
* with a trail back to the page. See the header of this file.
|
|
77
|
-
*
|
|
78
|
-
* const drill = useDrill();
|
|
79
|
-
* …<RelationValue name={product} onOpen={() => drill.open("product")} />…
|
|
80
|
-
* <Drill state={drill} origin={{ kind: "pedido", label: "S00142" }}>
|
|
81
|
-
* <Drill.Context id="product" title={product} flush>
|
|
82
|
-
* <Record identity={…}>…</Record>
|
|
83
|
-
* </Drill.Context>
|
|
84
|
-
* <Drill.Context id="supplier" title={supplier} footer={…}>…</Drill.Context>
|
|
85
|
-
* </Drill>
|
|
86
|
-
*/
|
|
87
|
-
export declare function Drill({ state, origin, width, children }: DrillProps): import("react").JSX.Element;
|
|
88
|
-
export declare namespace Drill {
|
|
89
|
-
var Context: ((props: DrillContextProps) => ReactNode) & import("../../foundations/slots.js").Slotted<"context">;
|
|
90
|
-
}
|
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
-
import { fillCopy, useUiCopy } from "../../foundations/copy.js";
|
|
4
|
-
import { flatten, propsOf, slot, slotOf } from "../../foundations/slots.js";
|
|
5
|
-
import { cn, focusRing, tv } from "../../foundations/variants.js";
|
|
6
|
-
import { IconButton } from "../button/Button.js";
|
|
7
|
-
import { dialogStyles, } from "./Dialog.js";
|
|
8
|
-
import { ArrowLeft, ChevronRight, X } from "lucide-react";
|
|
9
|
-
import { useEffect, useId, useRef, useState } from "react";
|
|
10
|
-
import { useKeyboard } from "react-aria";
|
|
11
|
-
import { Dialog as AriaDialog, Heading, Link, Modal, ModalOverlay, } from "react-aria-components";
|
|
12
|
-
function focused() {
|
|
13
|
-
if (typeof document === "undefined")
|
|
14
|
-
return null;
|
|
15
|
-
return document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
16
|
-
}
|
|
17
|
-
/** One level deeper: push the id — or, when it is already open, go back to it. Internal,
|
|
18
|
-
* exported for the test. */
|
|
19
|
-
export function openPath(path, id) {
|
|
20
|
-
const at = path.indexOf(id);
|
|
21
|
-
return at === -1 ? [...path, id] : path.slice(0, at + 1);
|
|
22
|
-
}
|
|
23
|
-
/** Back to the context with this id; an id not on the path changes nothing. */
|
|
24
|
-
export function toPath(path, id) {
|
|
25
|
-
const at = path.indexOf(id);
|
|
26
|
-
return at === -1 ? path : path.slice(0, at + 1);
|
|
27
|
-
}
|
|
28
|
-
/** The written contexts that are open, in the order of the path. A context on the path
|
|
29
|
-
* that is not written is skipped: there is nothing to draw for it. */
|
|
30
|
-
export function framesOf(children, path) {
|
|
31
|
-
const written = flatten(children)
|
|
32
|
-
.filter((node) => slotOf(node) === "context")
|
|
33
|
-
.map((node) => propsOf(node))
|
|
34
|
-
.filter((props) => props !== undefined);
|
|
35
|
-
return path
|
|
36
|
-
.map((id) => written.find((context) => context.id === id))
|
|
37
|
-
.filter((frame) => frame !== undefined);
|
|
38
|
-
}
|
|
39
|
-
/** The state of a `Drill`: the path of open contexts. Pass it to `<Drill state={…}>` and
|
|
40
|
-
* call `open(id)` from wherever the context is opened — a relation's «Abrir», a
|
|
41
|
-
* `Picker`'s open button, a rail action, a line's cell. */
|
|
42
|
-
export function useDrill(initial = []) {
|
|
43
|
-
const [path, setPath] = useState(initial);
|
|
44
|
-
const focusMarks = useRef([]);
|
|
45
|
-
return {
|
|
46
|
-
path,
|
|
47
|
-
open: (id) => {
|
|
48
|
-
if (!path.includes(id))
|
|
49
|
-
focusMarks.current[path.length] = focused();
|
|
50
|
-
setPath(openPath(path, id));
|
|
51
|
-
},
|
|
52
|
-
back: () => setPath(path.slice(0, -1)),
|
|
53
|
-
to: (id) => setPath(toPath(path, id)),
|
|
54
|
-
close: () => setPath([]),
|
|
55
|
-
focusMarks,
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
const crumb = tv({
|
|
59
|
-
extend: focusRing,
|
|
60
|
-
base: "block max-w-(--container-crumb) min-w-0 cursor-pointer truncate rounded-mark text-muted transition-colors duration-(--duration-instant) hover:text-ink hover:underline hover:underline-offset-4",
|
|
61
|
-
});
|
|
62
|
-
/**
|
|
63
|
-
* The inside of the window: the trail, the context in front (and the ones behind it,
|
|
64
|
-
* hidden), and the footer of the one in front.
|
|
65
|
-
*
|
|
66
|
-
* Internal — exported only so a test can read its markup, which the modal (a portal)
|
|
67
|
-
* does not produce on the server.
|
|
68
|
-
*/
|
|
69
|
-
export function DrillSurface({ frames, origin, onBack, onTo, onClose, focusMarks, }) {
|
|
70
|
-
const copy = useUiCopy();
|
|
71
|
-
const depth = frames.length;
|
|
72
|
-
const top = frames[depth - 1];
|
|
73
|
-
const parent = depth > 1 ? frames[depth - 2] : undefined;
|
|
74
|
-
const bodies = useRef(new Map());
|
|
75
|
-
const was = useRef(depth);
|
|
76
|
-
const subtitleId = useId();
|
|
77
|
-
const { keyboardProps } = useKeyboard({
|
|
78
|
-
onKeyDown: (e) => {
|
|
79
|
-
if (e.key !== "Escape") {
|
|
80
|
-
e.continuePropagation();
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
if (depth > 1)
|
|
84
|
-
onBack();
|
|
85
|
-
else
|
|
86
|
-
onClose();
|
|
87
|
-
},
|
|
88
|
-
});
|
|
89
|
-
/* Focus follows the level. Down: into the new context, so the keyboard does not stay on
|
|
90
|
-
a control that is now hidden. Up: back to what opened the level that closed. */
|
|
91
|
-
const topId = top?.id;
|
|
92
|
-
useEffect(() => {
|
|
93
|
-
const before = was.current;
|
|
94
|
-
was.current = depth;
|
|
95
|
-
if (before === depth || depth === 0 || topId === undefined)
|
|
96
|
-
return;
|
|
97
|
-
const box = bodies.current.get(topId);
|
|
98
|
-
if (depth > before) {
|
|
99
|
-
box?.focus();
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
const mark = focusMarks?.current[depth];
|
|
103
|
-
(mark?.isConnected === true ? mark : box)?.focus();
|
|
104
|
-
}, [depth, topId, focusMarks]);
|
|
105
|
-
if (top === undefined)
|
|
106
|
-
return null;
|
|
107
|
-
const trail = [
|
|
108
|
-
...(origin === undefined
|
|
109
|
-
? []
|
|
110
|
-
: [
|
|
111
|
-
{
|
|
112
|
-
key: "\u0000origin",
|
|
113
|
-
label: (_jsxs(_Fragment, { children: [origin.kind === undefined ? null : (_jsxs("span", { className: "font-medium", children: [origin.kind, " "] })), _jsx("span", { className: "text-ink", children: origin.label })] })),
|
|
114
|
-
onPress: onClose,
|
|
115
|
-
},
|
|
116
|
-
]),
|
|
117
|
-
...frames.slice(0, -1).map((frame) => ({
|
|
118
|
-
key: frame.id,
|
|
119
|
-
label: frame.title,
|
|
120
|
-
onPress: () => onTo(frame.id),
|
|
121
|
-
})),
|
|
122
|
-
];
|
|
123
|
-
return (_jsx(AriaDialog, { ...(top.subtitle === undefined ? {} : { "aria-describedby": subtitleId }), className: "flex max-h-[inherit] min-h-0 flex-1 flex-col outline-0", children: _jsxs("div", { ...keyboardProps, className: "flex max-h-[inherit] min-h-0 flex-1 flex-col", children: [_jsxs("header", { className: "flex shrink-0 items-start gap-2 border-b border-line px-4 py-3", children: [parent === undefined ? null : (_jsx("span", { className: "-ml-1.5 shrink-0", children: _jsx(IconButton, { icon: ArrowLeft, label: fillCopy(copy.drill.back, { name: parent.title }), onClick: onBack }) })), _jsxs("div", { className: "min-w-0 flex-1", children: [trail.length === 0 ? null : (
|
|
124
|
-
/* Where this was opened from, small and on top — the same place `Dialog`
|
|
125
|
-
says what it is about. Every crumb is a way back; the one in front is
|
|
126
|
-
not a crumb, it is the title. */
|
|
127
|
-
_jsx("nav", { "aria-label": copy.drill.trail, className: "mb-0.5 flex min-w-0", children: _jsx("ol", { className: "flex min-w-0 flex-wrap items-center gap-x-1 gap-y-0.5 text-micro", children: trail.map((one, i) => (_jsxs("li", { className: "flex min-w-0 items-center gap-1", children: [i === 0 ? null : (_jsx(ChevronRight, { className: "size-(--size-icon-sm) shrink-0 text-line-strong", strokeWidth: 2, "aria-hidden": true })), _jsx(Link, { onPress: one.onPress, className: ({ isFocusVisible }) => crumb({ isFocusVisible }), children: one.label })] }, one.key))) }) })), _jsx(Heading, { slot: "title", className: "truncate text-title font-semibold text-ink", children: top.title }), top.subtitle === undefined ? null : (_jsx("p", { id: subtitleId, className: "mt-1 max-w-prose text-meta text-muted", children: top.subtitle }))] }), _jsx(IconButton, { icon: X, label: copy.shell.close, onClick: onClose })] }), frames.map((frame) => {
|
|
128
|
-
const front = frame.id === top.id;
|
|
129
|
-
return (
|
|
130
|
-
/* Each level scrolls on its own and the ones behind are `hidden`, not gone:
|
|
131
|
-
their state — a half-typed payment, the open tab of a product — is still
|
|
132
|
-
there when the level above closes. */
|
|
133
|
-
_jsx("div", { ref: (node) => {
|
|
134
|
-
if (node === null)
|
|
135
|
-
bodies.current.delete(frame.id);
|
|
136
|
-
else
|
|
137
|
-
bodies.current.set(frame.id, node);
|
|
138
|
-
}, hidden: !front, tabIndex: -1, "data-drill-context": frame.id, className: cn("min-h-0 flex-1 overflow-y-auto outline-0", frame.flush !== true && "px-4 py-4", front && frame.footer === undefined && "max-sm:safe-bottom"), children: frame.children }, frame.id));
|
|
139
|
-
}), top.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: top.footer }))] }) }));
|
|
140
|
-
}
|
|
141
|
-
/** One context of a drill: a title, what goes inside, and its own footer. It does not
|
|
142
|
-
* draw where it is written — the `Drill` draws it when its id is on the path. */
|
|
143
|
-
const DrillContextSlot = slot("context", (_props) => null);
|
|
144
|
-
/**
|
|
145
|
-
* ONE dialog that holds a stack of contexts opened from a record — and from each other —
|
|
146
|
-
* with a trail back to the page. See the header of this file.
|
|
147
|
-
*
|
|
148
|
-
* const drill = useDrill();
|
|
149
|
-
* …<RelationValue name={product} onOpen={() => drill.open("product")} />…
|
|
150
|
-
* <Drill state={drill} origin={{ kind: "pedido", label: "S00142" }}>
|
|
151
|
-
* <Drill.Context id="product" title={product} flush>
|
|
152
|
-
* <Record identity={…}>…</Record>
|
|
153
|
-
* </Drill.Context>
|
|
154
|
-
* <Drill.Context id="supplier" title={supplier} footer={…}>…</Drill.Context>
|
|
155
|
-
* </Drill>
|
|
156
|
-
*/
|
|
157
|
-
export function Drill({ state, origin, width = "lg", children }) {
|
|
158
|
-
/* While the window fades out the path is already empty: it keeps drawing the last levels
|
|
159
|
-
it had, so the content does not vanish before the window does. */
|
|
160
|
-
const [lastPath, setLastPath] = useState(state.path);
|
|
161
|
-
if (state.path.length > 0 && state.path !== lastPath)
|
|
162
|
-
setLastPath(state.path);
|
|
163
|
-
const path = state.path.length > 0 ? state.path : lastPath;
|
|
164
|
-
const frames = framesOf(children, path);
|
|
165
|
-
return (_jsx(ModalOverlay, { isOpen: state.path.length > 0, onOpenChange: (next) => {
|
|
166
|
-
if (!next)
|
|
167
|
-
state.close();
|
|
168
|
-
}, isDismissable: true,
|
|
169
|
-
/* Escape is ours: up one level, not out of everything. See the header. */
|
|
170
|
-
isKeyboardDismissDisabled: true, className: dialogStyles.overlay, children: _jsx(Modal, { className: (s) => dialogStyles.modal({ ...s, width }), children: _jsx(DrillSurface, { frames: frames, ...(origin === undefined ? {} : { origin }), onBack: state.back, onTo: state.to, onClose: state.close, focusMarks: state.focusMarks }) }) }));
|
|
171
|
-
}
|
|
172
|
-
Drill.Context = DrillContextSlot;
|