@sincpro/web-ui 0.2.0 → 1.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/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 +15 -1
- package/dist/components/search/SearchBar.js +34 -16
- package/dist/components/search/query.d.ts +14 -11
- package/dist/components/search/query.js +36 -3
- 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 +110 -14
- package/dist/foundations/copy.es.js +85 -9
- package/dist/foundations/copy.js +85 -9
- package/dist/foundations/grouping.d.ts +3 -0
- 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 +42 -7
- package/dist/primitives/picker/Picker.js +107 -29
- 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 +17 -0
- package/dist/primitives/tag/TagsField.js +46 -11
- package/dist/primitives/tag/choices.d.ts +6 -1
- package/dist/primitives/tag/choices.js +6 -2
- 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 +40 -9
- 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
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { fillCopy, useUiCopy } from "../../foundations/copy.js";
|
|
4
|
+
import { IconButton } from "../../primitives/button/Button.js";
|
|
5
|
+
import { StatusDot } from "../../primitives/tag/Tag.js";
|
|
6
|
+
import { ChevronsLeftRight, Plus } from "lucide-react";
|
|
7
|
+
import { useState } from "react";
|
|
8
|
+
/* The tones a progress bar draws, in the order they are read: what is wrong first. */
|
|
9
|
+
const PROGRESS_ORDER = ["bad", "warn", "good", "info", "neutral", "accent"];
|
|
10
|
+
const PROGRESS_FILL = {
|
|
11
|
+
bad: "bg-bad",
|
|
12
|
+
warn: "bg-warn",
|
|
13
|
+
good: "bg-good",
|
|
14
|
+
info: "bg-info",
|
|
15
|
+
neutral: "bg-muted",
|
|
16
|
+
accent: "bg-primary",
|
|
17
|
+
};
|
|
18
|
+
/** One-line input of a board: quick create, a new column. Enter keeps it, Escape leaves. */
|
|
19
|
+
function BoardInput({ label, placeholder, onCommit, onClose, }) {
|
|
20
|
+
const [text, setText] = useState("");
|
|
21
|
+
return (_jsx("input", { autoFocus: true, "aria-label": label, placeholder: placeholder, value: text, onChange: (event) => setText(event.target.value), onKeyDown: (event) => {
|
|
22
|
+
if (event.key === "Escape")
|
|
23
|
+
onClose();
|
|
24
|
+
if (event.key === "Enter" && text.trim() !== "") {
|
|
25
|
+
onCommit(text.trim());
|
|
26
|
+
setText("");
|
|
27
|
+
}
|
|
28
|
+
}, onBlur: () => {
|
|
29
|
+
if (text.trim() === "")
|
|
30
|
+
onClose();
|
|
31
|
+
}, className: "h-(--size-control-md) w-full min-w-0 rounded-control border border-primary/60 bg-surface px-2.5 text-meta text-ink outline-none placeholder:text-muted" }));
|
|
32
|
+
}
|
|
33
|
+
/** The column's bar: its records counted by tone, what is wrong first. Said in words too. */
|
|
34
|
+
function ProgressBar({ items, progress, }) {
|
|
35
|
+
const counts = new Map();
|
|
36
|
+
for (const item of items) {
|
|
37
|
+
const tone = progress(item);
|
|
38
|
+
if (tone !== undefined)
|
|
39
|
+
counts.set(tone, (counts.get(tone) ?? 0) + 1);
|
|
40
|
+
}
|
|
41
|
+
const parts = PROGRESS_ORDER.filter((tone) => (counts.get(tone) ?? 0) > 0);
|
|
42
|
+
if (parts.length === 0)
|
|
43
|
+
return null;
|
|
44
|
+
return (_jsx("div", { role: "img", "aria-label": parts.map((tone) => `${tone}: ${counts.get(tone)}`).join(", "), className: "mx-1 flex h-1.5 overflow-hidden rounded-pill bg-ink/8", children: parts.map((tone) => (_jsx("span", { className: PROGRESS_FILL[tone], style: { width: `${((counts.get(tone) ?? 0) / items.length) * 100}%` } }, tone))) }));
|
|
45
|
+
}
|
|
46
|
+
/* ==================================================================== *
|
|
47
|
+
* THE BOARD BY STAGES — Odoo's kanban.
|
|
48
|
+
*
|
|
49
|
+
* Context: a pipeline (quotations → orders → invoiced, leads → won) is read across: how many
|
|
50
|
+
* in each stage, and which is stuck. One column per stage, the count on its head, and a card
|
|
51
|
+
* moved by dragging it to another column — the gesture IS the change of stage.
|
|
52
|
+
*
|
|
53
|
+
* Dragging is not the only way: a focused card moves with Alt + ← / →, and the move is said
|
|
54
|
+
* out loud. A board that only drags is a board a keyboard cannot use.
|
|
55
|
+
* ==================================================================== */
|
|
56
|
+
export function BoardColumns({ items, keyOf, stages, card, }) {
|
|
57
|
+
const copy = useUiCopy();
|
|
58
|
+
const { of, onMove, nameOf, total, progress, onQuickCreate, onAddColumn } = stages;
|
|
59
|
+
const columns = stages.columns ??
|
|
60
|
+
[...new Set(items.map(of))].map((id) => ({ id, label: stages.labelOf?.(id) ?? id }));
|
|
61
|
+
const [folded, setFolded] = useState(() => new Set(stages.defaultFolded ?? []));
|
|
62
|
+
const [creating, setCreating] = useState(null);
|
|
63
|
+
const [adding, setAdding] = useState(false);
|
|
64
|
+
const [dragging, setDragging] = useState(null);
|
|
65
|
+
const [over, setOver] = useState(null);
|
|
66
|
+
const [said, setSaid] = useState("");
|
|
67
|
+
const byId = new Map(items.map((item) => [keyOf(item), item]));
|
|
68
|
+
const move = (id, to) => {
|
|
69
|
+
const item = byId.get(id);
|
|
70
|
+
if (item === undefined || of(item) === to || onMove === undefined)
|
|
71
|
+
return;
|
|
72
|
+
onMove(item, to);
|
|
73
|
+
const column = columns.find((one) => one.id === to)?.label ?? to;
|
|
74
|
+
setSaid(fillCopy(copy.board.moved, { name: nameOf?.(item) ?? id, column }));
|
|
75
|
+
};
|
|
76
|
+
const toggleFold = (id) => setFolded((current) => {
|
|
77
|
+
const next = new Set(current);
|
|
78
|
+
if (next.has(id))
|
|
79
|
+
next.delete(id);
|
|
80
|
+
else
|
|
81
|
+
next.add(id);
|
|
82
|
+
return next;
|
|
83
|
+
});
|
|
84
|
+
const dropOn = (id) => onMove === undefined
|
|
85
|
+
? {}
|
|
86
|
+
: {
|
|
87
|
+
onDragOver: (event) => {
|
|
88
|
+
event.preventDefault();
|
|
89
|
+
setOver(id);
|
|
90
|
+
},
|
|
91
|
+
onDragLeave: () => setOver(null),
|
|
92
|
+
onDrop: (event) => {
|
|
93
|
+
event.preventDefault();
|
|
94
|
+
move(event.dataTransfer.getData("text/plain"), id);
|
|
95
|
+
setOver(null);
|
|
96
|
+
setDragging(null);
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
const byKeyboard = (event, item) => {
|
|
100
|
+
if (!event.altKey || (event.key !== "ArrowLeft" && event.key !== "ArrowRight"))
|
|
101
|
+
return;
|
|
102
|
+
const at = columns.findIndex((one) => one.id === of(item));
|
|
103
|
+
const next = columns[at + (event.key === "ArrowLeft" ? -1 : 1)];
|
|
104
|
+
if (next === undefined)
|
|
105
|
+
return;
|
|
106
|
+
event.preventDefault();
|
|
107
|
+
move(keyOf(item), next.id);
|
|
108
|
+
};
|
|
109
|
+
return (_jsxs("div", { className: "flex min-w-0 items-start gap-3 overflow-x-auto p-3", children: [columns.map((column) => {
|
|
110
|
+
const inside = items.filter((item) => of(item) === column.id);
|
|
111
|
+
if (folded.has(column.id)) {
|
|
112
|
+
/* Folded: a strip with its count and its name down the spine. Still a drop target —
|
|
113
|
+
dropping on «perdido» is how a lost deal is marked. */
|
|
114
|
+
return (_jsxs("section", { "aria-label": column.label, ...dropOn(column.id), className: `flex w-12 shrink-0 flex-col items-center gap-2 rounded-box border py-2 ${over === column.id ? "border-primary/50 bg-primary/6" : "border-line bg-ink/3"}`, children: [_jsx(IconButton, { icon: ChevronsLeftRight, label: fillCopy(copy.board.unfold, { column: column.label }), onClick: () => toggleFold(column.id) }), _jsx("span", { className: "text-micro text-muted tabular-nums", children: inside.length }), _jsx("span", { className: "text-vertical text-meta font-semibold text-ink", children: column.label })] }, column.id));
|
|
115
|
+
}
|
|
116
|
+
return (_jsxs("section", { "aria-label": column.label, ...dropOn(column.id), className: `flex w-(--container-lane) shrink-0 flex-col gap-2 rounded-box border p-2 transition-colors duration-(--duration-instant) ${over === column.id ? "border-primary/50 bg-primary/6" : "border-line bg-ink/3"}`, children: [_jsxs("header", { className: "flex min-w-0 items-center gap-1.5 px-1 py-0.5", children: [_jsx("h3", { className: "min-w-0 flex-1 truncate text-meta font-semibold text-ink", children: column.tone === undefined ? (column.label) : (_jsx(StatusDot, { tone: column.tone, label: column.label })) }), _jsx("span", { className: "shrink-0 text-micro text-muted tabular-nums", children: inside.length }), onQuickCreate === undefined ? null : (_jsx(IconButton, { icon: Plus, label: fillCopy(copy.board.quickCreate, { column: column.label }), onClick: () => setCreating(column.id) })), _jsx(IconButton, { icon: ChevronsLeftRight, label: fillCopy(copy.board.fold, { column: column.label }), onClick: () => toggleFold(column.id) })] }), total === undefined ? null : (_jsx("p", { className: "px-1 text-meta font-semibold text-ink tabular-nums", children: total(inside) })), progress === undefined || inside.length === 0 ? null : (_jsx(ProgressBar, { items: inside, progress: progress })), creating === column.id && onQuickCreate !== undefined ? (_jsx(BoardInput, { label: fillCopy(copy.board.quickCreate, { column: column.label }), placeholder: copy.board.quickPlaceholder, onCommit: (name) => onQuickCreate(column.id, name), onClose: () => setCreating(null) })) : null, inside.length === 0 ? (_jsx("p", { className: "px-1 py-3 text-micro text-muted", children: copy.board.emptyLane })) : (_jsx("ul", { className: "flex min-w-0 flex-col gap-2", children: inside.map((item) => {
|
|
117
|
+
const id = keyOf(item);
|
|
118
|
+
return (_jsx("li", { ...(onMove === undefined
|
|
119
|
+
? {}
|
|
120
|
+
: {
|
|
121
|
+
draggable: true,
|
|
122
|
+
tabIndex: 0,
|
|
123
|
+
title: copy.board.moveHint,
|
|
124
|
+
"aria-keyshortcuts": "Alt+ArrowLeft Alt+ArrowRight",
|
|
125
|
+
onDragStart: (event) => {
|
|
126
|
+
event.dataTransfer.setData("text/plain", id);
|
|
127
|
+
event.dataTransfer.effectAllowed = "move";
|
|
128
|
+
setDragging(id);
|
|
129
|
+
},
|
|
130
|
+
onDragEnd: () => setDragging(null),
|
|
131
|
+
onKeyDown: (event) => byKeyboard(event, item),
|
|
132
|
+
}), className: `flex min-w-0 flex-col rounded-box focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary ${onMove === undefined ? "" : "cursor-grab active:cursor-grabbing"} ${dragging === id ? "opacity-50" : ""}`, children: card(item) }, id));
|
|
133
|
+
}) }))] }, column.id));
|
|
134
|
+
}), onAddColumn === undefined ? null : (_jsx("div", { className: "flex w-(--container-lane) shrink-0 flex-col gap-2 p-2", children: adding ? (_jsx(BoardInput, { label: copy.board.addColumn, placeholder: copy.board.addColumnPlaceholder, onCommit: (label) => {
|
|
135
|
+
onAddColumn(label);
|
|
136
|
+
setAdding(false);
|
|
137
|
+
}, onClose: () => setAdding(false) })) : (_jsxs("button", { type: "button", onClick: () => setAdding(true), className: "flex h-(--size-control-md) items-center gap-1.5 rounded-control px-2 text-meta text-muted transition-colors duration-(--duration-instant) hover:bg-ink/5 hover:text-ink focus-visible:outline-2 focus-visible:outline-primary", children: [_jsx(Plus, { "aria-hidden": true, className: "size-(--size-icon-sm)", strokeWidth: 2 }), copy.board.addColumn] })) })), _jsx("span", { role: "status", className: "sr-only", children: said })] }));
|
|
138
|
+
}
|
|
@@ -83,6 +83,11 @@ export declare function ListingBar({ actions, title, description, counters, sele
|
|
|
83
83
|
count: number;
|
|
84
84
|
onClear?: () => void;
|
|
85
85
|
actions?: ReactNode;
|
|
86
|
+
/** More match than are chosen: offer to choose them all. */
|
|
87
|
+
matching?: {
|
|
88
|
+
total: number;
|
|
89
|
+
onSelect: () => void;
|
|
90
|
+
};
|
|
86
91
|
};
|
|
87
92
|
search?: ReactNode;
|
|
88
93
|
pagination?: ReactNode;
|
|
@@ -88,7 +88,10 @@ export function ListingBar({ actions, title, description, counters, selection, s
|
|
|
88
88
|
Views used to sit next to the pagination. On a narrow window the search
|
|
89
89
|
field and «1–80 / 211» painted over each other; columns and the switcher
|
|
90
90
|
were the extra weight on a line that only needed how MANY. */
|
|
91
|
-
const center = selecting ? (_jsxs("div", { className: "flex min-w-0 items-center gap-x-2", children: [_jsxs("span", { className: `inline-flex h-(--size-control-md) shrink-0 items-center gap-1.5 rounded-control pr-1 pl-2.5 text-meta font-medium ${isChrome ? "bg-chrome-ink/14 text-chrome-ink" : "bg-ink/8 text-ink"}`, children: [_jsx("span", { className: "tabular-nums", children: selection?.count.toLocaleString(copy.locale) }), copy.selection.chosen, _jsx("button", { type: "button", onClick: selection?.onClear, "aria-label": copy.selection.clear, title: copy.selection.clear, className: `inline-flex size-5 items-center justify-center rounded-pill transition-colors duration-(--duration-instant) ${isChrome ? "hover:bg-chrome-ink/20" : "hover:bg-primary/20"}`, children: _jsx(X, { className: "size-3", strokeWidth: 2.5 }) })] }), selection?.
|
|
91
|
+
const center = selecting ? (_jsxs("div", { className: "flex min-w-0 items-center gap-x-2", children: [_jsxs("span", { className: `inline-flex h-(--size-control-md) shrink-0 items-center gap-1.5 rounded-control pr-1 pl-2.5 text-meta font-medium ${isChrome ? "bg-chrome-ink/14 text-chrome-ink" : "bg-ink/8 text-ink"}`, children: [_jsx("span", { className: "tabular-nums", children: selection?.count.toLocaleString(copy.locale) }), copy.selection.chosen, _jsx("button", { type: "button", onClick: selection?.onClear, "aria-label": copy.selection.clear, title: copy.selection.clear, className: `inline-flex size-5 items-center justify-center rounded-pill transition-colors duration-(--duration-instant) ${isChrome ? "hover:bg-chrome-ink/20" : "hover:bg-primary/20"}`, children: _jsx(X, { className: "size-3", strokeWidth: 2.5 }) })] }), selection?.matching === undefined ||
|
|
92
|
+
selection.matching.total <= selection.count ? null : (_jsx("button", { type: "button", onClick: selection.matching.onSelect, className: `shrink-0 rounded-control px-1 text-meta font-medium underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-primary ${isChrome ? "text-chrome-link" : "text-primary"}`, children: fillCopy(copy.selection.allMatching, {
|
|
93
|
+
count: selection.matching.total.toLocaleString(copy.locale),
|
|
94
|
+
}) })), selection?.actions === undefined ? null : (_jsx("div", { className: "flex min-w-0 items-center gap-1.5", children: selection.actions }))] })) : search === undefined ? null : (
|
|
92
95
|
/* Selecting replaces the search box: what gets selected is over what is
|
|
93
96
|
ALREADY filtered, so searching there would change the set underneath the
|
|
94
97
|
selection. */
|
|
@@ -98,7 +101,7 @@ export function ListingBar({ actions, title, description, counters, selection, s
|
|
|
98
101
|
(count === undefined ? null : (_jsx("span", { role: "status", className: "shrink-0 text-meta text-muted tabular-nums", children: count === 1
|
|
99
102
|
? copy.list.countOne
|
|
100
103
|
: fillCopy(copy.list.count, { count: count.toLocaleString(copy.locale) }) })));
|
|
101
|
-
return (_jsx(SurfaceProvider, { value: "canvas", children: _jsxs("div", { className: `flex min-w-0 flex-wrap items-center gap-y-1.5 ${byDensity(d, "gap-x-3", "gap-x-2")}`, children: [center === null ? null : (_jsx("div", { className: "flex min-w-0 flex-1 basis-(--container-name) items-center overflow-x-clip overflow-y-visible", children: center })), howMany, views === undefined ? null : (_jsx("div", { className: "ml-auto flex shrink-0 items-center gap-2", children: views })), actions === undefined ? null : (_jsx("div", { className: `flex shrink-0 items-center gap-1.5 ${views === undefined ? "ml-auto" : ""}`, children: actions }))] }) }));
|
|
104
|
+
return (_jsx(SurfaceProvider, { value: "canvas", children: _jsxs("div", { className: `flex min-w-0 flex-wrap items-center gap-y-1.5 ${byDensity(d, "gap-x-3", "gap-x-2")}`, children: [title === undefined ? null : (_jsx("h3", { className: "min-w-0 shrink-0 truncate text-body font-semibold text-ink", children: title })), center === null ? null : (_jsx("div", { className: "flex min-w-0 flex-1 basis-(--container-name) items-center overflow-x-clip overflow-y-visible", children: center })), howMany, views === undefined ? null : (_jsx("div", { className: "ml-auto flex shrink-0 items-center gap-2", children: views })), actions === undefined ? null : (_jsx("div", { className: `flex shrink-0 items-center gap-1.5 ${views === undefined ? "ml-auto" : ""}`, children: actions }))] }) }));
|
|
102
105
|
}
|
|
103
106
|
/* El campo de búsqueda va en el CENTRO de la línea, y centrado contra la
|
|
104
107
|
línea: pide `--container-search` y los dos lados se reparten lo que sobra
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { GroupableField, GroupLevel, PileOf } from "../../foundations/grouping.js";
|
|
2
|
+
import { type PivotFold, type PivotKey } from "../../foundations/pivot.js";
|
|
3
|
+
export type PivotExplorerProps<T> = {
|
|
4
|
+
items: readonly T[];
|
|
5
|
+
/** Which pile a record falls in, at a level — the same contract that groups a list. */
|
|
6
|
+
of: PileOf<T>;
|
|
7
|
+
/** What can be split by: a field, and its grains when it is a date. */
|
|
8
|
+
fields: readonly GroupableField[];
|
|
9
|
+
/** Every measure that can be shown; `defaultMeasures` says which start on. */
|
|
10
|
+
measures: readonly PivotFold<T>[];
|
|
11
|
+
defaultMeasures?: readonly string[];
|
|
12
|
+
/** The rows' levels, CONTROLLED from outside — the search bar's grouping. While given, the
|
|
13
|
+
* rows follow it and are not edited here. */
|
|
14
|
+
rows?: readonly GroupLevel[];
|
|
15
|
+
defaultRows?: readonly GroupLevel[];
|
|
16
|
+
defaultColumns?: readonly GroupLevel[];
|
|
17
|
+
/** A cell was pressed: open the records that fell there. */
|
|
18
|
+
onCell?: (row: PivotKey, column: PivotKey) => void;
|
|
19
|
+
height?: number | string;
|
|
20
|
+
/** The file's name when downloaded. */
|
|
21
|
+
fileName?: string;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* A pivot people actually pivot: what goes down the rows, what goes across the columns, which
|
|
25
|
+
* measures — chosen on the grid, not fixed in code.
|
|
26
|
+
*
|
|
27
|
+
* Context: the fold (`pivotFrom`) was already a real pivot — N levels each way, margins folded
|
|
28
|
+
* from the base — but its axes and measures were written once by whoever composed it. Here
|
|
29
|
+
* they are the person's: «+» adds a field (a date with its grain) to an axis, a level's ×
|
|
30
|
+
* drops it, ⇄ swaps the axes, the measures are ticked, and the grid downloads as it is seen.
|
|
31
|
+
* Inside a list the rows FOLLOW the search bar's grouping and the records are the ones the
|
|
32
|
+
* search left: the pivot answers the same question the list is asking.
|
|
33
|
+
*/
|
|
34
|
+
export declare function PivotExplorer<T>({ items, of, fields, measures, defaultMeasures, rows: outerRows, defaultRows, defaultColumns, onCell, height, fileName, }: PivotExplorerProps<T>): import("react").JSX.Element;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { grainName } from "../../foundations/calendar.js";
|
|
4
|
+
import { fillCopy, useUiCopy } from "../../foundations/copy.js";
|
|
5
|
+
import { pivotFrom } from "../../foundations/pivot.js";
|
|
6
|
+
import { downloadPivot } from "../../foundations/pivotSheet.js";
|
|
7
|
+
import { Button, IconButton } from "../../primitives/button/Button.js";
|
|
8
|
+
import { Menu } from "../../primitives/menu/Menu.js";
|
|
9
|
+
import { PivotTable } from "../../primitives/table/PivotTable.js";
|
|
10
|
+
import { Tags } from "../../primitives/tag/Tags.js";
|
|
11
|
+
import { ArrowLeftRight, Download, Plus, Sigma } from "lucide-react";
|
|
12
|
+
import { useMemo, useState } from "react";
|
|
13
|
+
const levelId = (level) => `${level.field}:${level.grain ?? ""}`;
|
|
14
|
+
const fromId = (id) => {
|
|
15
|
+
const [field = "", grain = ""] = id.split(":");
|
|
16
|
+
return grain === "" ? { field } : { field, grain: grain };
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* A pivot people actually pivot: what goes down the rows, what goes across the columns, which
|
|
20
|
+
* measures — chosen on the grid, not fixed in code.
|
|
21
|
+
*
|
|
22
|
+
* Context: the fold (`pivotFrom`) was already a real pivot — N levels each way, margins folded
|
|
23
|
+
* from the base — but its axes and measures were written once by whoever composed it. Here
|
|
24
|
+
* they are the person's: «+» adds a field (a date with its grain) to an axis, a level's ×
|
|
25
|
+
* drops it, ⇄ swaps the axes, the measures are ticked, and the grid downloads as it is seen.
|
|
26
|
+
* Inside a list the rows FOLLOW the search bar's grouping and the records are the ones the
|
|
27
|
+
* search left: the pivot answers the same question the list is asking.
|
|
28
|
+
*/
|
|
29
|
+
export function PivotExplorer({ items, of, fields, measures, defaultMeasures, rows: outerRows, defaultRows = [], defaultColumns = [], onCell, height, fileName, }) {
|
|
30
|
+
const copy = useUiCopy();
|
|
31
|
+
const words = copy.pivot;
|
|
32
|
+
const [ownRows, setOwnRows] = useState(defaultRows);
|
|
33
|
+
const [columns, setColumns] = useState(defaultColumns);
|
|
34
|
+
const [chosen, setChosen] = useState(() => new Set(defaultMeasures ?? measures.slice(0, 1).map((one) => one.id)));
|
|
35
|
+
const rows = outerRows ?? ownRows;
|
|
36
|
+
const following = outerRows !== undefined;
|
|
37
|
+
const labelOf = (level) => {
|
|
38
|
+
const field = fields.find((one) => one.field === level.field)?.label ?? level.field;
|
|
39
|
+
return level.grain === undefined
|
|
40
|
+
? field
|
|
41
|
+
: `${field} · ${grainName(level.grain).toLowerCase()}`;
|
|
42
|
+
};
|
|
43
|
+
const shownMeasures = measures.filter((one) => chosen.has(one.id));
|
|
44
|
+
const pivot = useMemo(() => pivotFrom(items, { rows, columns, of, measures: shownMeasures, labelOf }),
|
|
45
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
46
|
+
[items, rows, columns, of, shownMeasures.map((one) => one.id).join("|")]);
|
|
47
|
+
/* What can be added to an axis: every field (each grain of a date) not on either axis. */
|
|
48
|
+
const used = new Set([...rows, ...columns].map(levelId));
|
|
49
|
+
const options = fields.flatMap((field) => (field.grains ?? [undefined]).map((grain) => {
|
|
50
|
+
const level = grain === undefined ? { field: field.field } : { field: field.field, grain };
|
|
51
|
+
return {
|
|
52
|
+
id: levelId(level),
|
|
53
|
+
label: labelOf(level),
|
|
54
|
+
disabled: used.has(levelId(level)),
|
|
55
|
+
};
|
|
56
|
+
}));
|
|
57
|
+
const axis = (name, levels, set) => (_jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-1.5", children: [_jsx("span", { className: "text-micro font-medium text-muted", children: name }), _jsx(Tags, { label: name, items: levels.map((level) => ({
|
|
58
|
+
id: levelId(level),
|
|
59
|
+
label: labelOf(level),
|
|
60
|
+
...(set === null
|
|
61
|
+
? {}
|
|
62
|
+
: {
|
|
63
|
+
onRemove: () => set(levels.filter((one) => levelId(one) !== levelId(level))),
|
|
64
|
+
removeLabel: fillCopy(words.removeLevel, { level: labelOf(level) }),
|
|
65
|
+
}),
|
|
66
|
+
})) }), set === null ? (_jsx("span", { className: "text-micro text-muted italic", children: words.fromSearch })) : (_jsx(Menu, { label: fillCopy(words.addLevel, { axis: name }), icon: Plus, iconOnly: true, chevron: false, variant: "text", groups: [{ options }], onSelect: (id) => set([...levels, fromId(id)]) }))] }));
|
|
67
|
+
return (_jsxs("div", { className: "flex min-w-0 flex-col gap-3", children: [_jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-x-5 gap-y-2", children: [_jsx(Menu, { label: words.measures, icon: Sigma, groups: [
|
|
68
|
+
{
|
|
69
|
+
options: measures.map((one) => ({
|
|
70
|
+
id: one.id,
|
|
71
|
+
label: one.header,
|
|
72
|
+
checked: chosen.has(one.id),
|
|
73
|
+
})),
|
|
74
|
+
},
|
|
75
|
+
], onSelect: (id) => setChosen((current) => {
|
|
76
|
+
const next = new Set(current);
|
|
77
|
+
if (next.has(id))
|
|
78
|
+
next.delete(id);
|
|
79
|
+
else
|
|
80
|
+
next.add(id);
|
|
81
|
+
return next;
|
|
82
|
+
}) }), axis(words.rowsAxis, rows, following ? null : setOwnRows), axis(words.columnsAxis, columns, setColumns), _jsxs("div", { className: "ml-auto flex items-center gap-1", children: [following ? null : (_jsx(IconButton, { icon: ArrowLeftRight, label: words.swap, onClick: () => {
|
|
83
|
+
setOwnRows(columns);
|
|
84
|
+
setColumns(rows);
|
|
85
|
+
} })), _jsx(Button, { role: "ghost", size: "sm", icon: Download, onClick: () => downloadPivot(pivot, fileName === undefined ? {} : { name: fileName }), children: words.download })] })] }), _jsx(PivotTable, { pivot: pivot, ...(onCell === undefined ? {} : { onCell }), ...(height === undefined ? {} : { height }) })] }));
|
|
86
|
+
}
|
|
@@ -12,22 +12,20 @@ import type { ReactNode } from "react";
|
|
|
12
12
|
* Its hierarchy is the row's: the state first (what it is), the name as the entry point,
|
|
13
13
|
* the facts below in the quiet size.
|
|
14
14
|
*/
|
|
15
|
-
export declare function RecordCard({ title, tags, meta, figure, media, onOpen, }: {
|
|
16
|
-
/** The record's name — the entry point of the card. */
|
|
15
|
+
export declare function RecordCard({ title, subtitle, tags, meta, figure, media, footer, onOpen, }: {
|
|
17
16
|
title: ReactNode;
|
|
18
|
-
/**
|
|
17
|
+
/** Who or what it is about, under the title: the customer of an opportunity. */
|
|
18
|
+
subtitle?: ReactNode;
|
|
19
19
|
tags?: ReactNode;
|
|
20
|
-
/** The facts that tell two cards apart — a number, a date. */
|
|
21
20
|
meta?: ReactNode;
|
|
22
|
-
/** The record's key figure, on the right of the name — an amount. */
|
|
23
21
|
figure?: ReactNode;
|
|
24
|
-
/** The record's picture — a product photo, a customer's logo — or its icon. A board of
|
|
25
|
-
* products is recognised by the pictures before a single name is read. */
|
|
26
22
|
media?: {
|
|
27
23
|
src?: string;
|
|
28
24
|
alt?: string;
|
|
29
25
|
icon?: LucideIcon;
|
|
30
26
|
};
|
|
31
|
-
/**
|
|
27
|
+
/** The card's last line, its controls: priority, kanban state, next activity, the owner's
|
|
28
|
+
* avatar — each its own button, pressable without opening the card. */
|
|
29
|
+
footer?: ReactNode;
|
|
32
30
|
onOpen?: () => void;
|
|
33
31
|
}): import("react").JSX.Element;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Thumb } from "../../primitives/cell/CellStack.js";
|
|
4
4
|
/**
|
|
5
5
|
* One record as a card — the board presentation of a list.
|
|
@@ -13,10 +13,16 @@ import { Thumb } from "../../primitives/cell/CellStack.js";
|
|
|
13
13
|
* Its hierarchy is the row's: the state first (what it is), the name as the entry point,
|
|
14
14
|
* the facts below in the quiet size.
|
|
15
15
|
*/
|
|
16
|
-
export function RecordCard({ title, tags, meta, figure, media, onOpen, }) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
export function RecordCard({ title, subtitle, tags, meta, figure, media, footer, onOpen, }) {
|
|
17
|
+
/* The card is a box, and the TITLE is the button that opens it, stretched over the whole
|
|
18
|
+
card (`after:inset-0`). It used to be a `<button>` around everything: a priority star or a
|
|
19
|
+
kanban state inside it would be a control inside a control, which is invalid and which a
|
|
20
|
+
screen reader announces as one thing. The footer's controls sit above the stretch. */
|
|
21
|
+
const titleText = _jsx("span", { className: "min-w-0 truncate", children: title });
|
|
22
|
+
return (_jsxs("div", { className: `relative flex w-full min-w-0 flex-col gap-2 rounded-box border border-line bg-surface p-4 text-left ${onOpen === undefined
|
|
23
|
+
? ""
|
|
24
|
+
: "transition-colors duration-(--duration-instant) focus-within:border-line-strong hover:border-line-strong"}`, children: [media === undefined ? null : (_jsx(Thumb, { size: "lg", ...(media.src === undefined ? {} : { src: media.src }), ...(media.alt === undefined ? {} : { alt: media.alt }), ...(media.icon === undefined ? {} : { icon: media.icon }) })), tags === undefined ? null : _jsx("div", { className: "flex flex-wrap gap-1.5", children: tags }), _jsxs("div", { className: "flex min-w-0 items-baseline justify-between gap-3", children: [onOpen === undefined ? (_jsx("p", { className: "flex min-w-0 text-lead font-medium text-ink", children: titleText })) : (_jsx("button", { type: "button", onClick: onOpen, className: "flex min-w-0 text-left text-lead font-medium text-ink outline-none after:absolute after:inset-0 after:rounded-box focus-visible:after:outline-2 focus-visible:after:outline-primary", children: titleText })), figure === undefined ? null : (_jsx("p", { className: "shrink-0 text-lead font-semibold tabular-nums text-ink", children: figure }))] }), subtitle === undefined ? null : (
|
|
25
|
+
/* Above the stretched title, at its own width: the subtitle may be a link to ANOTHER
|
|
26
|
+
record (the customer), which must open that one and not the card. */
|
|
27
|
+
_jsx("p", { className: "relative z-10 -mt-1.5 w-fit max-w-full min-w-0 truncate text-meta text-muted", children: subtitle })), meta === undefined ? null : (_jsx("p", { className: "text-micro tabular-nums text-muted", children: meta })), footer === undefined ? null : (_jsx("div", { className: "relative z-10 flex min-w-0 items-center gap-1.5", children: footer }))] }));
|
|
22
28
|
}
|
|
@@ -18,8 +18,13 @@ import type { ReactNode } from "react";
|
|
|
18
18
|
*
|
|
19
19
|
* «some metadata that might help is not visible» — and it was literal: to know
|
|
20
20
|
* whether `FAC/2026/0118` was paid or overdue you had to open it. */
|
|
21
|
-
export declare function Row({ icon: Icon, name, onOpen, onPeek, tags, meta, selected, onSelectedChange, actions, more, onMoreSelect, openHint, children, copy: copyAs, }: {
|
|
21
|
+
export declare function Row({ icon: Icon, avatar, name, onOpen, onPeek, click, openers, tags, meta, selected, onSelectedChange, actions, more, onMoreSelect, openHint, children, copy: copyAs, }: {
|
|
22
22
|
icon?: LucideIcon;
|
|
23
|
+
/** A person or a company in the icon's place: who the row is, recognised before it is read. */
|
|
24
|
+
avatar?: {
|
|
25
|
+
name: string;
|
|
26
|
+
src?: string;
|
|
27
|
+
};
|
|
23
28
|
name: string;
|
|
24
29
|
/** The copy button among the row's actions: on by default, copying the row's name — the
|
|
25
30
|
* number of an invoice, a customer — without opening it. A string copies that instead;
|
|
@@ -27,6 +32,10 @@ export declare function Row({ icon: Icon, name, onOpen, onPeek, tags, meta, sele
|
|
|
27
32
|
copy?: string | false;
|
|
28
33
|
onOpen?: () => void;
|
|
29
34
|
onPeek?: () => void;
|
|
35
|
+
/** What a press on the body does when both exist: the look (default) or the record. */
|
|
36
|
+
click?: "peek" | "open";
|
|
37
|
+
/** The 👁 / ↗ buttons. Off in a list that chooses: there the row answers, it does not open. */
|
|
38
|
+
openers?: boolean;
|
|
30
39
|
tags?: ReactNode;
|
|
31
40
|
/** The secondary data, already formatted. In mono: it aligns across rows. */
|
|
32
41
|
meta?: string;
|
|
@@ -67,7 +76,7 @@ export declare function Row({ icon: Icon, name, onOpen, onPeek, tags, meta, sele
|
|
|
67
76
|
* espaciadores de casilla, ícono y acciones— en vez de declarar un ancho
|
|
68
77
|
* propio. Si `Row` cambia su geometría, esto la sigue.
|
|
69
78
|
*/
|
|
70
|
-
export declare function RowHeader({ name, meta, tags, withSelection, withIcon, withActions, }: {
|
|
79
|
+
export declare function RowHeader({ name, meta, tags, withSelection, withIcon, withAvatar, withActions, trail, }: {
|
|
71
80
|
name: string;
|
|
72
81
|
meta?: string;
|
|
73
82
|
tags?: string;
|
|
@@ -75,10 +84,20 @@ export declare function RowHeader({ name, meta, tags, withSelection, withIcon, w
|
|
|
75
84
|
* es el único que sabe si esta lista elige y si sus filas tienen ícono. */
|
|
76
85
|
withSelection?: boolean;
|
|
77
86
|
withIcon?: boolean;
|
|
87
|
+
/** The rows carry a face instead of an icon: a wider spacer, so the labels stay aligned. */
|
|
88
|
+
withAvatar?: boolean;
|
|
78
89
|
/** La zona de acciones de la derecha ocupa su lugar aunque esté invisible
|
|
79
90
|
* —así nada se corre al pasar el puntero—, así que el rótulo de las fichas
|
|
80
91
|
* queda desalineado si acá no se reserva lo mismo. */
|
|
81
92
|
withActions?: boolean;
|
|
93
|
+
/** What the rows have at their end — the same flags `Row` gets — drawn invisible here so
|
|
94
|
+
* the header reserves exactly that width. Takes over from `withActions`. */
|
|
95
|
+
trail?: {
|
|
96
|
+
actions?: ReactNode;
|
|
97
|
+
peek?: boolean;
|
|
98
|
+
open?: boolean;
|
|
99
|
+
copy?: boolean;
|
|
100
|
+
};
|
|
82
101
|
}): import("react").JSX.Element;
|
|
83
102
|
/**
|
|
84
103
|
* The header of a group inside a list.
|
|
@@ -1,12 +1,28 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx,
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { RecordLink } from "./RecordLink.js";
|
|
4
4
|
import { fillCopy, useUiCopy } from "../../foundations/copy.js";
|
|
5
5
|
import { byDensity, useDensity } from "../../foundations/density.js";
|
|
6
6
|
import { stickyTop } from "../../foundations/sticky.js";
|
|
7
7
|
import { Menu } from "../../primitives/menu/Menu.js";
|
|
8
|
+
import { HEADER_TEXT } from "../../primitives/table/Table.js";
|
|
8
9
|
import { CopyButton } from "../../primitives/value/Copyable.js";
|
|
9
|
-
import {
|
|
10
|
+
import { Avatar } from "../../primitives/value/Person.js";
|
|
11
|
+
import { ChevronRight, Eye, MoreHorizontal, SquareArrowOutUpRight } from "lucide-react";
|
|
12
|
+
const ICON_BUTTON = "inline-flex size-(--size-control-sm) items-center justify-center rounded-pill text-muted transition-colors duration-(--duration-instant) hover:bg-ink/6 hover:text-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary";
|
|
13
|
+
/** The buttons at the end of a row. Its own piece so `RowHeader` can draw the same one,
|
|
14
|
+
* invisible, and reserve exactly its width — a spacer of a guessed width drifted every time
|
|
15
|
+
* a button was added. */
|
|
16
|
+
function Trail({ name, openAs, actions, copy: copyAs, onPeek, onOpen, more, onMoreSelect, }) {
|
|
17
|
+
const copy = useUiCopy();
|
|
18
|
+
return (_jsxs(_Fragment, { children: [actions, copyAs === false ? null : (_jsx(CopyButton, { value: copyAs ?? name, label: fillCopy(copy.copyable.field, { label: name }) })), onPeek === undefined ? null : (_jsx("button", { type: "button", onClick: (e) => {
|
|
19
|
+
e.stopPropagation();
|
|
20
|
+
onPeek();
|
|
21
|
+
}, title: copy.list.peek, "aria-label": `${copy.list.peek}: ${name}`, className: ICON_BUTTON, children: _jsx(Eye, { className: "size-(--size-icon-sm)", strokeWidth: 2 }) })), onOpen === undefined ? null : (_jsx("button", { type: "button", onClick: (e) => {
|
|
22
|
+
e.stopPropagation();
|
|
23
|
+
onOpen();
|
|
24
|
+
}, title: openAs, "aria-label": `${openAs}: ${name}`, className: ICON_BUTTON, children: _jsx(SquareArrowOutUpRight, { className: "size-(--size-icon-sm)", strokeWidth: 2 }) })), more === undefined || more.length === 0 ? null : (_jsx("span", { onClick: (e) => e.stopPropagation(), role: "presentation", children: _jsx(Menu, { label: fillCopy(copy.selection.moreOf, { name }), groups: more, icon: MoreHorizontal, iconOnly: true, chevron: false, align: "end", width: "sm", variant: "text", ...(onMoreSelect === undefined ? {} : { onSelect: onMoreSelect }) }) }))] }));
|
|
25
|
+
}
|
|
10
26
|
/* ---------------------------------------------------------------- *
|
|
11
27
|
* THE RICH ROW — the other density
|
|
12
28
|
* ---------------------------------------------------------------- */
|
|
@@ -27,10 +43,13 @@ import { ChevronRight, MoreHorizontal, SquareArrowOutUpRight } from "lucide-reac
|
|
|
27
43
|
*
|
|
28
44
|
* «some metadata that might help is not visible» — and it was literal: to know
|
|
29
45
|
* whether `FAC/2026/0118` was paid or overdue you had to open it. */
|
|
30
|
-
export function Row({ icon: Icon, name, onOpen, onPeek, tags, meta, selected, onSelectedChange, actions, more, onMoreSelect, openHint, children, copy: copyAs, }) {
|
|
46
|
+
export function Row({ icon: Icon, avatar, name, onOpen, onPeek, click = "peek", openers = true, tags, meta, selected, onSelectedChange, actions, more, onMoreSelect, openHint, children, copy: copyAs, }) {
|
|
31
47
|
const d = useDensity();
|
|
32
48
|
const copy = useUiCopy();
|
|
33
|
-
const openAs = openHint ?? copy.
|
|
49
|
+
const openAs = openHint ?? copy.list.open;
|
|
50
|
+
/* A read-only row (nothing to peek, nothing to open) does not light up on hover: a wash
|
|
51
|
+
and a hand promise a click that does nothing. */
|
|
52
|
+
const interactive = (onPeek ?? onOpen) !== undefined;
|
|
34
53
|
const row = (
|
|
35
54
|
/* A `div` and not an `li`: whoever composes decides whether this is a list,
|
|
36
55
|
a table or a branch of a tree. And the zone that peeks is a `div` with
|
|
@@ -42,21 +61,18 @@ export function Row({ icon: Icon, name, onOpen, onPeek, tags, meta, selected, on
|
|
|
42
61
|
no rule between them read as a paragraph. The wash is the inner row,
|
|
43
62
|
inset and rounded, so the hover is a mark and not a stripe. The inner
|
|
44
63
|
padding is shorter by exactly that inset, so the text does not move. */
|
|
45
|
-
className: `group/row flex min-w-0 items-center rounded-control transition-colors duration-(--duration-instant) hover:bg-ink/4 ${byDensity(d, "gap-3 px-2 py-2", "gap-2 px-1.5 py-1")}`,
|
|
64
|
+
className: `group/row flex min-w-0 items-center rounded-control transition-colors duration-(--duration-instant) ${interactive ? "cursor-pointer hover:bg-ink/4" : ""} ${byDensity(d, "gap-3 px-2 py-2", "gap-2 px-1.5 py-1")}`,
|
|
46
65
|
/* `onPeek` con `onOpen` de reserva: quien sólo declara `onOpen` —lo
|
|
47
66
|
natural si nunca oyó hablar de la vista previa— tiene que poder abrir
|
|
48
67
|
igual haciendo click en cualquier parte de la fila, no sólo en el
|
|
49
68
|
nombre. Con la vista previa apagada (`PeekLayer.tsx`) esto es hoy el
|
|
50
69
|
caso de todos: un click en la fila abre, sin más. */
|
|
51
|
-
onClick: onPeek ?? onOpen, role: "presentation", children: [onSelectedChange === undefined ? null : (_jsx("input", { type: "checkbox", checked: selected ?? false, "aria-label": fillCopy(copy.selection.named, { name }), onChange: (e) => onSelectedChange(e.target.checked), onClick: (e) => e.stopPropagation(), className: "size-3.5 shrink-0 cursor-pointer accent-primary" })), Icon === undefined ? null : (_jsx(Icon, { className: "size-(--size-icon-md) shrink-0 text-muted", strokeWidth: 1.75, "aria-hidden": true })), _jsxs("div", { className: `flex min-w-0 shrink grow basis-(--container-name) flex-wrap items-center gap-y-0 ${byDensity(d, "gap-x-3", "gap-x-2")}`, children: [_jsx("span", { className: "min-w-0 grow basis-(--container-name)", children: _jsx(RecordLink, { ...((onOpen ?? onPeek) === undefined ? {} : { onClick: (onOpen ?? onPeek) }), title: name, children: name }) }), meta === undefined ? null : (
|
|
70
|
+
onClick: click === "open" ? (onOpen ?? onPeek) : (onPeek ?? onOpen), role: "presentation", children: [onSelectedChange === undefined ? null : (_jsx("input", { type: "checkbox", checked: selected ?? false, "aria-label": fillCopy(copy.selection.named, { name }), onChange: (e) => onSelectedChange(e.target.checked), onClick: (e) => e.stopPropagation(), className: "size-3.5 shrink-0 cursor-pointer accent-primary" })), avatar !== undefined ? (_jsx(Avatar, { name: avatar.name, size: "sm", ...(avatar.src === undefined ? {} : { src: avatar.src }) })) : Icon === undefined ? null : (_jsx(Icon, { className: "size-(--size-icon-md) shrink-0 text-muted", strokeWidth: 1.75, "aria-hidden": true })), _jsxs("div", { className: `flex min-w-0 shrink grow basis-(--container-name) flex-wrap items-center gap-y-0 ${byDensity(d, "gap-x-3", "gap-x-2")}`, children: [_jsx("span", { className: "min-w-0 grow basis-(--container-name)", children: _jsx(RecordLink, { ...((onOpen ?? onPeek) === undefined ? {} : { onClick: (onOpen ?? onPeek) }), title: name, children: name }) }), meta === undefined ? null : (
|
|
52
71
|
/* Wrapped or not, the metadata is still there —it is what saves
|
|
53
72
|
having to open a set to know whether it is a thousand rows or a
|
|
54
73
|
million— and it truncates instead of wrapping into itself: three
|
|
55
74
|
lines per row in a 480 px drawer means three rows fit on screen. */
|
|
56
|
-
_jsx("span", { className: "min-w-0 truncate text-micro tabular-nums text-muted", children: meta }))] }), tags === undefined ? null : (_jsx("span", { className: "flex min-w-fit shrink basis-(--container-tags) items-center justify-end gap-1.5", children: tags })),
|
|
57
|
-
e.stopPropagation();
|
|
58
|
-
onOpen();
|
|
59
|
-
}, title: openAs, "aria-label": `${openAs}: ${name}`, className: "inline-flex size-(--size-control-sm) items-center justify-center rounded-pill text-muted transition-colors duration-(--duration-instant) hover:bg-ink/6 hover:text-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary", children: _jsx(SquareArrowOutUpRight, { className: "size-(--size-icon-sm)", strokeWidth: 2 }) })), more === undefined || more.length === 0 ? null : (_jsx("span", { onClick: (e) => e.stopPropagation(), role: "presentation", children: _jsx(Menu, { label: fillCopy(copy.selection.moreOf, { name }), groups: more, icon: MoreHorizontal, iconOnly: true, chevron: false, align: "end", width: "sm", variant: "text", ...(onMoreSelect === undefined ? {} : { onSelect: onMoreSelect }) }) }))] })] }) }));
|
|
75
|
+
_jsx("span", { className: "min-w-0 truncate text-micro tabular-nums text-muted", children: meta }))] }), tags === undefined ? null : (_jsx("span", { className: "flex min-w-fit shrink basis-(--container-tags) items-center justify-end gap-1.5", children: tags })), _jsx("span", { className: "flex shrink-0 items-center gap-1 opacity-0 transition-opacity duration-(--duration-instant) group-hover/row:opacity-100 focus-within:opacity-100 pointer-coarse:opacity-100", children: _jsx(Trail, { name: name, openAs: openAs, ...(actions === undefined ? {} : { actions }), copy: copyAs, ...(onPeek === undefined || !openers ? {} : { onPeek }), ...(onOpen === undefined || !openers ? {} : { onOpen }), ...(more === undefined ? {} : { more }), ...(onMoreSelect === undefined ? {} : { onMoreSelect }) }) })] }) }));
|
|
60
76
|
if (children === undefined)
|
|
61
77
|
return row;
|
|
62
78
|
/* What the resource produced, hanging below. The indent reaches up to where
|
|
@@ -87,13 +103,13 @@ export function Row({ icon: Icon, name, onOpen, onPeek, tags, meta, selected, on
|
|
|
87
103
|
* espaciadores de casilla, ícono y acciones— en vez de declarar un ancho
|
|
88
104
|
* propio. Si `Row` cambia su geometría, esto la sigue.
|
|
89
105
|
*/
|
|
90
|
-
export function RowHeader({ name, meta, tags, withSelection = false, withIcon = false, withActions = false, }) {
|
|
106
|
+
export function RowHeader({ name, meta, tags, withSelection = false, withIcon = false, withAvatar = false, withActions = false, trail, }) {
|
|
91
107
|
const d = useDensity();
|
|
92
|
-
const rotulo =
|
|
108
|
+
const rotulo = `truncate ${HEADER_TEXT}`;
|
|
93
109
|
return (_jsxs("div", { "data-slot": "row-header",
|
|
94
110
|
/* Se pega debajo de la barra de la lista, como el encabezado de la tabla
|
|
95
111
|
y el de la planilla. Ver `system/sticky.tsx`. */
|
|
96
|
-
className: `sticky z-10 flex min-w-0 items-center border-b border-line-strong bg-ground ${byDensity(d, "gap-3 px-3 py-2", "gap-2 px-2.5 py-1.5")}`, style: stickyTop, role: "presentation", children: [!withSelection ? null : _jsx("span", { className: "size-3.5 shrink-0", "aria-hidden": true }), !withIcon ? null : _jsx("span", { className: "size-(--size-icon-md) shrink-0", "aria-hidden": true }), _jsxs("div", { className: `flex min-w-0 shrink grow basis-(--container-name) flex-wrap items-center gap-y-0 ${byDensity(d, "gap-x-3", "gap-x-2")}`, children: [_jsx("span", { className: `min-w-0 grow basis-(--container-name) ${rotulo}`, children: name }), meta === undefined ? null : _jsx("span", { className: `min-w-0 ${rotulo}`, children: meta })] }), tags === undefined ? null : (_jsx("span", { className: "flex min-w-fit shrink basis-(--container-tags) items-center justify-end", children: _jsx("span", { className: rotulo, children: tags }) })), !withActions ? null : (_jsx("span", { className: "size-(--size-control-sm) shrink-0", "aria-hidden": true }))] }));
|
|
112
|
+
className: `sticky z-10 flex min-w-0 items-center border-b border-line-strong bg-ground ${byDensity(d, "gap-3 px-3 py-2", "gap-2 px-2.5 py-1.5")}`, style: stickyTop, role: "presentation", children: [!withSelection ? null : _jsx("span", { className: "size-3.5 shrink-0", "aria-hidden": true }), withAvatar ? (_jsx("span", { className: "size-5 shrink-0", "aria-hidden": true })) : !withIcon ? null : (_jsx("span", { className: "size-(--size-icon-md) shrink-0", "aria-hidden": true })), _jsxs("div", { className: `flex min-w-0 shrink grow basis-(--container-name) flex-wrap items-center gap-y-0 ${byDensity(d, "gap-x-3", "gap-x-2")}`, children: [_jsx("span", { className: `min-w-0 grow basis-(--container-name) ${rotulo}`, children: name }), meta === undefined ? null : _jsx("span", { className: `min-w-0 ${rotulo}`, children: meta })] }), tags === undefined ? null : (_jsx("span", { className: "flex min-w-fit shrink basis-(--container-tags) items-center justify-end", children: _jsx("span", { className: rotulo, children: tags }) })), trail !== undefined ? (_jsx("span", { inert: true, "aria-hidden": true, className: "invisible flex shrink-0 items-center gap-1", children: _jsx(Trail, { name: "", openAs: "", ...(trail.actions === undefined ? {} : { actions: trail.actions }), ...(trail.copy === false ? { copy: false } : {}), ...(trail.peek === true ? { onPeek: () => { } } : {}), ...(trail.open === true ? { onOpen: () => { } } : {}) }) })) : !withActions ? null : (_jsx("span", { className: "size-(--size-control-sm) shrink-0", "aria-hidden": true }))] }));
|
|
97
113
|
}
|
|
98
114
|
/**
|
|
99
115
|
* The header of a group inside a list.
|
|
@@ -11,7 +11,9 @@ export { type Selection, useSelection } from "./selection.js";
|
|
|
11
11
|
export { type SetControl, useSetControl } from "./set.js";
|
|
12
12
|
export { toggleCollapsed, type TreeControl, useTreeControl, } from "./tree.js";
|
|
13
13
|
export { type ViewOption, ViewSwitcher, } from "./ViewSwitcher.js";
|
|
14
|
+
export { type BoardColumn, BoardColumns, type BoardStages, } from "./BoardColumns.js";
|
|
14
15
|
export { type GroupingControl, useGroupingControl, } from "./grouping.js";
|
|
15
16
|
export { GroupingLevels, type GroupingLevelsProps, } from "./GroupingLevels.js";
|
|
17
|
+
export { PivotExplorer, type PivotExplorerProps, } from "./PivotExplorer.js";
|
|
16
18
|
export { RecordCard } from "./RecordCard.js";
|
|
17
19
|
export { type SortField, SortMenu, type SortMenuProps, } from "./SortMenu.js";
|
|
@@ -14,7 +14,9 @@ export { useSetControl } from "./set.js";
|
|
|
14
14
|
export { toggleCollapsed, useTreeControl, } from "./tree.js";
|
|
15
15
|
export { ViewSwitcher, } from "./ViewSwitcher.js";
|
|
16
16
|
/* El control de los niveles. El vocabulario está en `system/grouping.ts`. */
|
|
17
|
+
export { BoardColumns, } from "./BoardColumns.js";
|
|
17
18
|
export { useGroupingControl, } from "./grouping.js";
|
|
18
19
|
export { GroupingLevels, } from "./GroupingLevels.js";
|
|
20
|
+
export { PivotExplorer, } from "./PivotExplorer.js";
|
|
19
21
|
export { RecordCard } from "./RecordCard.js";
|
|
20
22
|
export { SortMenu, } from "./SortMenu.js";
|
|
@@ -33,7 +33,7 @@ import { type Pages } from "./pages.js";
|
|
|
33
33
|
* también; lo que se lee está en el cruce. Tampoco se
|
|
34
34
|
* ordena: el orden de cada eje es el de sus claves.
|
|
35
35
|
*/
|
|
36
|
-
export type ListPresent = "rows" | "table" | "board" | "sheet" | "report" | "pivot";
|
|
36
|
+
export type ListPresent = "rows" | "table" | "board" | "sheet" | "report" | "pivot" | "calendar" | "gantt" | "timeline";
|
|
37
37
|
export interface SetControl<Row, Present extends string = ListPresent> extends Pages<Row> {
|
|
38
38
|
present: Present;
|
|
39
39
|
setPresent: (next: Present) => void;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/* LA NAVEGACIÓN — el árbol lateral de una aplicación
|
|
2
|
-
|
|
3
|
-
export {} from "./commands.js";
|
|
1
|
+
/* LA NAVEGACIÓN — el árbol lateral de una aplicación. La paleta de comandos (⌘K) es del
|
|
2
|
+
espacio de trabajo (composer), no de acá. */
|
|
4
3
|
export { NavigationTree, } from "./NavigationTree.js";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
/** Who did something to the record and when, already written: «Carla Rojas», «hace 3 días». */
|
|
3
|
+
export type RecordStamp = {
|
|
4
|
+
by: ReactNode;
|
|
5
|
+
at: ReactNode;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* The last line of the paper: who created the record and who touched it last.
|
|
9
|
+
*
|
|
10
|
+
* Context: «¿quién cambió esto?» is the first question in front of a number that looks wrong,
|
|
11
|
+
* and the activity's timeline answers it only for what was logged. Small and at the end: it is
|
|
12
|
+
* consulted, not read.
|
|
13
|
+
*/
|
|
14
|
+
export declare function RecordAudit({ created, updated, }: {
|
|
15
|
+
created?: RecordStamp;
|
|
16
|
+
updated?: RecordStamp;
|
|
17
|
+
}): import("react").JSX.Element | null;
|