@sincpro/web-ui 1.1.0 → 1.3.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/dist/components/activity/Activity.js +1 -1
- package/dist/components/activity/Messages.d.ts +4 -2
- package/dist/components/activity/Messages.js +4 -4
- package/dist/components/activity/Timeline.js +1 -1
- package/dist/components/assist/AssistMark.js +1 -1
- package/dist/components/assist/AssistSuggestion.js +1 -1
- package/dist/components/assist/ChangeReview.js +2 -2
- package/dist/components/calendar/RecordCalendar.js +4 -1
- package/dist/components/form/Sheet.js +1 -1
- package/dist/components/lines/LineItems.d.ts +1 -1
- package/dist/components/lines/LineItems.js +2 -2
- package/dist/components/listing/ListingBar.js +1 -1
- package/dist/components/listing/Row.d.ts +7 -0
- package/dist/components/listing/Row.js +7 -1
- package/dist/components/listing/ViewSwitcher.d.ts +7 -2
- package/dist/components/listing/index.d.ts +1 -1
- package/dist/components/listing/set.d.ts +4 -1
- package/dist/components/record/Breadcrumb.js +5 -1
- package/dist/components/search/CriteriaBuilder.d.ts +1 -1
- package/dist/components/search/CriteriaBuilder.js +3 -3
- package/dist/components/search/FilterBuilder.d.ts +1 -18
- package/dist/components/search/FilterBuilder.js +4 -4
- package/dist/components/search/JsonEditor.js +5 -1
- package/dist/components/search/SearchBar.d.ts +1 -2
- package/dist/components/search/SearchBar.js +6 -1
- package/dist/components/search/criteria.d.ts +17 -1
- package/dist/components/search/index.d.ts +2 -1
- package/dist/components/search/index.js +1 -0
- package/dist/components/shell/Panes.d.ts +18 -0
- package/dist/components/shell/Panes.js +36 -0
- package/dist/components/shell/index.d.ts +1 -0
- package/dist/components/shell/index.js +2 -0
- package/dist/foundations/calendar.d.ts +3 -1
- package/dist/foundations/catalog.d.ts +27 -0
- package/dist/foundations/catalog.js +48 -0
- package/dist/foundations/copy.d.ts +33 -62
- package/dist/foundations/copy.es.js +6 -0
- package/dist/foundations/copy.js +21 -93
- package/dist/{primitives/field/context.d.ts → foundations/form.d.ts} +9 -0
- package/dist/{primitives/field/mode.js → foundations/form.js} +11 -0
- package/dist/foundations/grouping.d.ts +9 -3
- package/dist/foundations/index.d.ts +2 -0
- package/dist/foundations/index.js +5 -0
- package/dist/foundations/sticky.d.ts +4 -0
- package/dist/foundations/sticky.js +1 -0
- package/dist/primitives/choice/ComboBox.d.ts +1 -1
- package/dist/primitives/choice/ComboBox.js +4 -4
- package/dist/primitives/choice/RadioGroup.d.ts +1 -1
- package/dist/primitives/choice/RadioGroup.js +3 -3
- package/dist/primitives/choice/SearchField.js +1 -1
- package/dist/primitives/choice/SegmentedControl.d.ts +1 -1
- package/dist/primitives/choice/SegmentedControl.js +2 -2
- package/dist/primitives/choice/Slider.d.ts +1 -1
- package/dist/primitives/choice/Slider.js +2 -2
- package/dist/primitives/choice/Switch.d.ts +1 -1
- package/dist/primitives/choice/Switch.js +2 -2
- package/dist/primitives/date/Calendar.js +1 -1
- package/dist/primitives/date/DatePicker.d.ts +1 -1
- package/dist/primitives/date/DatePicker.js +2 -2
- package/dist/primitives/date/DateTime.d.ts +1 -1
- package/dist/primitives/date/DateTime.js +2 -2
- package/dist/primitives/field/Checks.d.ts +1 -1
- package/dist/primitives/field/Checks.js +4 -4
- package/dist/primitives/field/DropZone.d.ts +1 -1
- package/dist/primitives/field/DropZone.js +2 -2
- package/dist/primitives/field/Field.d.ts +2 -2
- package/dist/primitives/field/Field.js +3 -3
- package/dist/primitives/field/Suggest.d.ts +1 -1
- package/dist/primitives/field/Suggest.js +2 -2
- package/dist/primitives/field/index.d.ts +1 -3
- package/dist/primitives/field/index.js +4 -4
- package/dist/primitives/picker/Picker.d.ts +1 -1
- package/dist/primitives/picker/Picker.js +4 -4
- package/dist/primitives/{tag → picker}/TagsField.d.ts +2 -2
- package/dist/primitives/{tag → picker}/TagsField.js +5 -5
- package/dist/primitives/picker/index.d.ts +2 -0
- package/dist/primitives/picker/index.js +5 -1
- package/dist/primitives/table/CellCopy.js +1 -1
- package/dist/primitives/table/CellEditor.js +2 -4
- package/dist/primitives/table/DataTable.d.ts +11 -1
- package/dist/primitives/table/DataTable.js +137 -38
- package/dist/primitives/table/EditableTable.js +2 -2
- package/dist/primitives/table/PivotTable.js +0 -0
- package/dist/primitives/table/ReportTable.js +4 -3
- package/dist/primitives/table/SheetTable.js +8 -14
- package/dist/primitives/table/Table.d.ts +12 -3
- package/dist/primitives/table/Table.js +7 -7
- package/dist/primitives/table/rowWindow.d.ts +39 -0
- package/dist/primitives/table/rowWindow.js +177 -0
- package/dist/primitives/tabs/Tabs.d.ts +22 -4
- package/dist/primitives/tabs/Tabs.js +77 -23
- package/dist/primitives/tabs/index.d.ts +1 -1
- package/dist/primitives/tabs/index.js +1 -1
- package/dist/primitives/tag/Tags.js +4 -1
- package/dist/primitives/tag/index.d.ts +0 -2
- package/dist/primitives/tag/index.js +0 -3
- package/dist/primitives/value/Avatar.js +8 -1
- package/dist/primitives/value/{Fecha.d.ts → DateValue.d.ts} +5 -2
- package/dist/primitives/value/{Fecha.js → DateValue.js} +9 -8
- package/dist/primitives/value/Relation.d.ts +1 -1
- package/dist/primitives/value/Relation.js +2 -2
- package/dist/primitives/value/Value.js +4 -1
- package/dist/primitives/value/index.d.ts +1 -1
- package/dist/primitives/value/index.js +1 -1
- package/dist/views/list/List.d.ts +14 -1
- package/dist/views/list/List.js +28 -6
- package/dist/views/list/ListView.d.ts +1 -1
- package/dist/views/list/ListView.js +41 -14
- package/dist/views/list/index.d.ts +2 -2
- package/dist/views/list/presentations.js +3 -1
- package/dist/views/list/types.d.ts +49 -1
- package/dist/views/list/useNarrowList.d.ts +2 -2
- package/dist/views/list/useNarrowList.js +5 -5
- package/dist/views/record/Record.js +46 -42
- package/package.json +1 -1
- package/dist/primitives/field/context.js +0 -13
- package/dist/primitives/field/mode.d.ts +0 -9
- /package/dist/primitives/field/{copy.d.ts → clipboard.d.ts} +0 -0
- /package/dist/primitives/field/{copy.js → clipboard.js} +0 -0
- /package/dist/primitives/{tag/choices.d.ts → picker/tagChoices.d.ts} +0 -0
- /package/dist/primitives/{tag/choices.js → picker/tagChoices.js} +0 -0
|
@@ -50,7 +50,7 @@ function says(node) {
|
|
|
50
50
|
/** The label of a group band — its field, its value, how many rows it holds. The same
|
|
51
51
|
* in the table's band and the stacked list's. */
|
|
52
52
|
function pileTag(pile, locale) {
|
|
53
|
-
return (_jsxs("span", { className: "flex min-w-0 items-center gap-1.5", children: [_jsx("span", { className: "shrink-0 text-micro text-muted
|
|
53
|
+
return (_jsxs("span", { className: "flex min-w-0 items-center gap-1.5", children: [_jsx("span", { className: "shrink-0 text-micro text-muted", children: pile.field }), _jsx("span", { className: "truncate text-micro font-medium text-ink", children: pile.label }), _jsx("span", { className: "inline-flex shrink-0 items-center rounded-pill border border-line-strong bg-ink/5 px-2 py-0.5 text-micro tabular-nums text-muted", children: pile.count.toLocaleString(locale) })] }));
|
|
54
54
|
}
|
|
55
55
|
/** The focus ring of an operable row: drawn on the row, inset, so it is not cut by the
|
|
56
56
|
* box and does not move the cells. */
|
|
@@ -602,7 +602,7 @@ groupTotal, height, layout = "auto", onOpen, onDiscard, bands, adds, flag, onReo
|
|
|
602
602
|
return null;
|
|
603
603
|
return (_jsxs("div", { className: "flex items-baseline justify-between gap-3 text-meta", children: [_jsx("dt", { className: "text-muted", children: c.header }), _jsx("dd", { className: "font-semibold text-ink tabular-nums", children: total })] }, c.id));
|
|
604
604
|
}) }))] }));
|
|
605
|
-
return (_jsxs("div", { ref: frame, className: "min-w-0", children: [_jsx("div", { ref: box.ref, className: `min-w-0 ${box.className}`, style: box.style, children: stacked ? (list) : (_jsxs("table", { className: "w-full border-separate border-spacing-0 px-1 text-left", children: [_jsx("thead", { className: "sticky z-10 bg-ground", style: stickyTop, children: _jsxs("tr", { children: [reorderable ? _jsx("
|
|
605
|
+
return (_jsxs("div", { ref: frame, className: "min-w-0", children: [_jsx("div", { ref: box.ref, tabIndex: box.tabIndex, className: `min-w-0 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-primary ${box.className}`, style: box.style, children: stacked ? (list) : (_jsxs("table", { className: "w-full border-separate border-spacing-0 px-1 text-left", children: [_jsx("thead", { className: "sticky z-10 bg-ground", style: stickyTop, children: _jsxs("tr", { children: [reorderable ? _jsx("td", { className: "w-0 border-b border-line-strong" }) : null, columns.map((c) => c.header === "" ? (_jsx("td", { className: "w-0 border-b border-line-strong" }, c.id)) : (_jsx("th", { scope: "col", style: c.width === undefined ? undefined : { width: c.width }, className: `border-b border-line-strong ${HEADER_TEXT} ${cell} ${c.numeric === true ? "text-right" : "text-left"}`, children: c.header }, c.id))), onRemove === undefined ? null : (_jsx("td", { className: "w-0 border-b border-line-strong" }))] }) }), _jsx("tbody", { ...(reorderable
|
|
606
606
|
? {
|
|
607
607
|
onDragOver: overRow,
|
|
608
608
|
onDrop: (event) => {
|
|
Binary file
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
-
import {
|
|
3
|
+
import { fillCopy, useUiCopy } from "../../foundations/copy.js";
|
|
4
4
|
import { byDensity, useDensity } from "../../foundations/density.js";
|
|
5
5
|
import { stickyTop, useScrollBox } from "../../foundations/sticky.js";
|
|
6
6
|
import { HEADER_TEXT } from "./Table.js";
|
|
@@ -44,11 +44,12 @@ const SKIN = {
|
|
|
44
44
|
},
|
|
45
45
|
};
|
|
46
46
|
function Amount({ value, format }) {
|
|
47
|
+
const { locale } = useUiCopy();
|
|
47
48
|
if (value === null)
|
|
48
49
|
return _jsx("span", { className: "text-line-strong", children: "\u00B7" });
|
|
49
50
|
const zero = value === 0;
|
|
50
51
|
const negative = value < 0;
|
|
51
|
-
return (_jsx("span", { className: `tabular-nums ${zero ? "text-muted" : negative ? "text-bad" : "text-ink"}`, children: format === undefined ? amountFormat(
|
|
52
|
+
return (_jsx("span", { className: `tabular-nums ${zero ? "text-muted" : negative ? "text-bad" : "text-ink"}`, children: format === undefined ? amountFormat(locale).format(value) : format(value) }));
|
|
52
53
|
}
|
|
53
54
|
function ReportLine({ row, columns, open, onToggle, compact, }) {
|
|
54
55
|
const copy = useUiCopy();
|
|
@@ -103,5 +104,5 @@ defaultOpen, caption, height, }) {
|
|
|
103
104
|
desplazamiento de la barra y quedaba empujado 68 px hacia adentro de su
|
|
104
105
|
propia caja — banda en blanco arriba y títulos de columna cayendo entre
|
|
105
106
|
las filas. */
|
|
106
|
-
_jsx("div", { ref: caja.ref, className: `min-w-0 ${caja.className}`, style: caja.style, children: _jsxs("table", { className: "w-full border-collapse px-1", children: [caption === undefined ? null : (_jsx("caption", { className: `text-left text-meta text-muted ${byDensity(d, "px-3 pb-2", "px-2 pb-1.5")}`, children: caption })), _jsx("thead", { className: "sticky z-20 bg-ground", style: stickyTop, children: _jsxs("tr", { className: "border-b border-line-strong", children: [_jsx("th", { className: `sticky left-0 z-10 bg-ground text-left ${compact ? "px-2 py-1.5" : "px-3 py-2"}`, children: _jsx("span", { className: "sr-only", children: copy.report.concept }) }), columns.map((c) => (_jsx("th", { scope: "col", className: `text-right ${HEADER_TEXT} ${compact ? "px-2 py-1.5" : "px-3 py-2"}`, children: c.header }, c.id))), _jsx("
|
|
107
|
+
_jsx("div", { ref: caja.ref, tabIndex: caja.tabIndex, className: `min-w-0 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-primary ${caja.className}`, style: caja.style, children: _jsxs("table", { className: "w-full border-collapse px-1", children: [caption === undefined ? null : (_jsx("caption", { className: `text-left text-meta text-muted ${byDensity(d, "px-3 pb-2", "px-2 pb-1.5")}`, children: caption })), _jsx("thead", { className: "sticky z-20 bg-ground", style: stickyTop, children: _jsxs("tr", { className: "border-b border-line-strong", children: [_jsx("th", { className: `sticky left-0 z-10 bg-ground text-left ${compact ? "px-2 py-1.5" : "px-3 py-2"}`, children: _jsx("span", { className: "sr-only", children: copy.report.concept }) }), columns.map((c) => (_jsx("th", { scope: "col", className: `text-right ${HEADER_TEXT} ${compact ? "px-2 py-1.5" : "px-3 py-2"}`, children: c.header }, c.id))), _jsx("td", { className: "w-0" })] }) }), _jsx("tbody", { children: rows.map((r) => (_jsx(ReportLine, { row: r, columns: columns, open: open, onToggle: toggle, compact: compact }, r.id))) })] }) }));
|
|
107
108
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useUiCopy } from "../../foundations/copy.js";
|
|
3
|
+
import { fillCopy, useUiCopy } from "../../foundations/copy.js";
|
|
4
4
|
import { stickyTop, useScrollBox } from "../../foundations/sticky.js";
|
|
5
5
|
import { Cell } from "../value/Value.js";
|
|
6
6
|
import { useVirtualizer } from "@tanstack/react-virtual";
|
|
@@ -168,7 +168,7 @@ note, height = 420, rowLabel, band, onBandToggle, caption, onCommit, editable, o
|
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
170
|
}
|
|
171
|
-
return (_jsxs("div", { className: "flex min-w-0 flex-col", children: [caption === undefined ? null : (_jsx("div", { className: "px-3 pb-2 text-meta text-muted", children: caption })), _jsx("div", { ref: caja.ref, style: caja.style, onKeyDown: isEditable ? onKey : undefined, tabIndex: isEditable ? 0 :
|
|
171
|
+
return (_jsxs("div", { className: "flex min-w-0 flex-col", children: [caption === undefined ? null : (_jsx("div", { className: "px-3 pb-2 text-meta text-muted", children: caption })), _jsx("div", { ref: caja.ref, style: caja.style, onKeyDown: isEditable ? onKey : undefined, tabIndex: isEditable ? 0 : caja.tabIndex, className: `relative min-w-0 rounded-box border border-line bg-surface outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-primary ${caja.className}`, role: "grid", "aria-rowcount": rows, "aria-colcount": columns.length + 1, children: _jsxs("div", { style: {
|
|
172
172
|
minWidth: `${ROW_LABEL_WIDTH + columns.reduce((a, c) => a + (c.width ?? WIDTH), 0)}px`,
|
|
173
173
|
}, children: [_jsxs("div", { className: "sticky z-20 flex border-b border-line-strong bg-ground", style: stickyTop, role: "row", children: [_jsx("div", { className: "sticky left-0 z-30 shrink-0 border-r border-line bg-ground", style: { width: ROW_LABEL_WIDTH } }), columns.map((c) => (_jsxs("div", { role: "columnheader", style: { width: c.width ?? WIDTH }, className: `flex shrink-0 flex-col justify-center gap-0.5 border-r border-line px-2.5 py-1.5 ${c.numeric === true ? "items-end" : "items-start"}`, children: [_jsx("span", { className: "max-w-full truncate text-meta font-medium text-ink", title: c.header, children: c.header }), c.type === undefined ? null : (_jsx("span", { className: "max-w-full truncate font-mono text-micro text-muted", children: c.type }))] }, c.id))), _jsx("div", { className: "flex-1" })] }), _jsx("div", { style: { height: v.getTotalSize(), position: "relative" }, children: items.map((item) => {
|
|
174
174
|
const montón = band?.(item.index);
|
|
@@ -183,21 +183,15 @@ note, height = 420, rowLabel, band, onBandToggle, caption, onCommit, editable, o
|
|
|
183
183
|
que esa columna sume algo—. Antes era un rótulo suelto que
|
|
184
184
|
ignoraba las columnas, y entonces un total quedaba en un
|
|
185
185
|
rincón sin decir de qué columna era. */
|
|
186
|
-
_jsxs("div", { role: "row", "aria-rowindex": item.index + 1, className: "absolute top-0 left-0 flex w-full border-b border-line-strong bg-ground", style: { height: item.size, transform: `translateY(${item.start}px)` }, children: [_jsx("button", { type: "button", onClick: onBandToggle
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
/* El mismo ancho que la columna del número de fila, para
|
|
191
|
-
que el rótulo no corra la cuadrícula. */
|
|
192
|
-
width: ROW_LABEL_WIDTH,
|
|
193
|
-
...(montón.depth === undefined || montón.depth === 0
|
|
194
|
-
? {}
|
|
186
|
+
_jsxs("div", { role: "row", "aria-rowindex": item.index + 1, className: "absolute top-0 left-0 flex w-full border-b border-line-strong bg-ground", style: { height: item.size, transform: `translateY(${item.start}px)` }, children: [_jsx("div", { role: "gridcell", className: "sticky left-0 z-10 shrink-0 border-r border-line bg-ground", style: { height: "100%", width: ROW_LABEL_WIDTH }, children: onBandToggle === undefined ? null : (_jsx("button", { type: "button", onClick: () => onBandToggle(item.index), "aria-expanded": montón.open, "aria-label": fillCopy(montón.open ? copy.report.close : copy.report.open, {
|
|
187
|
+
name: montón.label,
|
|
188
|
+
}), className: "flex h-full w-full min-w-0 items-center gap-1.5 px-2.5 text-left focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-primary", style: montón.depth === undefined || montón.depth === 0
|
|
189
|
+
? undefined
|
|
195
190
|
: {
|
|
196
191
|
paddingLeft: `calc(var(--spacing-indent) * ${montón.depth} + 0.625rem)`,
|
|
197
|
-
}),
|
|
198
|
-
}, children: onBandToggle === undefined ? null : (_jsx(ChevronRight, { className: `size-(--size-icon-sm) shrink-0 text-muted transition-transform duration-(--duration-instant) ${montón.open ? "rotate-90" : ""}`, strokeWidth: 2, "aria-hidden": true })) }), columns.map((c, ci) => {
|
|
192
|
+
}, children: _jsx(ChevronRight, { className: `size-(--size-icon-sm) shrink-0 text-muted transition-transform duration-(--duration-instant) ${montón.open ? "rotate-90" : ""}`, strokeWidth: 2, "aria-hidden": true }) })) }), columns.map((c, ci) => {
|
|
199
193
|
const total = montón.total?.(c, ci) ?? null;
|
|
200
|
-
return (_jsx("div", { role: "gridcell", style: { width: c.width ?? WIDTH }, className: `flex shrink-0 items-center overflow-hidden border-r border-line px-2.5 whitespace-nowrap ${c.numeric === true ? "justify-end" : "justify-start"}`, children: ci === 0 ? (_jsxs("span", { className: "flex min-w-0 items-center gap-1.5", children: [montón.field === undefined ? null : (_jsx("span", { className: "shrink-0 text-micro text-muted
|
|
194
|
+
return (_jsx("div", { role: "gridcell", style: { width: c.width ?? WIDTH }, className: `flex shrink-0 items-center overflow-hidden border-r border-line px-2.5 whitespace-nowrap ${c.numeric === true ? "justify-end" : "justify-start"}`, children: ci === 0 ? (_jsxs("span", { className: "flex min-w-0 items-center gap-1.5", children: [montón.field === undefined ? null : (_jsx("span", { className: "shrink-0 text-micro text-muted", children: montón.field })), _jsx("span", { className: "truncate text-micro font-medium text-ink", children: montón.label }), _jsx("span", { className: "inline-flex shrink-0 items-center rounded-pill border border-line-strong bg-ink/5 px-2 py-0.5 text-micro tabular-nums text-muted", children: montón.count.toLocaleString(copy.locale) })] })) : total === null ? null : (_jsx("span", { className: "truncate text-meta font-medium tabular-nums text-ink", children: typeof total === "number"
|
|
201
195
|
? total.toLocaleString(copy.locale)
|
|
202
196
|
: total })) }, c.id));
|
|
203
197
|
})] }, item.key));
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Tone } from "../../foundations/types.js";
|
|
2
|
+
import type { WindowedRow } from "./rowWindow.js";
|
|
2
3
|
import { type ReactNode } from "react";
|
|
3
4
|
export type SortDirection = "asc" | "desc";
|
|
4
5
|
export interface SortState {
|
|
@@ -8,17 +9,23 @@ export interface SortState {
|
|
|
8
9
|
export declare function Table({ children,
|
|
9
10
|
/** With a height, the table scrolls on its own and the header sticks.
|
|
10
11
|
* Without a height, the page scrolls and the header goes away. */
|
|
11
|
-
height, }: {
|
|
12
|
+
height, rowCount, }: {
|
|
12
13
|
children: ReactNode;
|
|
13
14
|
/** A number means pixels; a string goes through as is, so `26rem` follows the
|
|
14
15
|
* system scale instead of pinning a size in pixels. */
|
|
15
16
|
height?: number | string;
|
|
17
|
+
/** How many rows the table REALLY has, header and footer included, when it draws only
|
|
18
|
+
* some of them (`useRowWindow`): a screen reader says «row 412 of 3 000» and not
|
|
19
|
+
* «row 12 of 40». Leave it out when every row is drawn. */
|
|
20
|
+
rowCount?: number;
|
|
16
21
|
}): import("react").JSX.Element;
|
|
17
22
|
/** The text of a column header, shared by every table of the family. Ink and semibold, not
|
|
18
23
|
* muted: in grey at the cells' weight the names of the columns read as one more row. */
|
|
19
24
|
export declare const HEADER_TEXT = "text-meta font-semibold whitespace-nowrap text-ink";
|
|
20
|
-
export declare function THead({ children }: {
|
|
25
|
+
export declare function THead({ children, rowIndex, }: {
|
|
21
26
|
children: ReactNode;
|
|
27
|
+
/** Its `aria-rowindex`, set only when the table says its `rowCount`. */
|
|
28
|
+
rowIndex?: number;
|
|
22
29
|
}): import("react").JSX.Element;
|
|
23
30
|
/** A column. If it gets a `sortKey`, it can be sorted.
|
|
24
31
|
*
|
|
@@ -42,10 +49,12 @@ export declare function Th({ children, sortKey, sort, onSort, numeric, align, wi
|
|
|
42
49
|
* system scale instead of pinning a size in pixels. */
|
|
43
50
|
width?: number | string;
|
|
44
51
|
}): import("react").JSX.Element;
|
|
45
|
-
export declare function Tr({ children, onClick, selected, }: {
|
|
52
|
+
export declare function Tr({ children, onClick, selected, windowed, }: {
|
|
46
53
|
children: ReactNode;
|
|
47
54
|
onClick?: () => void;
|
|
48
55
|
selected?: boolean;
|
|
56
|
+
/** Set by a table that draws only the rows in view (`useRowWindow`). */
|
|
57
|
+
windowed?: WindowedRow;
|
|
49
58
|
}): import("react").JSX.Element;
|
|
50
59
|
/**
|
|
51
60
|
* A row that opens its own detail underneath.
|
|
@@ -6,25 +6,25 @@ import { createContext, useContext, useId } from "react";
|
|
|
6
6
|
export function Table({ children,
|
|
7
7
|
/** With a height, the table scrolls on its own and the header sticks.
|
|
8
8
|
* Without a height, the page scrolls and the header goes away. */
|
|
9
|
-
height, }) {
|
|
9
|
+
height, rowCount, }) {
|
|
10
10
|
const caja = useScrollBox(height === undefined ? {} : { height });
|
|
11
11
|
return (
|
|
12
12
|
/* Cuándo esta caja es un contenedor de scroll y cuándo no lo decide
|
|
13
13
|
`useScrollBox`: ahí vive el dilema entero y ahí vive su explicación. Las
|
|
14
14
|
tres tablas del sistema lo resolvían con tres copias de la misma
|
|
15
15
|
medición. */
|
|
16
|
-
_jsx("div", { ref: caja.ref, className: `min-w-0 ${caja.className}`, style: caja.style, children: _jsx("table", { className: "w-full border-separate border-spacing-0 px-1 text-left", children: children }) }));
|
|
16
|
+
_jsx("div", { ref: caja.ref, tabIndex: caja.tabIndex, className: `min-w-0 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-primary ${caja.className}`, style: caja.style, children: _jsx("table", { "aria-rowcount": rowCount, className: "w-full border-separate border-spacing-0 px-1 text-left", children: children }) }));
|
|
17
17
|
}
|
|
18
18
|
/** The text of a column header, shared by every table of the family. Ink and semibold, not
|
|
19
19
|
* muted: in grey at the cells' weight the names of the columns read as one more row. */
|
|
20
20
|
export const HEADER_TEXT = "text-meta font-semibold whitespace-nowrap text-ink";
|
|
21
|
-
export function THead({ children }) {
|
|
21
|
+
export function THead({ children, rowIndex, }) {
|
|
22
22
|
return (
|
|
23
23
|
/* Se pega DEBAJO de lo que ya esté pegado —la barra de la aplicación y la
|
|
24
24
|
de la lista— y no en `top: 0`, que lo metía atrás de las dos. Sin alto en
|
|
25
25
|
`Table` el que scrollea es la página, y ahí la diferencia es justamente
|
|
26
26
|
la que hacía desaparecer los nombres de columna. Ver `system/sticky.tsx`. */
|
|
27
|
-
_jsx("thead", { className: "sticky z-10 bg-ground", style: stickyTop, children: _jsx("tr", { children: children }) }));
|
|
27
|
+
_jsx("thead", { className: "sticky z-10 bg-ground", style: stickyTop, children: _jsx("tr", { "aria-rowindex": rowIndex, children: children }) }));
|
|
28
28
|
}
|
|
29
29
|
/** A column. If it gets a `sortKey`, it can be sorted.
|
|
30
30
|
*
|
|
@@ -59,8 +59,8 @@ const ROW_ON = "bg-primary/8";
|
|
|
59
59
|
function rowWash({ selected, interactive }) {
|
|
60
60
|
return selected ? ROW_ON : interactive ? ROW_WASH : "";
|
|
61
61
|
}
|
|
62
|
-
export function Tr({ children, onClick, selected = false, }) {
|
|
63
|
-
return (_jsx(RowMark.Provider, { value: { selected, interactive: onClick !== undefined }, children: _jsx("tr", { onClick: onClick, className: `group/row ${onClick === undefined ? "" : "cursor-pointer"}`, children: children }) }));
|
|
62
|
+
export function Tr({ children, onClick, selected = false, windowed, }) {
|
|
63
|
+
return (_jsx(RowMark.Provider, { value: { selected, interactive: onClick !== undefined }, children: _jsx("tr", { ...windowed, onClick: onClick, className: `group/row ${onClick === undefined ? "" : "cursor-pointer"}`, children: children }) }));
|
|
64
64
|
}
|
|
65
65
|
/**
|
|
66
66
|
* A row that opens its own detail underneath.
|
|
@@ -110,7 +110,7 @@ mono = false,
|
|
|
110
110
|
* gets lost. */
|
|
111
111
|
tone, title, }) {
|
|
112
112
|
const mark = useContext(RowMark);
|
|
113
|
-
return (_jsx("td", { title: title, className: `border-b border-line px-3 py-1.5 text-meta text-ink first:rounded-l-control last:rounded-r-control ${(align ?? (numeric ? "right" : "left")) === "right" ? "text-right" : "text-left"} ${mono ? "font-mono tabular-nums" : numeric ? "tabular-nums" : ""} ${tone === undefined ? rowWash(mark) : CELL_TONE[tone]}`, children: children }));
|
|
113
|
+
return (_jsx("td", { title: title, className: `border-b border-line px-3 py-1.5 text-meta text-ink first:rounded-l-control last:rounded-r-control ${(align ?? (numeric ? "right" : "left")) === "right" ? "text-right" : "text-left"} ${mono ? "font-mono tabular-nums" : numeric ? "tabular-nums whitespace-nowrap" : ""} ${tone === undefined ? rowWash(mark) : CELL_TONE[tone]}`, children: children }));
|
|
114
114
|
}
|
|
115
115
|
/** The selection checkbox, with its third state.
|
|
116
116
|
*
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type ReactNode, type RefObject } from "react";
|
|
2
|
+
/** From how many rows the table draws only the ones in view. */
|
|
3
|
+
export declare const WINDOW_FROM = 200;
|
|
4
|
+
/** What a drawn row has to carry: spread on its `<tr>`. */
|
|
5
|
+
export type WindowedRow = {
|
|
6
|
+
ref: (node: HTMLTableRowElement | null) => void;
|
|
7
|
+
"data-index": number;
|
|
8
|
+
"aria-rowindex": number;
|
|
9
|
+
onFocus: () => void;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Which rows of `count` to draw. Spread `body` on the `<tbody>`, draw `rows` in the
|
|
13
|
+
* order given — `windowed(i)` on each one's `<tr>` — with `pad(...)` between them, and
|
|
14
|
+
* pass `rowCount` to the table.
|
|
15
|
+
*
|
|
16
|
+
* `rowsBefore` is how many rows the table has above the body (the header), so the
|
|
17
|
+
* `aria-rowindex` of the rows counts from there.
|
|
18
|
+
*/
|
|
19
|
+
export declare function useRowWindow({ count, rowsBefore, getKey, }: {
|
|
20
|
+
count: number;
|
|
21
|
+
rowsBefore?: number;
|
|
22
|
+
getKey: (index: number) => string;
|
|
23
|
+
}): {
|
|
24
|
+
on: boolean;
|
|
25
|
+
body: {
|
|
26
|
+
ref: RefObject<HTMLTableSectionElement | null>;
|
|
27
|
+
};
|
|
28
|
+
/** The indices to draw, in order. All of them when the window is off. */
|
|
29
|
+
rows: readonly {
|
|
30
|
+
index: number;
|
|
31
|
+
start: number;
|
|
32
|
+
end: number;
|
|
33
|
+
}[];
|
|
34
|
+
windowed: (index: number) => WindowedRow | undefined;
|
|
35
|
+
/** The spacer that stands for the rows not drawn before `start`. */
|
|
36
|
+
pad: (from: number, to: number, columns: number, key: string) => ReactNode;
|
|
37
|
+
/** The spacer after the last drawn row. */
|
|
38
|
+
tail: (columns: number) => ReactNode;
|
|
39
|
+
};
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useVirtualizer, useWindowVirtualizer } from "@tanstack/react-virtual";
|
|
4
|
+
import { useLayoutEffect, useRef, useState } from "react";
|
|
5
|
+
/* ==================================================================== *
|
|
6
|
+
* ROW WINDOW — the rows of a `<table>` that are worth drawing.
|
|
7
|
+
*
|
|
8
|
+
* Context: `DataTable` drew every row it got. A page of eighty is nothing; a
|
|
9
|
+
* «load more» that reached three thousand was three thousand rows of cells,
|
|
10
|
+
* copy buttons and editors, and the tab froze. web-framework put a ceiling on
|
|
11
|
+
* «load more» and kept the pages BECAUSE of it. This is what lifts the ceiling.
|
|
12
|
+
*
|
|
13
|
+
* ── It stays a `<table>` ────────────────────────────────────────────
|
|
14
|
+
*
|
|
15
|
+
* `SheetTable` positions `<div>` rows absolutely. A list cannot: its columns
|
|
16
|
+
* size themselves from the content, its header sticks with `position: sticky`
|
|
17
|
+
* and its rows carry the row semantics a screen reader walks. So the rows that
|
|
18
|
+
* are NOT drawn become one spacer row above and one below — real height, no
|
|
19
|
+
* content, `aria-hidden` —, and the table says how many rows it really has
|
|
20
|
+
* (`aria-rowcount`) and where each drawn one sits (`aria-rowindex`).
|
|
21
|
+
*
|
|
22
|
+
* ── The scroller is found, not declared ─────────────────────────────
|
|
23
|
+
*
|
|
24
|
+
* A list inside `AppFrame` scrolls with the PAGE — that is what keeps the
|
|
25
|
+
* application bar, the list bar and the header stuck one under the other. The
|
|
26
|
+
* same list inside a composer panel or a `Peek` scrolls inside THAT box, and a
|
|
27
|
+
* `DataTable` with `height` scrolls inside its own. The nearest ancestor that
|
|
28
|
+
* really scrolls vertically is the one; none means the page. Giving the table a
|
|
29
|
+
* scroll box of its own instead would break the sticky stack — the trap
|
|
30
|
+
* AGENTS.md writes down for `overflow-hidden`.
|
|
31
|
+
*
|
|
32
|
+
* ── Only past a size ────────────────────────────────────────────────
|
|
33
|
+
*
|
|
34
|
+
* Under `WINDOW_FROM` rows everything is drawn, as before: the browser's find,
|
|
35
|
+
* printing and a row that is always there are worth more than the few
|
|
36
|
+
* milliseconds saved on a page of eighty. Over it, the find of the browser only
|
|
37
|
+
* sees what is drawn — the search bar is how a set that large is searched.
|
|
38
|
+
*
|
|
39
|
+
* ── What has focus stays drawn ──────────────────────────────────────
|
|
40
|
+
*
|
|
41
|
+
* A cell being edited, a copy button just pressed: if its row scrolled out and
|
|
42
|
+
* got unmounted, the focus would fall to `<body>` and the popover with it. The
|
|
43
|
+
* last row that took focus is always kept in the window, wherever it is.
|
|
44
|
+
* Tabbing forward works without help: the next row is in the overscan, the
|
|
45
|
+
* browser scrolls it into view, and the window follows.
|
|
46
|
+
* ==================================================================== */
|
|
47
|
+
/** From how many rows the table draws only the ones in view. */
|
|
48
|
+
export const WINDOW_FROM = 200;
|
|
49
|
+
/** A first guess of a row's height; each drawn row is measured after. */
|
|
50
|
+
const ESTIMATE = 36;
|
|
51
|
+
/** Rows drawn beyond each edge of the view. */
|
|
52
|
+
const OVERSCAN = 10;
|
|
53
|
+
/** Rows drawn before the scroller is known — the first frame only. */
|
|
54
|
+
const FIRST_FRAME = 40;
|
|
55
|
+
/** The nearest ancestor that scrolls vertically, or the page. */
|
|
56
|
+
function scrollerOf(node) {
|
|
57
|
+
for (let el = node.parentElement; el !== null; el = el.parentElement) {
|
|
58
|
+
if (el === document.body || el === document.documentElement)
|
|
59
|
+
break;
|
|
60
|
+
const { overflowY } = getComputedStyle(el);
|
|
61
|
+
const scrolls = overflowY === "auto" || overflowY === "scroll" || overflowY === "overlay";
|
|
62
|
+
/* A box that is `overflow-auto` only because the table is WIDER than it grows with
|
|
63
|
+
its content and never scrolls down: it is not the one. */
|
|
64
|
+
if (scrolls && el.scrollHeight > el.clientHeight + 1)
|
|
65
|
+
return el;
|
|
66
|
+
}
|
|
67
|
+
return "page";
|
|
68
|
+
}
|
|
69
|
+
/** Where the body starts inside the scroller's content — what the window counts from. */
|
|
70
|
+
function offsetIn(body, scroller) {
|
|
71
|
+
const top = body.getBoundingClientRect().top;
|
|
72
|
+
if (scroller === "page")
|
|
73
|
+
return top + window.scrollY;
|
|
74
|
+
return top - scroller.getBoundingClientRect().top + scroller.scrollTop;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Which rows of `count` to draw. Spread `body` on the `<tbody>`, draw `rows` in the
|
|
78
|
+
* order given — `windowed(i)` on each one's `<tr>` — with `pad(...)` between them, and
|
|
79
|
+
* pass `rowCount` to the table.
|
|
80
|
+
*
|
|
81
|
+
* `rowsBefore` is how many rows the table has above the body (the header), so the
|
|
82
|
+
* `aria-rowindex` of the rows counts from there.
|
|
83
|
+
*/
|
|
84
|
+
export function useRowWindow({ count, rowsBefore = 1, getKey, }) {
|
|
85
|
+
const on = count >= WINDOW_FROM;
|
|
86
|
+
const body = useRef(null);
|
|
87
|
+
const [scroller, setScroller] = useState(null);
|
|
88
|
+
const [margin, setMargin] = useState(0);
|
|
89
|
+
const pinned = useRef(null);
|
|
90
|
+
/* Every render, before paint: which box scrolls and where the body starts in it. The
|
|
91
|
+
window re-renders on scroll, so what moves above the table — metrics that loaded,
|
|
92
|
+
chips that wrapped — is caught on the next one. Set only on a real change: each set
|
|
93
|
+
is one more render. No dependency list on purpose: what it measures is the layout,
|
|
94
|
+
which no prop of this hook describes; it settles because it sets only on a change. */
|
|
95
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
96
|
+
useLayoutEffect(() => {
|
|
97
|
+
const node = body.current;
|
|
98
|
+
if (!on || node === null)
|
|
99
|
+
return;
|
|
100
|
+
const found = scrollerOf(node);
|
|
101
|
+
if (found !== scroller)
|
|
102
|
+
setScroller(found);
|
|
103
|
+
const at = offsetIn(node, found);
|
|
104
|
+
if (Math.abs(at - margin) > 1)
|
|
105
|
+
setMargin(at);
|
|
106
|
+
});
|
|
107
|
+
const options = {
|
|
108
|
+
count,
|
|
109
|
+
estimateSize: () => ESTIMATE,
|
|
110
|
+
overscan: OVERSCAN,
|
|
111
|
+
scrollMargin: margin,
|
|
112
|
+
getItemKey: getKey,
|
|
113
|
+
rangeExtractor: (range) => {
|
|
114
|
+
const first = Math.max(0, range.startIndex - OVERSCAN);
|
|
115
|
+
const last = Math.min(count - 1, range.endIndex + OVERSCAN);
|
|
116
|
+
const out = [];
|
|
117
|
+
for (let i = first; i <= last; i += 1)
|
|
118
|
+
out.push(i);
|
|
119
|
+
const keep = pinned.current;
|
|
120
|
+
if (keep !== null && keep < count && (keep < first || keep > last)) {
|
|
121
|
+
out.push(keep);
|
|
122
|
+
out.sort((a, b) => a - b);
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
const inBox = useVirtualizer({
|
|
128
|
+
...options,
|
|
129
|
+
enabled: on && scroller !== null && scroller !== "page",
|
|
130
|
+
getScrollElement: () => (scroller === null || scroller === "page" ? null : scroller),
|
|
131
|
+
});
|
|
132
|
+
const inPage = useWindowVirtualizer({
|
|
133
|
+
...options,
|
|
134
|
+
enabled: on && scroller === "page",
|
|
135
|
+
});
|
|
136
|
+
const v = scroller === "page" ? inPage : inBox;
|
|
137
|
+
const all = () => Array.from({ length: count }, (_, index) => ({
|
|
138
|
+
index,
|
|
139
|
+
start: index * ESTIMATE,
|
|
140
|
+
end: (index + 1) * ESTIMATE,
|
|
141
|
+
}));
|
|
142
|
+
/* The positions come back in the scroller's coordinates; the spacers live inside the
|
|
143
|
+
body, so they are counted from where it starts. */
|
|
144
|
+
const rows = !on
|
|
145
|
+
? all()
|
|
146
|
+
: scroller === null
|
|
147
|
+
? all().slice(0, FIRST_FRAME)
|
|
148
|
+
: v.getVirtualItems().map((item) => ({
|
|
149
|
+
index: item.index,
|
|
150
|
+
start: item.start - margin,
|
|
151
|
+
end: item.end - margin,
|
|
152
|
+
}));
|
|
153
|
+
const total = !on ? 0 : scroller === null ? count * ESTIMATE : v.getTotalSize();
|
|
154
|
+
const spacer = (height, columns, key) => height <= 0 ? null : (_jsx("tr", { "aria-hidden": true, style: { height }, children: _jsx("td", { colSpan: columns, className: "p-0" }) }, key));
|
|
155
|
+
return {
|
|
156
|
+
on,
|
|
157
|
+
body: { ref: body },
|
|
158
|
+
rows,
|
|
159
|
+
windowed: (index) => !on
|
|
160
|
+
? undefined
|
|
161
|
+
: {
|
|
162
|
+
ref: scroller === null ? () => { } : v.measureElement,
|
|
163
|
+
"data-index": index,
|
|
164
|
+
"aria-rowindex": index + rowsBefore + 1,
|
|
165
|
+
onFocus: () => {
|
|
166
|
+
pinned.current = index;
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
pad: (from, to, columns, key) => (on ? spacer(to - from, columns, key) : null),
|
|
170
|
+
tail: (columns) => {
|
|
171
|
+
if (!on)
|
|
172
|
+
return null;
|
|
173
|
+
const end = rows.length === 0 ? 0 : rows[rows.length - 1].end;
|
|
174
|
+
return spacer(total - end, columns, "\u0000tail");
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Tone } from "../../foundations/types.js";
|
|
2
2
|
import type { LucideIcon } from "lucide-react";
|
|
3
|
+
import { type ReactNode } from "react";
|
|
3
4
|
/**
|
|
4
5
|
* UNA TIRA DE PESTAÑAS. La misma en todo el sistema.
|
|
5
6
|
*
|
|
@@ -76,11 +77,28 @@ export type TabItem = {
|
|
|
76
77
|
notice?: TabNotice;
|
|
77
78
|
};
|
|
78
79
|
/**
|
|
79
|
-
*
|
|
80
|
+
* A notebook whose strip and page are far apart — the record's tabs ride in its header and
|
|
81
|
+
* the page is drawn in its paper. It only shares an id: no box, no context of anything
|
|
82
|
+
* else, so it can wrap a whole record without touching what is inside.
|
|
80
83
|
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
|
|
84
|
+
* Context: it was React Aria's `Tabs`, and that one walks its whole subtree to collect its
|
|
85
|
+
* items — wrapped around a record it collected the breadcrumbs too, and broke them.
|
|
86
|
+
*/
|
|
87
|
+
export declare function TabsScope({ children }: {
|
|
88
|
+
children: ReactNode;
|
|
89
|
+
}): import("react").JSX.Element;
|
|
90
|
+
/** The page of the open tab, inside a `TabsScope`. `id` is the tab's. */
|
|
91
|
+
export declare function TabsPanel({ id, className, children, }: {
|
|
92
|
+
id: string;
|
|
93
|
+
className?: string;
|
|
94
|
+
children: ReactNode;
|
|
95
|
+
}): import("react").JSX.Element;
|
|
96
|
+
/**
|
|
97
|
+
* The strip. Inside a `TabsScope` each tab points at its panel; alone it is a strip whose
|
|
98
|
+
* page the caller draws.
|
|
99
|
+
*
|
|
100
|
+
* The keyboard is the ARIA tabs pattern with automatic activation: one tab stop (the open
|
|
101
|
+
* tab), ← / → move AND open (wrapping), Home / End go to the ends.
|
|
84
102
|
*/
|
|
85
103
|
export declare function Tabs({ tabs, current, onSelect, label, }: {
|
|
86
104
|
tabs: readonly TabItem[];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useUiCopy } from "../../foundations/copy.js";
|
|
4
|
-
import {
|
|
4
|
+
import { createContext, useContext, useId, useRef, } from "react";
|
|
5
5
|
/* El color de la notificación. Estático y en una tabla porque Tailwind no puede
|
|
6
6
|
componer una clase con un pedazo variable: `bg-${tone}/12` no existe en la
|
|
7
7
|
hoja. */
|
|
@@ -28,29 +28,83 @@ const NOTICE_DOT = {
|
|
|
28
28
|
* arrows move between tabs and select them, Home/End jump to the ends, and only the open
|
|
29
29
|
* tab is in the Tab order. The hand-written version had the roles and none of the keys.
|
|
30
30
|
*/
|
|
31
|
+
/* Set by `TabsScope`: the strip and its page share an id, so each tab can say which panel
|
|
32
|
+
it controls and the panel which tab names it. */
|
|
33
|
+
const Scope = createContext(null);
|
|
34
|
+
const tabIdOf = (base, id) => `${base}-tab-${id}`;
|
|
35
|
+
const panelIdOf = (base, id) => `${base}-panel-${id}`;
|
|
36
|
+
/**
|
|
37
|
+
* A notebook whose strip and page are far apart — the record's tabs ride in its header and
|
|
38
|
+
* the page is drawn in its paper. It only shares an id: no box, no context of anything
|
|
39
|
+
* else, so it can wrap a whole record without touching what is inside.
|
|
40
|
+
*
|
|
41
|
+
* Context: it was React Aria's `Tabs`, and that one walks its whole subtree to collect its
|
|
42
|
+
* items — wrapped around a record it collected the breadcrumbs too, and broke them.
|
|
43
|
+
*/
|
|
44
|
+
export function TabsScope({ children }) {
|
|
45
|
+
return _jsx(Scope, { value: useId(), children: children });
|
|
46
|
+
}
|
|
47
|
+
/** The page of the open tab, inside a `TabsScope`. `id` is the tab's. */
|
|
48
|
+
export function TabsPanel({ id, className, children, }) {
|
|
49
|
+
const base = useContext(Scope);
|
|
50
|
+
return (_jsx("div", { role: "tabpanel", ...(base === null
|
|
51
|
+
? {}
|
|
52
|
+
: { id: panelIdOf(base, id), "aria-labelledby": tabIdOf(base, id) }),
|
|
53
|
+
/* Reachable on its own (the ARIA pattern), with the ring every focus stop has. */
|
|
54
|
+
tabIndex: 0, className: `focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary ${className ?? ""}`, children: children }));
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The strip. Inside a `TabsScope` each tab points at its panel; alone it is a strip whose
|
|
58
|
+
* page the caller draws.
|
|
59
|
+
*
|
|
60
|
+
* The keyboard is the ARIA tabs pattern with automatic activation: one tab stop (the open
|
|
61
|
+
* tab), ← / → move AND open (wrapping), Home / End go to the ends.
|
|
62
|
+
*/
|
|
31
63
|
export function Tabs({ tabs, current, onSelect, label, }) {
|
|
32
64
|
const copy = useUiCopy();
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
65
|
+
const base = useContext(Scope);
|
|
66
|
+
const strip = useRef(null);
|
|
67
|
+
const go = (to) => {
|
|
68
|
+
const next = tabs[(to + tabs.length) % tabs.length];
|
|
69
|
+
if (next === undefined)
|
|
70
|
+
return;
|
|
71
|
+
onSelect?.(next.id);
|
|
72
|
+
strip.current?.querySelector(`[data-tab="${CSS.escape(next.id)}"]`)?.focus();
|
|
73
|
+
};
|
|
74
|
+
const onKeyDown = (event) => {
|
|
75
|
+
const at = tabs.findIndex((one) => one.id === current);
|
|
76
|
+
const to = event.key === "ArrowRight"
|
|
77
|
+
? at + 1
|
|
78
|
+
: event.key === "ArrowLeft"
|
|
79
|
+
? at - 1
|
|
80
|
+
: event.key === "Home"
|
|
81
|
+
? 0
|
|
82
|
+
: event.key === "End"
|
|
83
|
+
? tabs.length - 1
|
|
84
|
+
: null;
|
|
85
|
+
if (to === null)
|
|
86
|
+
return;
|
|
87
|
+
event.preventDefault();
|
|
88
|
+
go(to);
|
|
89
|
+
};
|
|
90
|
+
return (_jsx("div", { ref: strip, role: "tablist", "aria-label": label ?? copy.record.faces, onKeyDown: onKeyDown, className: "flex min-w-0 flex-wrap items-end gap-1", children: tabs.map((a) => {
|
|
91
|
+
const Icon = a.icon;
|
|
92
|
+
const empty = a.count === 0;
|
|
93
|
+
/* Un cero no pide nada, así que no se dibuja. Es la diferencia con el
|
|
94
|
+
contador, donde «0» es una respuesta. */
|
|
95
|
+
const notice = a.notice?.count === 0 ? undefined : a.notice;
|
|
96
|
+
const isSelected = a.id === current;
|
|
97
|
+
return (_jsx("button", { type: "button", role: "tab", "data-tab": a.id, "aria-selected": isSelected, tabIndex: isSelected ? 0 : -1, ...(base === null
|
|
98
|
+
? {}
|
|
99
|
+
: { id: tabIdOf(base, a.id), "aria-controls": panelIdOf(base, a.id) }), onClick: () => onSelect?.(a.id), className: `inline-flex shrink-0 cursor-default items-center px-3 py-2 text-meta outline-0 transition-colors duration-(--duration-instant) focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-primary ${isSelected
|
|
100
|
+
? "-mb-px rounded-t-control border border-b-0 border-line-strong bg-surface font-medium text-ink"
|
|
101
|
+
: "border border-transparent text-muted hover:text-ink"}`, children: _jsxs("span", { className: "inline-flex items-center gap-1.5", ...(notice === undefined ? {} : { title: notice.label }), children: [Icon === undefined ? null : (_jsx(Icon, { className: `size-(--size-icon-sm) shrink-0 ${isSelected ? "text-ink" : "text-muted"}`, strokeWidth: 2, "aria-hidden": true })), a.label, a.count === undefined ? null : (_jsx("span", { className: `rounded-pill px-1.5 text-micro tabular-nums ${empty
|
|
102
|
+
? "bg-ink/5 text-muted"
|
|
103
|
+
: isSelected
|
|
104
|
+
? "bg-ink/8 text-ink"
|
|
105
|
+
: "bg-ink/8 text-muted"}`, children: a.count.toLocaleString(copy.locale) })), notice === undefined ? null : (_jsx("span", { className: "sr-only", children: ` · ${notice.label}` })), notice === undefined ? null : notice.count === undefined ? (
|
|
106
|
+
/* Sin número, un punto. La palabra no se dibuja pero está: en el
|
|
53
107
|
`title` y en el nombre accesible de la pestaña. */
|
|
54
|
-
|
|
55
|
-
|
|
108
|
+
_jsx("span", { className: `size-1.5 shrink-0 rounded-pill ${NOTICE_DOT[notice.tone ?? "bad"]}`, "aria-hidden": true })) : (_jsx("span", { className: `rounded-pill px-1.5 text-micro font-medium tabular-nums ${NOTICE_TONE[notice.tone ?? "bad"]}`, "aria-hidden": true, children: notice.count.toLocaleString(copy.locale) }))] }) }, a.id));
|
|
109
|
+
}) }));
|
|
56
110
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { type TabItem, type TabNotice, Tabs } from "./Tabs.js";
|
|
1
|
+
export { type TabItem, type TabNotice, Tabs, TabsPanel, TabsScope, } from "./Tabs.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { Tabs } from "./Tabs.js";
|
|
1
|
+
export { Tabs, TabsPanel, TabsScope, } from "./Tabs.js";
|
|
@@ -96,7 +96,10 @@ function shell({ item, surface, stack, removable, pressable, }) {
|
|
|
96
96
|
}
|
|
97
97
|
function Inside({ item }) {
|
|
98
98
|
const Icon = item.icon;
|
|
99
|
-
return (_jsxs(_Fragment, { children: [Icon === undefined ? null : (_jsx(Icon, { className: "size-3 shrink-0 opacity-70", strokeWidth: 2, "aria-hidden": true })), item.avatar === undefined ? null : (_jsx("span", { "aria-hidden": true, className: "-my-0.5 -ml-1.5 flex shrink-0", children: _jsx(Avatar, { name: item.label, size: "sm", ...(typeof item.avatar === "string" ? { src: item.avatar } : {}) }) })), _jsx("span", { className: "min-w-0 truncate", children: item.label }), item.note === undefined ? null : (
|
|
99
|
+
return (_jsxs(_Fragment, { children: [Icon === undefined ? null : (_jsx(Icon, { className: "size-3 shrink-0 opacity-70", strokeWidth: 2, "aria-hidden": true })), item.avatar === undefined ? null : (_jsx("span", { "aria-hidden": true, className: "-my-0.5 -ml-1.5 flex shrink-0", children: _jsx(Avatar, { name: item.label, size: "sm", ...(typeof item.avatar === "string" ? { src: item.avatar } : {}) }) })), _jsx("span", { className: "min-w-0 truncate", children: item.label }), item.note === undefined ? null : (
|
|
100
|
+
/* Secondary by its separator and weight, not by fading: at 60 % the note fell
|
|
101
|
+
under 4.5:1 on a tinted chip. */
|
|
102
|
+
_jsxs("span", { className: "shrink-0 font-normal", children: ["\u00B7 ", item.note] }))] }));
|
|
100
103
|
}
|
|
101
104
|
/* The × is React Aria's `remove` slot: it removes THIS chip and never presses it, so
|
|
102
105
|
removing a pressable chip does not also choose it. */
|
|
@@ -3,6 +3,3 @@ export { StatusDot, Tag } from "./Tag.js";
|
|
|
3
3
|
/* La tira de fichas: la misma en la barra, en el desplegable y en cualquier
|
|
4
4
|
lugar donde se dibuje una lista de palabras cortas. */
|
|
5
5
|
export { Tags } from "./Tags.js";
|
|
6
|
-
/* El many2many: las fichas elegidas más una entrada que agrega la siguiente. */
|
|
7
|
-
export {} from "./choices.js";
|
|
8
|
-
export { TagsField } from "./TagsField.js";
|