@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.
Files changed (121) hide show
  1. package/dist/components/activity/Activity.js +1 -1
  2. package/dist/components/activity/Messages.d.ts +4 -2
  3. package/dist/components/activity/Messages.js +4 -4
  4. package/dist/components/activity/Timeline.js +1 -1
  5. package/dist/components/assist/AssistMark.js +1 -1
  6. package/dist/components/assist/AssistSuggestion.js +1 -1
  7. package/dist/components/assist/ChangeReview.js +2 -2
  8. package/dist/components/calendar/RecordCalendar.js +4 -1
  9. package/dist/components/form/Sheet.js +1 -1
  10. package/dist/components/lines/LineItems.d.ts +1 -1
  11. package/dist/components/lines/LineItems.js +2 -2
  12. package/dist/components/listing/ListingBar.js +1 -1
  13. package/dist/components/listing/Row.d.ts +7 -0
  14. package/dist/components/listing/Row.js +7 -1
  15. package/dist/components/listing/ViewSwitcher.d.ts +7 -2
  16. package/dist/components/listing/index.d.ts +1 -1
  17. package/dist/components/listing/set.d.ts +4 -1
  18. package/dist/components/record/Breadcrumb.js +5 -1
  19. package/dist/components/search/CriteriaBuilder.d.ts +1 -1
  20. package/dist/components/search/CriteriaBuilder.js +3 -3
  21. package/dist/components/search/FilterBuilder.d.ts +1 -18
  22. package/dist/components/search/FilterBuilder.js +4 -4
  23. package/dist/components/search/JsonEditor.js +5 -1
  24. package/dist/components/search/SearchBar.d.ts +1 -2
  25. package/dist/components/search/SearchBar.js +6 -1
  26. package/dist/components/search/criteria.d.ts +17 -1
  27. package/dist/components/search/index.d.ts +2 -1
  28. package/dist/components/search/index.js +1 -0
  29. package/dist/components/shell/Panes.d.ts +18 -0
  30. package/dist/components/shell/Panes.js +36 -0
  31. package/dist/components/shell/index.d.ts +1 -0
  32. package/dist/components/shell/index.js +2 -0
  33. package/dist/foundations/calendar.d.ts +3 -1
  34. package/dist/foundations/catalog.d.ts +27 -0
  35. package/dist/foundations/catalog.js +48 -0
  36. package/dist/foundations/copy.d.ts +33 -62
  37. package/dist/foundations/copy.es.js +6 -0
  38. package/dist/foundations/copy.js +21 -93
  39. package/dist/{primitives/field/context.d.ts → foundations/form.d.ts} +9 -0
  40. package/dist/{primitives/field/mode.js → foundations/form.js} +11 -0
  41. package/dist/foundations/grouping.d.ts +9 -3
  42. package/dist/foundations/index.d.ts +2 -0
  43. package/dist/foundations/index.js +5 -0
  44. package/dist/foundations/sticky.d.ts +4 -0
  45. package/dist/foundations/sticky.js +1 -0
  46. package/dist/primitives/choice/ComboBox.d.ts +1 -1
  47. package/dist/primitives/choice/ComboBox.js +4 -4
  48. package/dist/primitives/choice/RadioGroup.d.ts +1 -1
  49. package/dist/primitives/choice/RadioGroup.js +3 -3
  50. package/dist/primitives/choice/SearchField.js +1 -1
  51. package/dist/primitives/choice/SegmentedControl.d.ts +1 -1
  52. package/dist/primitives/choice/SegmentedControl.js +2 -2
  53. package/dist/primitives/choice/Slider.d.ts +1 -1
  54. package/dist/primitives/choice/Slider.js +2 -2
  55. package/dist/primitives/choice/Switch.d.ts +1 -1
  56. package/dist/primitives/choice/Switch.js +2 -2
  57. package/dist/primitives/date/Calendar.js +1 -1
  58. package/dist/primitives/date/DatePicker.d.ts +1 -1
  59. package/dist/primitives/date/DatePicker.js +2 -2
  60. package/dist/primitives/date/DateTime.d.ts +1 -1
  61. package/dist/primitives/date/DateTime.js +2 -2
  62. package/dist/primitives/field/Checks.d.ts +1 -1
  63. package/dist/primitives/field/Checks.js +4 -4
  64. package/dist/primitives/field/DropZone.d.ts +1 -1
  65. package/dist/primitives/field/DropZone.js +2 -2
  66. package/dist/primitives/field/Field.d.ts +2 -2
  67. package/dist/primitives/field/Field.js +3 -3
  68. package/dist/primitives/field/Suggest.d.ts +1 -1
  69. package/dist/primitives/field/Suggest.js +2 -2
  70. package/dist/primitives/field/index.d.ts +1 -3
  71. package/dist/primitives/field/index.js +4 -4
  72. package/dist/primitives/picker/Picker.d.ts +1 -1
  73. package/dist/primitives/picker/Picker.js +4 -4
  74. package/dist/primitives/{tag → picker}/TagsField.d.ts +2 -2
  75. package/dist/primitives/{tag → picker}/TagsField.js +5 -5
  76. package/dist/primitives/picker/index.d.ts +2 -0
  77. package/dist/primitives/picker/index.js +5 -1
  78. package/dist/primitives/table/CellCopy.js +1 -1
  79. package/dist/primitives/table/CellEditor.js +2 -4
  80. package/dist/primitives/table/DataTable.d.ts +11 -1
  81. package/dist/primitives/table/DataTable.js +137 -38
  82. package/dist/primitives/table/EditableTable.js +2 -2
  83. package/dist/primitives/table/PivotTable.js +0 -0
  84. package/dist/primitives/table/ReportTable.js +4 -3
  85. package/dist/primitives/table/SheetTable.js +8 -14
  86. package/dist/primitives/table/Table.d.ts +12 -3
  87. package/dist/primitives/table/Table.js +7 -7
  88. package/dist/primitives/table/rowWindow.d.ts +39 -0
  89. package/dist/primitives/table/rowWindow.js +177 -0
  90. package/dist/primitives/tabs/Tabs.d.ts +22 -4
  91. package/dist/primitives/tabs/Tabs.js +77 -23
  92. package/dist/primitives/tabs/index.d.ts +1 -1
  93. package/dist/primitives/tabs/index.js +1 -1
  94. package/dist/primitives/tag/Tags.js +4 -1
  95. package/dist/primitives/tag/index.d.ts +0 -2
  96. package/dist/primitives/tag/index.js +0 -3
  97. package/dist/primitives/value/Avatar.js +8 -1
  98. package/dist/primitives/value/{Fecha.d.ts → DateValue.d.ts} +5 -2
  99. package/dist/primitives/value/{Fecha.js → DateValue.js} +9 -8
  100. package/dist/primitives/value/Relation.d.ts +1 -1
  101. package/dist/primitives/value/Relation.js +2 -2
  102. package/dist/primitives/value/Value.js +4 -1
  103. package/dist/primitives/value/index.d.ts +1 -1
  104. package/dist/primitives/value/index.js +1 -1
  105. package/dist/views/list/List.d.ts +14 -1
  106. package/dist/views/list/List.js +28 -6
  107. package/dist/views/list/ListView.d.ts +1 -1
  108. package/dist/views/list/ListView.js +41 -14
  109. package/dist/views/list/index.d.ts +2 -2
  110. package/dist/views/list/presentations.js +3 -1
  111. package/dist/views/list/types.d.ts +49 -1
  112. package/dist/views/list/useNarrowList.d.ts +2 -2
  113. package/dist/views/list/useNarrowList.js +5 -5
  114. package/dist/views/record/Record.js +46 -42
  115. package/package.json +1 -1
  116. package/dist/primitives/field/context.js +0 -13
  117. package/dist/primitives/field/mode.d.ts +0 -9
  118. /package/dist/primitives/field/{copy.d.ts → clipboard.d.ts} +0 -0
  119. /package/dist/primitives/field/{copy.js → clipboard.js} +0 -0
  120. /package/dist/primitives/{tag/choices.d.ts → picker/tagChoices.d.ts} +0 -0
  121. /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/70", 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) })] }));
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("th", { className: "w-0 border-b border-line-strong" }) : null, columns.map((c) => (_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("th", { className: "w-0 border-b border-line-strong" }))] }) }), _jsx("tbody", { ...(reorderable
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 { currentCopyLocale, fillCopy, useUiCopy } from "../../foundations/copy.js";
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(currentCopyLocale()).format(value) : format(value) }));
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("th", { className: "w-0" })] }) }), _jsx("tbody", { children: rows.map((r) => (_jsx(ReportLine, { row: r, columns: columns, open: open, onToggle: toggle, compact: compact }, r.id))) })] }) }));
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 : undefined, className: `relative min-w-0 rounded-box border border-line bg-surface focus:outline-none ${caja.className}`, role: "grid", "aria-rowcount": rows, "aria-colcount": columns.length + 1, children: _jsxs("div", { style: {
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 === undefined
187
- ? undefined
188
- : () => onBandToggle(item.index), "aria-expanded": montón.open, className: "sticky left-0 z-10 flex min-w-0 shrink-0 items-center gap-1.5 border-r border-line bg-ground px-2.5 text-left", style: {
189
- height: "100%",
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/70", 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"
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
- * The strip only: what each tab opens is drawn by whoever uses it (`Record`, `Activity`).
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
- * Built on React Aria's `Tabs`, so the strip is a real tab list for the keyboard: the
82
- * arrows move between tabs and select them, Home/End jump to the ends, and only the open
83
- * tab is in the Tab order. The hand-written version had the roles and none of the keys.
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 { Tab, TabList, Tabs as AriaTabs } from "react-aria-components";
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
- return (_jsx(AriaTabs, { selectedKey: current, onSelectionChange: (key) => onSelect?.(String(key)), children: _jsx(TabList, { "aria-label": label ?? copy.record.faces, className: "flex min-w-0 flex-wrap items-end gap-1", children: tabs.map((a) => {
34
- const Icon = a.icon;
35
- const empty = a.count === 0;
36
- /* Un cero no pide nada, así que no se dibuja. Es la diferencia con el
37
- contador, donde «0» es una respuesta. */
38
- const notice = a.notice?.count === 0 ? undefined : a.notice;
39
- return (_jsx(Tab, { id: a.id, ...(notice === undefined
40
- ? {}
41
- : { "aria-label": `${a.label} · ${notice.label}` }), className: ({ isSelected }) => `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
42
- ? "-mb-px rounded-t-control border border-b-0 border-line-strong bg-surface font-medium text-ink"
43
- : "border border-transparent text-muted hover:text-ink"}`, children: ({ isSelected }) => (
44
- /* The `title` goes on the content and not on the tab: React Aria does not
45
- forward it, and on the content the native tooltip still covers the
46
- whole tab. */
47
- _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
48
- ? "bg-ink/5 text-muted/70"
49
- : isSelected
50
- ? "bg-ink/8 text-ink"
51
- : "bg-ink/8 text-muted"}`, children: a.count.toLocaleString(copy.locale) })), notice === undefined ? null : notice.count === undefined ? (
52
- /* Sin número, un punto. La palabra no se dibuja pero está: en el
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
- _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));
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 : (_jsxs("span", { className: "shrink-0 opacity-60", children: ["\u00B7 ", item.note] }))] }));
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. */
@@ -1,4 +1,2 @@
1
1
  export { StatusDot, Tag } from "./Tag.js";
2
2
  export { type TagItem, Tags } from "./Tags.js";
3
- export { type TagOption } from "./choices.js";
4
- export { TagsField, type TagsFieldProps } from "./TagsField.js";
@@ -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";