@eifi1/ui-kit 0.47.0 → 0.48.1

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 (152) hide show
  1. package/README.md +5 -3
  2. package/dist/admin/admin-sections.d.ts +8 -0
  3. package/dist/admin/admin-sections.js +2 -1
  4. package/dist/admin/admin-sections.js.map +1 -1
  5. package/dist/auth/verify-email.d.ts +9 -1
  6. package/dist/auth/verify-email.js +22 -13
  7. package/dist/auth/verify-email.js.map +1 -1
  8. package/dist/components/alert-banner.d.ts +29 -1
  9. package/dist/components/alert-banner.js +84 -7
  10. package/dist/components/alert-banner.js.map +1 -1
  11. package/dist/components/button-look.d.ts +4 -0
  12. package/dist/components/button-look.js +2 -0
  13. package/dist/components/button-look.js.map +1 -1
  14. package/dist/components/combobox-core.js +5 -1
  15. package/dist/components/combobox-core.js.map +1 -1
  16. package/dist/components/data-table-column-settings.d.ts +6 -1
  17. package/dist/components/data-table-column-settings.js +12 -2
  18. package/dist/components/data-table-column-settings.js.map +1 -1
  19. package/dist/components/data-table-fit.d.ts +44 -0
  20. package/dist/components/data-table-fit.js +103 -0
  21. package/dist/components/data-table-fit.js.map +1 -0
  22. package/dist/components/data-table-groups.d.ts +95 -0
  23. package/dist/components/data-table-groups.js +165 -0
  24. package/dist/components/data-table-groups.js.map +1 -0
  25. package/dist/components/data-table-head.js +3 -1
  26. package/dist/components/data-table-head.js.map +1 -1
  27. package/dist/components/data-table-labels.d.ts +7 -0
  28. package/dist/components/data-table-labels.js +3 -0
  29. package/dist/components/data-table-labels.js.map +1 -1
  30. package/dist/components/data-table-leading-row.d.ts +17 -0
  31. package/dist/components/data-table-leading-row.js +41 -0
  32. package/dist/components/data-table-leading-row.js.map +1 -0
  33. package/dist/components/data-table-resize.js +1 -1
  34. package/dist/components/data-table-resize.js.map +1 -1
  35. package/dist/components/data-table.d.ts +95 -2
  36. package/dist/components/data-table.js +360 -267
  37. package/dist/components/data-table.js.map +1 -1
  38. package/dist/components/disclosure.d.ts +3 -1
  39. package/dist/components/disclosure.js +10 -4
  40. package/dist/components/disclosure.js.map +1 -1
  41. package/dist/components/dropdown.js +8 -1
  42. package/dist/components/dropdown.js.map +1 -1
  43. package/dist/components/floating-panel.d.ts +14 -1
  44. package/dist/components/floating-panel.js +7 -2
  45. package/dist/components/floating-panel.js.map +1 -1
  46. package/dist/components/hover-menu.js +5 -1
  47. package/dist/components/hover-menu.js.map +1 -1
  48. package/dist/components/icon-button.js +2 -1
  49. package/dist/components/icon-button.js.map +1 -1
  50. package/dist/components/popover.js +5 -1
  51. package/dist/components/popover.js.map +1 -1
  52. package/dist/components/table-skeleton.d.ts +35 -0
  53. package/dist/components/table-skeleton.js +75 -0
  54. package/dist/components/table-skeleton.js.map +1 -0
  55. package/dist/components/tabs.d.ts +10 -12
  56. package/dist/components/tabs.js +20 -10
  57. package/dist/components/tabs.js.map +1 -1
  58. package/dist/components/use-table-state.d.ts +8 -7
  59. package/dist/components/use-table-state.js +37 -3
  60. package/dist/components/use-table-state.js.map +1 -1
  61. package/dist/data-table.d.ts +1 -0
  62. package/dist/data-table.js.map +1 -1
  63. package/dist/eslint/index.js +1 -1
  64. package/dist/feedback/feedback-table.d.ts +5 -0
  65. package/dist/feedback/feedback-table.js +30 -7
  66. package/dist/feedback/feedback-table.js.map +1 -1
  67. package/dist/hooks/use-dismiss.d.ts +30 -1
  68. package/dist/hooks/use-dismiss.js +30 -2
  69. package/dist/hooks/use-dismiss.js.map +1 -1
  70. package/dist/i18n/german.js +6 -1
  71. package/dist/i18n/german.js.map +1 -1
  72. package/dist/i18n/kit-labels.d.ts +4 -0
  73. package/dist/i18n/kit-labels.js.map +1 -1
  74. package/dist/i18n/locales/en.js +6 -1
  75. package/dist/i18n/locales/en.js.map +2 -2
  76. package/dist/i18n/locales/es.js +6 -1
  77. package/dist/i18n/locales/es.js.map +1 -1
  78. package/dist/i18n/locales/fr.js +6 -1
  79. package/dist/i18n/locales/fr.js.map +1 -1
  80. package/dist/i18n/locales/hu.js +6 -1
  81. package/dist/i18n/locales/hu.js.map +1 -1
  82. package/dist/i18n/locales/it.js +6 -1
  83. package/dist/i18n/locales/it.js.map +1 -1
  84. package/dist/i18n/locales/zh.js +6 -1
  85. package/dist/i18n/locales/zh.js.map +1 -1
  86. package/dist/i18n/shared-labels.js +3 -1
  87. package/dist/i18n/shared-labels.js.map +1 -1
  88. package/dist/index.d.ts +2 -0
  89. package/dist/index.js +2 -0
  90. package/dist/index.js.map +1 -1
  91. package/dist/lib/float-clearance.d.ts +8 -0
  92. package/dist/lib/float-clearance.js +29 -0
  93. package/dist/lib/float-clearance.js.map +1 -0
  94. package/dist/settings/settings-catalogue.d.ts +10 -0
  95. package/dist/settings/settings-catalogue.js.map +1 -1
  96. package/dist/settings/settings-layout.js +10 -3
  97. package/dist/settings/settings-layout.js.map +1 -1
  98. package/dist/shell/app-shell.d.ts +5 -0
  99. package/dist/shell/app-shell.js +5 -3
  100. package/dist/shell/app-shell.js.map +1 -1
  101. package/dist/shell/option-switcher-menu.d.ts +2 -0
  102. package/dist/shell/option-switcher-menu.js +6 -2
  103. package/dist/shell/option-switcher-menu.js.map +1 -1
  104. package/dist/shell/topbar-action-menu.js +2 -1
  105. package/dist/shell/topbar-action-menu.js.map +1 -1
  106. package/dist/shell/topbar-controls.js +5 -3
  107. package/dist/shell/topbar-controls.js.map +1 -1
  108. package/dist/tour/tour.js +1 -0
  109. package/dist/tour/tour.js.map +1 -1
  110. package/package.json +1 -1
  111. package/src/admin/admin-sections.tsx +8 -0
  112. package/src/auth/verify-email.tsx +34 -9
  113. package/src/components/alert-banner.tsx +131 -7
  114. package/src/components/button-look.ts +6 -0
  115. package/src/components/combobox-core.tsx +7 -1
  116. package/src/components/data-table-column-settings.tsx +23 -2
  117. package/src/components/data-table-fit.ts +187 -0
  118. package/src/components/data-table-groups.tsx +275 -0
  119. package/src/components/data-table-head.tsx +4 -1
  120. package/src/components/data-table-labels.ts +13 -0
  121. package/src/components/data-table-leading-row.tsx +55 -0
  122. package/src/components/data-table-resize.ts +2 -1
  123. package/src/components/data-table.tsx +279 -54
  124. package/src/components/disclosure.tsx +22 -5
  125. package/src/components/dropdown.tsx +11 -1
  126. package/src/components/floating-panel.tsx +24 -1
  127. package/src/components/hover-menu.tsx +6 -1
  128. package/src/components/icon-button.tsx +3 -3
  129. package/src/components/popover.tsx +7 -1
  130. package/src/components/table-skeleton.tsx +133 -0
  131. package/src/components/tabs.tsx +67 -37
  132. package/src/components/use-table-state.ts +54 -3
  133. package/src/data-table.ts +2 -0
  134. package/src/feedback/feedback-table.tsx +33 -1
  135. package/src/hooks/use-dismiss.ts +65 -3
  136. package/src/i18n/german.ts +5 -0
  137. package/src/i18n/kit-labels.tsx +4 -0
  138. package/src/i18n/locales/es.ts +5 -0
  139. package/src/i18n/locales/fr.ts +5 -0
  140. package/src/i18n/locales/hu.ts +5 -0
  141. package/src/i18n/locales/it.ts +5 -0
  142. package/src/i18n/locales/zh.ts +5 -0
  143. package/src/i18n/shared-labels.ts +2 -0
  144. package/src/index.ts +3 -0
  145. package/src/lib/float-clearance.ts +54 -0
  146. package/src/settings/settings-catalogue.ts +10 -0
  147. package/src/settings/settings-layout.tsx +18 -4
  148. package/src/shell/app-shell.tsx +17 -3
  149. package/src/shell/option-switcher-menu.tsx +16 -4
  150. package/src/shell/topbar-action-menu.tsx +22 -10
  151. package/src/shell/topbar-controls.tsx +18 -12
  152. package/src/tour/tour.tsx +5 -0
@@ -2,11 +2,16 @@ import type { Dispatch, SetStateAction } from "react";
2
2
  import type { DataTableLabels } from "./data-table-labels.js";
3
3
  import type { DataTableColumn } from "./data-table.js";
4
4
  /** The "Columns" rail and its drawer (`columnSettings`). */
5
- export declare function ColumnSettingsPanel<T>({ columns, hiddenCols, setHiddenCols, showSettings, setShowSettings, columnsCountLabel, labels, autoSizeAll, }: {
5
+ export declare function ColumnSettingsPanel<T>({ columns, hiddenCols, setHiddenCols, autoHidden, keepColumn, showSettings, setShowSettings, columnsCountLabel, labels, autoSizeAll, }: {
6
6
  /** Every column, hidden ones included; the actions column is left out here. */
7
7
  columns: DataTableColumn<T>[];
8
8
  hiddenCols: Set<string>;
9
9
  setHiddenCols: Dispatch<SetStateAction<Set<string>>>;
10
+ /** The columns the table's width leaves out (`fitPriority`, 0.48): unticked, and
11
+ * marked "hidden for width". */
12
+ autoHidden: Set<string>;
13
+ /** A tick keeps a column in whatever the width; an untick drops that (data-table-fit.ts). */
14
+ keepColumn: (key: string, on: boolean) => void;
10
15
  showSettings: boolean;
11
16
  setShowSettings: (open: boolean) => void;
12
17
  /** "Columns (n/m)", the rail's text and name. */
@@ -10,6 +10,8 @@ function ColumnSettingsPanel({
10
10
  columns,
11
11
  hiddenCols,
12
12
  setHiddenCols,
13
+ autoHidden,
14
+ keepColumn,
13
15
  showSettings,
14
16
  setShowSettings,
15
17
  columnsCountLabel,
@@ -79,7 +81,8 @@ function ColumnSettingsPanel({
79
81
  ),
80
82
  /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-0.5", children: columns.map((col) => {
81
83
  if (isTableOwnColumn(col.key)) return null;
82
- const checked = !hiddenCols.has(col.key);
84
+ const forWidth = !hiddenCols.has(col.key) && autoHidden.has(col.key);
85
+ const checked = !hiddenCols.has(col.key) && !forWidth;
83
86
  const headerLabel = columnLabel(col);
84
87
  return /* @__PURE__ */ jsxs(
85
88
  "label",
@@ -93,6 +96,7 @@ function ColumnSettingsPanel({
93
96
  className: "size-3.5 shrink-0",
94
97
  checked,
95
98
  onChange: (e) => {
99
+ keepColumn(col.key, e.target.checked);
96
100
  setHiddenCols((s) => {
97
101
  const next = new Set(s);
98
102
  if (e.target.checked) next.delete(col.key);
@@ -102,7 +106,13 @@ function ColumnSettingsPanel({
102
106
  }
103
107
  }
104
108
  ),
105
- /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate-until-large", children: headerLabel })
109
+ forWidth ? (
110
+ // Under the name, as part of the box's name: "Email, hidden for width".
111
+ /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-col", children: [
112
+ /* @__PURE__ */ jsx("span", { className: "truncate-until-large", children: headerLabel }),
113
+ /* @__PURE__ */ jsx("span", { className: "text-micro text-muted", "data-hidden-for-width": "", children: labels.hiddenForWidth })
114
+ ] })
115
+ ) : /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate-until-large", children: headerLabel })
106
116
  ]
107
117
  },
108
118
  col.key
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/data-table-column-settings.tsx"],"sourcesContent":["import { useRef } from \"react\";\nimport type { Dispatch, SetStateAction } from \"react\";\nimport { X } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { Tooltip } from \"./tooltip\";\nimport type { DataTableLabels } from \"./data-table-labels\";\nimport { isTableOwnColumn } from \"./data-table-row-actions\";\nimport { columnLabel } from \"./data-table-shared\";\nimport type { DataTableColumn } from \"./data-table\";\n\n// DataTable's column settings on a wide screen: the vertical \"Columns (n/m)\" rail and\n// the drawer it opens, with \"Auto-size\" and a show/hide box per column. Moved out of\n// data-table.tsx whole (refactor audit 2026-10, MOD-3 step 5).\n\n/** The \"Columns\" rail and its drawer (`columnSettings`). */\nexport function ColumnSettingsPanel<T>({\n columns,\n hiddenCols,\n setHiddenCols,\n showSettings,\n setShowSettings,\n columnsCountLabel,\n labels,\n autoSizeAll,\n}: {\n /** Every column, hidden ones included; the actions column is left out here. */\n columns: DataTableColumn<T>[];\n hiddenCols: Set<string>;\n setHiddenCols: Dispatch<SetStateAction<Set<string>>>;\n showSettings: boolean;\n setShowSettings: (open: boolean) => void;\n /** \"Columns (n/m)\", the rail's text and name. */\n columnsCountLabel: string;\n labels: DataTableLabels;\n autoSizeAll: () => void;\n}) {\n // The \"Columns\" rail button — where the drawer's X hands focus back (BUG-7a).\n const columnsRail = useRef<HTMLButtonElement | null>(null);\n return (\n <>\n <Tooltip label={columnsCountLabel} portal>\n <button\n ref={columnsRail}\n type=\"button\"\n onClick={() => setShowSettings(true)}\n aria-label={columnsCountLabel}\n aria-hidden={showSettings}\n tabIndex={showSettings ? -1 : 0}\n className={cn(\n \"group flex shrink-0 items-start justify-center overflow-hidden pt-3 transition-[width] duration-200 ease-out\",\n \"border-s border-subtle\",\n \"hover:bg-hover\",\n showSettings ? \"w-0 border-s-0\" : \"w-8 cursor-pointer\",\n )}\n >\n <span\n className=\"whitespace-nowrap text-xs font-medium tracking-wide text-muted group-hover:text-secondary\"\n style={{ writingMode: \"vertical-rl\", transform: \"rotate(180deg)\" }}\n >\n {columnsCountLabel}\n </span>\n </button>\n </Tooltip>\n <div\n // Closed, the drawer is only `w-0`: without `inert` its X, Auto-size and one\n // checkbox per column stayed tab stops and in the accessibility tree, and Tab\n // after the last cell walked into controls nobody could see (BUG-7a).\n inert={!showSettings || undefined}\n className={cn(\n \"shrink-0 overflow-hidden transition-[width] duration-200 ease-out\",\n showSettings ? \"w-56\" : \"w-0\",\n )}\n >\n <div className=\"flex w-56 flex-col border-s border-subtle p-2\">\n <div className=\"mb-2 flex items-center justify-between gap-1\">\n <div className=\"text-xs font-medium uppercase tracking-wide text-muted\">\n {columnsCountLabel}\n </div>\n <Tooltip label={labels.close} portal>\n <button\n type=\"button\"\n onClick={() => {\n setShowSettings(false);\n // Back to the rail that opened it: the X is about to go inert, and\n // focus would fall to <body> with it (BUG-7a).\n columnsRail.current?.focus();\n }}\n aria-label={labels.close}\n className=\"rounded p-0.5 text-placeholder hover:bg-hover hover:text-secondary\"\n >\n <X className=\"size-4\" />\n </button>\n </Tooltip>\n </div>\n <button\n type=\"button\"\n onClick={autoSizeAll}\n className=\"mb-2 rounded border border-subtle px-2 py-1 text-xs font-medium text-secondary hover:bg-hover\"\n >\n {labels.autoSize}\n </button>\n <div className=\"flex flex-col gap-0.5\">\n {columns.map((col) => {\n if (isTableOwnColumn(col.key)) return null;\n const checked = !hiddenCols.has(col.key);\n const headerLabel = columnLabel(col);\n return (\n <label\n key={col.key}\n className=\"inline-flex cursor-pointer items-center gap-1.5 rounded px-1 py-0.5 text-xs hover:bg-hover\"\n >\n <input\n type=\"checkbox\"\n className=\"size-3.5 shrink-0\"\n checked={checked}\n onChange={(e) => {\n setHiddenCols((s) => {\n const next = new Set(s);\n if (e.target.checked) next.delete(col.key);\n else next.add(col.key);\n return next;\n });\n }}\n />\n {/* The panel is `w-56`: a long column name wraps from Large up rather\n than reading \"Accoun…\" beside its box (text size §10.17, round\n 0.34 T1). */}\n <span className=\"min-w-0 truncate-until-large\">{headerLabel}</span>\n </label>\n );\n })}\n </div>\n </div>\n </div>\n </>\n );\n}\n"],"mappings":";AAuCI,mBAgBM,KAmBA,YAnCN;AAvCJ,SAAS,cAAc;AAEvB,SAAS,SAAS;AAClB,SAAS,UAAU;AACnB,SAAS,eAAe;AAExB,SAAS,wBAAwB;AACjC,SAAS,mBAAmB;AAQrB,SAAS,oBAAuB;AAAA,EACrC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAWG;AAED,QAAM,cAAc,OAAiC,IAAI;AACzD,SACE,iCACE;AAAA,wBAAC,WAAQ,OAAO,mBAAmB,QAAM,MACvC;AAAA,MAAC;AAAA;AAAA,QACC,KAAK;AAAA,QACL,MAAK;AAAA,QACL,SAAS,MAAM,gBAAgB,IAAI;AAAA,QACnC,cAAY;AAAA,QACZ,eAAa;AAAA,QACb,UAAU,eAAe,KAAK;AAAA,QAC9B,WAAW;AAAA,UACT;AAAA,UACA;AAAA,UACA;AAAA,UACA,eAAe,mBAAmB;AAAA,QACpC;AAAA,QAEA;AAAA,UAAC;AAAA;AAAA,YACC,WAAU;AAAA,YACV,OAAO,EAAE,aAAa,eAAe,WAAW,iBAAiB;AAAA,YAEhE;AAAA;AAAA,QACH;AAAA;AAAA,IACF,GACF;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QAIC,OAAO,CAAC,gBAAgB;AAAA,QACxB,WAAW;AAAA,UACT;AAAA,UACA,eAAe,SAAS;AAAA,QAC1B;AAAA,QAEA,+BAAC,SAAI,WAAU,iDACb;AAAA,+BAAC,SAAI,WAAU,gDACb;AAAA,gCAAC,SAAI,WAAU,0DACZ,6BACH;AAAA,YACA,oBAAC,WAAQ,OAAO,OAAO,OAAO,QAAM,MAClC;AAAA,cAAC;AAAA;AAAA,gBACC,MAAK;AAAA,gBACL,SAAS,MAAM;AACb,kCAAgB,KAAK;AAGrB,8BAAY,SAAS,MAAM;AAAA,gBAC7B;AAAA,gBACA,cAAY,OAAO;AAAA,gBACnB,WAAU;AAAA,gBAEV,8BAAC,KAAE,WAAU,UAAS;AAAA;AAAA,YACxB,GACF;AAAA,aACF;AAAA,UACA;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,SAAS;AAAA,cACT,WAAU;AAAA,cAET,iBAAO;AAAA;AAAA,UACV;AAAA,UACA,oBAAC,SAAI,WAAU,yBACZ,kBAAQ,IAAI,CAAC,QAAQ;AACpB,gBAAI,iBAAiB,IAAI,GAAG,EAAG,QAAO;AACtC,kBAAM,UAAU,CAAC,WAAW,IAAI,IAAI,GAAG;AACvC,kBAAM,cAAc,YAAY,GAAG;AACnC,mBACE;AAAA,cAAC;AAAA;AAAA,gBAEC,WAAU;AAAA,gBAEV;AAAA;AAAA,oBAAC;AAAA;AAAA,sBACC,MAAK;AAAA,sBACL,WAAU;AAAA,sBACV;AAAA,sBACA,UAAU,CAAC,MAAM;AACf,sCAAc,CAAC,MAAM;AACnB,gCAAM,OAAO,IAAI,IAAI,CAAC;AACtB,8BAAI,EAAE,OAAO,QAAS,MAAK,OAAO,IAAI,GAAG;AAAA,8BACpC,MAAK,IAAI,IAAI,GAAG;AACrB,iCAAO;AAAA,wBACT,CAAC;AAAA,sBACH;AAAA;AAAA,kBACF;AAAA,kBAIA,oBAAC,UAAK,WAAU,gCAAgC,uBAAY;AAAA;AAAA;AAAA,cAnBvD,IAAI;AAAA,YAoBX;AAAA,UAEJ,CAAC,GACH;AAAA,WACF;AAAA;AAAA,IACF;AAAA,KACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/components/data-table-column-settings.tsx"],"sourcesContent":["import { useRef } from \"react\";\nimport type { Dispatch, SetStateAction } from \"react\";\nimport { X } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { Tooltip } from \"./tooltip\";\nimport type { DataTableLabels } from \"./data-table-labels\";\nimport { isTableOwnColumn } from \"./data-table-row-actions\";\nimport { columnLabel } from \"./data-table-shared\";\nimport type { DataTableColumn } from \"./data-table\";\n\n// DataTable's column settings on a wide screen: the vertical \"Columns (n/m)\" rail and\n// the drawer it opens, with \"Auto-size\" and a show/hide box per column. Moved out of\n// data-table.tsx whole (refactor audit 2026-10, MOD-3 step 5).\n\n/** The \"Columns\" rail and its drawer (`columnSettings`). */\nexport function ColumnSettingsPanel<T>({\n columns,\n hiddenCols,\n setHiddenCols,\n autoHidden,\n keepColumn,\n showSettings,\n setShowSettings,\n columnsCountLabel,\n labels,\n autoSizeAll,\n}: {\n /** Every column, hidden ones included; the actions column is left out here. */\n columns: DataTableColumn<T>[];\n hiddenCols: Set<string>;\n setHiddenCols: Dispatch<SetStateAction<Set<string>>>;\n /** The columns the table's width leaves out (`fitPriority`, 0.48): unticked, and\n * marked \"hidden for width\". */\n autoHidden: Set<string>;\n /** A tick keeps a column in whatever the width; an untick drops that (data-table-fit.ts). */\n keepColumn: (key: string, on: boolean) => void;\n showSettings: boolean;\n setShowSettings: (open: boolean) => void;\n /** \"Columns (n/m)\", the rail's text and name. */\n columnsCountLabel: string;\n labels: DataTableLabels;\n autoSizeAll: () => void;\n}) {\n // The \"Columns\" rail button — where the drawer's X hands focus back (BUG-7a).\n const columnsRail = useRef<HTMLButtonElement | null>(null);\n return (\n <>\n <Tooltip label={columnsCountLabel} portal>\n <button\n ref={columnsRail}\n type=\"button\"\n onClick={() => setShowSettings(true)}\n aria-label={columnsCountLabel}\n aria-hidden={showSettings}\n tabIndex={showSettings ? -1 : 0}\n className={cn(\n \"group flex shrink-0 items-start justify-center overflow-hidden pt-3 transition-[width] duration-200 ease-out\",\n \"border-s border-subtle\",\n \"hover:bg-hover\",\n showSettings ? \"w-0 border-s-0\" : \"w-8 cursor-pointer\",\n )}\n >\n <span\n className=\"whitespace-nowrap text-xs font-medium tracking-wide text-muted group-hover:text-secondary\"\n style={{ writingMode: \"vertical-rl\", transform: \"rotate(180deg)\" }}\n >\n {columnsCountLabel}\n </span>\n </button>\n </Tooltip>\n <div\n // Closed, the drawer is only `w-0`: without `inert` its X, Auto-size and one\n // checkbox per column stayed tab stops and in the accessibility tree, and Tab\n // after the last cell walked into controls nobody could see (BUG-7a).\n inert={!showSettings || undefined}\n className={cn(\n \"shrink-0 overflow-hidden transition-[width] duration-200 ease-out\",\n showSettings ? \"w-56\" : \"w-0\",\n )}\n >\n <div className=\"flex w-56 flex-col border-s border-subtle p-2\">\n <div className=\"mb-2 flex items-center justify-between gap-1\">\n <div className=\"text-xs font-medium uppercase tracking-wide text-muted\">\n {columnsCountLabel}\n </div>\n <Tooltip label={labels.close} portal>\n <button\n type=\"button\"\n onClick={() => {\n setShowSettings(false);\n // Back to the rail that opened it: the X is about to go inert, and\n // focus would fall to <body> with it (BUG-7a).\n columnsRail.current?.focus();\n }}\n aria-label={labels.close}\n className=\"rounded p-0.5 text-placeholder hover:bg-hover hover:text-secondary\"\n >\n <X className=\"size-4\" />\n </button>\n </Tooltip>\n </div>\n <button\n type=\"button\"\n onClick={autoSizeAll}\n className=\"mb-2 rounded border border-subtle px-2 py-1 text-xs font-medium text-secondary hover:bg-hover\"\n >\n {labels.autoSize}\n </button>\n <div className=\"flex flex-col gap-0.5\">\n {columns.map((col) => {\n if (isTableOwnColumn(col.key)) return null;\n // Ticked = drawn: a column left out for width is unticked, so the boxes\n // and \"Columns (8/10)\" say the same (keksdose live #451).\n const forWidth = !hiddenCols.has(col.key) && autoHidden.has(col.key);\n const checked = !hiddenCols.has(col.key) && !forWidth;\n const headerLabel = columnLabel(col);\n return (\n <label\n key={col.key}\n className=\"inline-flex cursor-pointer items-center gap-1.5 rounded px-1 py-0.5 text-xs hover:bg-hover\"\n >\n <input\n type=\"checkbox\"\n className=\"size-3.5 shrink-0\"\n checked={checked}\n onChange={(e) => {\n keepColumn(col.key, e.target.checked);\n setHiddenCols((s) => {\n const next = new Set(s);\n if (e.target.checked) next.delete(col.key);\n else next.add(col.key);\n return next;\n });\n }}\n />\n {/* The panel is `w-56`: a long column name wraps from Large up rather\n than reading \"Accoun…\" beside its box (text size §10.17, round\n 0.34 T1). */}\n {forWidth ? (\n // Under the name, as part of the box's name: \"Email, hidden for width\".\n <span className=\"flex min-w-0 flex-col\">\n <span className=\"truncate-until-large\">{headerLabel}</span>\n <span className=\"text-micro text-muted\" data-hidden-for-width=\"\">\n {labels.hiddenForWidth}\n </span>\n </span>\n ) : (\n <span className=\"min-w-0 truncate-until-large\">{headerLabel}</span>\n )}\n </label>\n );\n })}\n </div>\n </div>\n </div>\n </>\n );\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,44 @@
1
+ import type { DataTableColumn } from "./data-table.js";
2
+ /** The least a `grow` column keeps before the table starts leaving columns out: about
3
+ * a sentence of a title. In rem, so Large text asks for more. */
4
+ export declare const GROW_MIN_REM = 10;
5
+ /** The `<th>`/`<td>` classes of a `grow` column. `truncate-until-large`: one line and an
6
+ * ellipsis at Normal, a wrap from Large up (text size §10.17), as every kit title. The
7
+ * header's `min-w-[10rem]` ({@link GROW_MIN_REM}) is the floor once no `fitPriority`
8
+ * column is left to leave out: the table then scrolls sideways rather than crushing the
9
+ * column it exists for (Kurvenschmiede S1, 0.48: Subject was 24px at 1100px). */
10
+ export declare const GROW_HEAD_CLASS = "w-full max-w-0 min-w-[10rem]";
11
+ export declare const GROW_CELL_CLASS = "w-full max-w-0 truncate-until-large";
12
+ /** Every OTHER cell of a table with a `grow` column (kastlan S2, 0.48). A `w-full` column
13
+ * squeezes the rest to their min-content width, so any text with a space broke in two
14
+ * even at 1920px ("CHF 2'568.38", "Peter Mueller", a date with its time). One line at
15
+ * Normal, so a column is never narrower than its text and `fitPriority` measures what
16
+ * the columns really need; from Large up the cell wraps as before (§10.17). The headers
17
+ * are `whitespace-nowrap` already. A column whose prose should wrap anyway passes
18
+ * `not-large:whitespace-normal` in its `className`. */
19
+ export declare const GROW_PEER_CELL_CLASS = "not-large:whitespace-nowrap";
20
+ /** The auto-hide order: lowest `fitPriority` first, the later column first on a tie. */
21
+ export declare function fitHideOrder<T>(columns: DataTableColumn<T>[]): DataTableColumn<T>[];
22
+ /**
23
+ * Which columns the table leaves out for width, and the Columns panel's "keep it".
24
+ *
25
+ * A column the user TICKS in the Columns panel is theirs: it is never left out for width
26
+ * again while the table is mounted (`keep`): the table makes room by leaving out the next
27
+ * candidate, and scrolls sideways once none is left — what they asked for. Unticking it hides it as before (the persisted `hiddenCols`) and
28
+ * drops the keep. Keeps are not persisted: on the next visit the table fits again.
29
+ */
30
+ export declare function useColumnFit<T>({ columns, hiddenCols, headRefs, scroller, enabled, }: {
31
+ columns: DataTableColumn<T>[];
32
+ /** The user's hidden columns: never candidates (they are out already). */
33
+ hiddenCols: Set<string>;
34
+ headRefs: Map<string, HTMLTableCellElement>;
35
+ /** The desktop scroller, read after render (it is declared below the call). */
36
+ scroller: () => HTMLElement | null;
37
+ /** The wide layout. On a phone (the card list) nothing is left out for width. */
38
+ enabled: boolean;
39
+ }): {
40
+ /** The columns left out for width now — empty while off. */
41
+ autoHidden: Set<string>;
42
+ /** The user's choice in the Columns panel: `true` keeps the column in whatever the width. */
43
+ keep: (key: string, on: boolean) => void;
44
+ };
@@ -0,0 +1,103 @@
1
+ "use client";
2
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
3
+ const GROW_MIN_REM = 10;
4
+ const GROW_HEAD_CLASS = "w-full max-w-0 min-w-[10rem]";
5
+ const GROW_CELL_CLASS = "w-full max-w-0 truncate-until-large";
6
+ const GROW_PEER_CELL_CLASS = "not-large:whitespace-nowrap";
7
+ function fitHideOrder(columns) {
8
+ return columns.map((col, index) => ({ col, index })).filter(({ col }) => col.fitPriority != null).sort((a, b) => a.col.fitPriority - b.col.fitPriority || b.index - a.index).map(({ col }) => col);
9
+ }
10
+ function useColumnFit({
11
+ columns,
12
+ hiddenCols,
13
+ headRefs,
14
+ scroller,
15
+ enabled
16
+ }) {
17
+ const [kept, setKept] = useState(() => /* @__PURE__ */ new Set());
18
+ const [hidden, setHidden] = useState([]);
19
+ const ownWidth = useRef(/* @__PURE__ */ new Map());
20
+ const hasFit = columns.some((c) => c.fitPriority != null);
21
+ const active = enabled && hasFit;
22
+ const order = useMemo(
23
+ () => fitHideOrder(columns).filter((c) => !hiddenCols.has(c.key) && !kept.has(c.key)),
24
+ [columns, hiddenCols, kept]
25
+ );
26
+ const current = useMemo(
27
+ () => active ? order.filter((c) => hidden.includes(c.key)).map((c) => c.key) : [],
28
+ [active, order, hidden]
29
+ );
30
+ const growKey = columns.find((c) => c.grow && !hiddenCols.has(c.key))?.key;
31
+ const latest = useRef({ order, current, growKey, active, scroller });
32
+ useLayoutEffect(() => {
33
+ latest.current = { order, current, growKey, active, scroller };
34
+ });
35
+ const measure = useCallback(() => {
36
+ const { order: order2, current: current2, growKey: growKey2, active: active2, scroller: scroller2 } = latest.current;
37
+ const el = scroller2();
38
+ if (!active2 || !el) return;
39
+ const table = el.querySelector("table");
40
+ const avail = el.clientWidth;
41
+ if (!table || avail <= 0) return;
42
+ const growTh = growKey2 ? headRefs.get(growKey2) : void 0;
43
+ const rem = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
44
+ let need = growTh ? table.offsetWidth - growTh.offsetWidth + GROW_MIN_REM * rem : table.offsetWidth;
45
+ if (!growTh && need <= avail + 0.5 && current2.length > 0) {
46
+ const before = table.style.width;
47
+ table.style.width = "min-content";
48
+ need = table.offsetWidth;
49
+ table.style.width = before;
50
+ }
51
+ if (need > avail + 0.5) {
52
+ const next = [...current2];
53
+ for (const col of order2) {
54
+ if (need <= avail + 0.5) break;
55
+ if (next.includes(col.key)) continue;
56
+ const w = headRefs.get(col.key)?.offsetWidth ?? 0;
57
+ ownWidth.current.set(col.key, w);
58
+ need -= w;
59
+ next.push(col.key);
60
+ }
61
+ if (next.length !== current2.length) setHidden(next);
62
+ return;
63
+ }
64
+ let n = current2.length;
65
+ while (n > 0) {
66
+ const w = ownWidth.current.get(current2[n - 1]);
67
+ if (w == null || need + w > avail + 0.5) break;
68
+ need += w;
69
+ n--;
70
+ }
71
+ if (n !== current2.length) setHidden(current2.slice(0, n));
72
+ }, [headRefs]);
73
+ useLayoutEffect(() => {
74
+ measure();
75
+ });
76
+ useEffect(() => {
77
+ const el = latest.current.scroller();
78
+ if (!active || !el || typeof ResizeObserver === "undefined") return;
79
+ const ro = new ResizeObserver(() => measure());
80
+ ro.observe(el);
81
+ return () => ro.disconnect();
82
+ }, [active, measure]);
83
+ const keep = useCallback((key, on) => {
84
+ setKept((s) => {
85
+ if (s.has(key) === on) return s;
86
+ const next = new Set(s);
87
+ if (on) next.add(key);
88
+ else next.delete(key);
89
+ return next;
90
+ });
91
+ }, []);
92
+ const autoHidden = useMemo(() => new Set(current), [current]);
93
+ return { autoHidden, keep };
94
+ }
95
+ export {
96
+ GROW_CELL_CLASS,
97
+ GROW_HEAD_CLASS,
98
+ GROW_MIN_REM,
99
+ GROW_PEER_CELL_CLASS,
100
+ fitHideOrder,
101
+ useColumnFit
102
+ };
103
+ //# sourceMappingURL=data-table-fit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/data-table-fit.ts"],"sourcesContent":["import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from \"react\";\nimport type { DataTableColumn } from \"./data-table\";\n\n// DataTable's fit to its own width (0.48, keksdose live #451: \"On expanded sidebar for\n// desktop the table scrolls horizontally. On collapsed sidebar it fits […] Compute the\n// width selectively\"). Two column options, both opt-in:\n//\n// - `grow`: the column takes what the others leave and ellipsises (`w-full max-w-0` on\n// its cells: in an auto table layout that is \"the rest of the row\", never its content's\n// width), so one long title can no longer widen the table.\n// - `fitPriority`: while the table is wider than its scroller — or, with a `grow` column,\n// while that column would get less than {@link GROW_MIN_REM} — the table leaves out\n// its `fitPriority` columns, the LOWEST number first (ties: the later column first),\n// and brings them back in the reverse order as soon as the table, as it is NOW, has\n// room for them (its need re-measured, plus the column's own header width from when\n// it was left out), so narrower content (a filter, another page) counts as much as a\n// wider scroller. The scroller's own width, through a ResizeObserver: a sidebar, a\n// narrower window, the Columns drawer and Large text all count the same.\n\n/** The least a `grow` column keeps before the table starts leaving columns out: about\n * a sentence of a title. In rem, so Large text asks for more. */\nexport const GROW_MIN_REM = 10;\n\n/** The `<th>`/`<td>` classes of a `grow` column. `truncate-until-large`: one line and an\n * ellipsis at Normal, a wrap from Large up (text size §10.17), as every kit title. The\n * header's `min-w-[10rem]` ({@link GROW_MIN_REM}) is the floor once no `fitPriority`\n * column is left to leave out: the table then scrolls sideways rather than crushing the\n * column it exists for (Kurvenschmiede S1, 0.48: Subject was 24px at 1100px). */\nexport const GROW_HEAD_CLASS = \"w-full max-w-0 min-w-[10rem]\";\nexport const GROW_CELL_CLASS = \"w-full max-w-0 truncate-until-large\";\n\n/** Every OTHER cell of a table with a `grow` column (kastlan S2, 0.48). A `w-full` column\n * squeezes the rest to their min-content width, so any text with a space broke in two\n * even at 1920px (\"CHF 2'568.38\", \"Peter Mueller\", a date with its time). One line at\n * Normal, so a column is never narrower than its text and `fitPriority` measures what\n * the columns really need; from Large up the cell wraps as before (§10.17). The headers\n * are `whitespace-nowrap` already. A column whose prose should wrap anyway passes\n * `not-large:whitespace-normal` in its `className`. */\nexport const GROW_PEER_CELL_CLASS = \"not-large:whitespace-nowrap\";\n\n// A cell's own flex-wrap row (chips) is left to wrap beside a `grow` column: a row of\n// badges reads as well stacked, and held to one line it widened the user roster's Status\n// until kastlan's admin pane scrolled 21px at 1280px (0.48 build review 2, kastlan S1).\n\n/** The auto-hide order: lowest `fitPriority` first, the later column first on a tie. */\nexport function fitHideOrder<T>(columns: DataTableColumn<T>[]): DataTableColumn<T>[] {\n return columns\n .map((col, index) => ({ col, index }))\n .filter(({ col }) => col.fitPriority != null)\n .sort((a, b) => a.col.fitPriority! - b.col.fitPriority! || b.index - a.index)\n .map(({ col }) => col);\n}\n\n/**\n * Which columns the table leaves out for width, and the Columns panel's \"keep it\".\n *\n * A column the user TICKS in the Columns panel is theirs: it is never left out for width\n * again while the table is mounted (`keep`): the table makes room by leaving out the next\n * candidate, and scrolls sideways once none is left — what they asked for. Unticking it hides it as before (the persisted `hiddenCols`) and\n * drops the keep. Keeps are not persisted: on the next visit the table fits again.\n */\nexport function useColumnFit<T>({\n columns,\n hiddenCols,\n headRefs,\n scroller,\n enabled,\n}: {\n columns: DataTableColumn<T>[];\n /** The user's hidden columns: never candidates (they are out already). */\n hiddenCols: Set<string>;\n headRefs: Map<string, HTMLTableCellElement>;\n /** The desktop scroller, read after render (it is declared below the call). */\n scroller: () => HTMLElement | null;\n /** The wide layout. On a phone (the card list) nothing is left out for width. */\n enabled: boolean;\n}): {\n /** The columns left out for width now — empty while off. */\n autoHidden: Set<string>;\n /** The user's choice in the Columns panel: `true` keeps the column in whatever the width. */\n keep: (key: string, on: boolean) => void;\n} {\n const [kept, setKept] = useState<ReadonlySet<string>>(() => new Set());\n const [hidden, setHidden] = useState<string[]>([]);\n // A column's own header width, written when it is left out: it comes back once the\n // table's need as it is now, plus this, fits the scroller (keksdose S3, 0.48: not the\n // whole table's need at hide time, which went stale when the content narrowed).\n const ownWidth = useRef(new Map<string, number>());\n\n const hasFit = columns.some((c) => c.fitPriority != null);\n const active = enabled && hasFit;\n const order = useMemo(\n () => fitHideOrder(columns).filter((c) => !hiddenCols.has(c.key) && !kept.has(c.key)),\n [columns, hiddenCols, kept],\n );\n // Only the candidates still in play, in hide order — a column the user hid or kept\n // since, or one the app dropped, is no longer \"hidden for width\".\n const current = useMemo(\n () => (active ? order.filter((c) => hidden.includes(c.key)).map((c) => c.key) : []),\n [active, order, hidden],\n );\n const growKey = columns.find((c) => c.grow && !hiddenCols.has(c.key))?.key;\n\n // What `measure` reads, refreshed after each render (before the measure below runs):\n // the ResizeObserver's callback is set up once and must see the current columns.\n const latest = useRef({ order, current, growKey, active, scroller });\n useLayoutEffect(() => {\n latest.current = { order, current, growKey, active, scroller };\n });\n\n const measure = useCallback(() => {\n const { order, current, growKey, active, scroller } = latest.current;\n const el = scroller();\n if (!active || !el) return;\n const table = el.querySelector(\"table\");\n const avail = el.clientWidth;\n // Not laid out (display:none, jsdom): nothing to fit to.\n if (!table || avail <= 0) return;\n const growTh = growKey ? headRefs.get(growKey) : undefined;\n const rem = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;\n let need = growTh\n ? table.offsetWidth - growTh.offsetWidth + GROW_MIN_REM * rem\n : table.offsetWidth;\n if (!growTh && need <= avail + 0.5 && current.length > 0) {\n // A `w-full` table is at least the scroller's width, which says nothing about how\n // much room its content leaves: probe its content's width (what it takes once it\n // overflows) for the bring-back below. Read and put back in one layout pass.\n const before = table.style.width;\n table.style.width = \"min-content\";\n need = table.offsetWidth;\n table.style.width = before;\n }\n if (need > avail + 0.5) {\n // Leave out, in order, until the rest fits: each column saves its header's width\n // (the others are at their content's width while a `grow` column takes the rest).\n const next = [...current];\n for (const col of order) {\n if (need <= avail + 0.5) break;\n if (next.includes(col.key)) continue;\n const w = headRefs.get(col.key)?.offsetWidth ?? 0;\n ownWidth.current.set(col.key, w);\n need -= w;\n next.push(col.key);\n }\n if (next.length !== current.length) setHidden(next);\n return;\n }\n // Bring back, the last left out first, while the table as it is now has room for\n // them. Should the column have grown since (wider content), the next measure leaves\n // it out again before paint and records its new width, so it cannot flicker.\n let n = current.length;\n while (n > 0) {\n const w = ownWidth.current.get(current[n - 1]);\n if (w == null || need + w > avail + 0.5) break;\n need += w;\n n--;\n }\n if (n !== current.length) setHidden(current.slice(0, n));\n }, [headRefs]);\n\n // After every render: rows, a filter or a page can widen a column as much as a resize.\n // Before paint, so a column the table cannot hold is never drawn first.\n useLayoutEffect(() => {\n measure();\n });\n\n useEffect(() => {\n const el = latest.current.scroller();\n if (!active || !el || typeof ResizeObserver === \"undefined\") return;\n const ro = new ResizeObserver(() => measure());\n ro.observe(el);\n return () => ro.disconnect();\n }, [active, measure]);\n\n const keep = useCallback((key: string, on: boolean) => {\n setKept((s) => {\n if (s.has(key) === on) return s;\n const next = new Set(s);\n if (on) next.add(key);\n else next.delete(key);\n return next;\n });\n }, []);\n\n const autoHidden = useMemo(() => new Set(current), [current]);\n return { autoHidden, keep };\n}\n"],"mappings":"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","names":["order","current","growKey","active","scroller"]}
@@ -0,0 +1,95 @@
1
+ import type { ReactNode, RefObject } from "react";
2
+ /**
3
+ * One level of {@link DataTableProps.groupBy} (0.48, keksdose dev #605): the rows are cut
4
+ * into consecutive runs of equal `key` — the rows must already be ordered so equal keys
5
+ * are contiguous, as for `mobileGroupBy` — and each run is headed by a full-width band.
6
+ *
7
+ * The same object drives the phone: `mobileGroupBy` takes a level too, so
8
+ * `groupBy={[period, day]} mobileGroupBy={period}` is one definition for both layouts.
9
+ */
10
+ export interface DataTableGroupBy<T> {
11
+ /** The group a row belongs to. */
12
+ key: (row: T) => string;
13
+ /** The band's label. Default: the key itself. */
14
+ label?: (key: string, rows: T[]) => ReactNode;
15
+ /**
16
+ * The band's end slot — a count, a total ("176 · +8'782.30 · −3'200.10"). `rows` are
17
+ * the group's rows the table holds; an app whose total covers rows not yet loaded
18
+ * looks it up by `key` instead.
19
+ */
20
+ end?: (key: string, rows: T[]) => ReactNode;
21
+ }
22
+ /**
23
+ * {@link DataTableProps.loadMore} (0.48, keksdose dev #605): a footer in place of the
24
+ * pager that loads the next slice of rows — on its own as it scrolls into view (an
25
+ * IntersectionObserver), and through a button that is always there for the keyboard and
26
+ * the screen reader. The owner appends the rows; the table renders every row it is given.
27
+ */
28
+ export interface DataTableLoadMore {
29
+ /** Whether there is anything left to load. False draws the end state. */
30
+ hasMore: boolean;
31
+ /** Load the next slice. Called at most once per row count while `loading` is false. */
32
+ onLoadMore: () => void;
33
+ /** A load is in flight: the button says "Loading…" and ignores presses. */
34
+ loading?: boolean;
35
+ /** The button's words — what it loads ("Load August 2026"). Default: the kit's "Load more". */
36
+ label?: ReactNode;
37
+ /** What the footer says once `hasMore` is false. Default: the kit's "All rows loaded";
38
+ * `null` draws nothing. */
39
+ endLabel?: ReactNode;
40
+ /** Load as the footer scrolls into view. Default true; false leaves only the button. */
41
+ auto?: boolean;
42
+ }
43
+ /** A run of rows under one band; `start` is its first row's index in the slice. */
44
+ export interface RowGroup<T> {
45
+ key: string;
46
+ rows: T[];
47
+ start: number;
48
+ /** The second level's runs inside this one, or null with one level. */
49
+ children: RowGroup<T>[] | null;
50
+ }
51
+ /** Consecutive runs by `level.key`, nested once for a second level. */
52
+ export declare function groupRows<T>(rows: T[], levels: readonly DataTableGroupBy<T>[], offset?: number): RowGroup<T>[];
53
+ /** `groupBy` as a list of at most two levels. */
54
+ export declare function groupLevels<T>(groupBy: DataTableGroupBy<T> | readonly [DataTableGroupBy<T>, DataTableGroupBy<T>?] | undefined): DataTableGroupBy<T>[];
55
+ /**
56
+ * The bands' sticky offsets: the first level under the header (and under a sticky
57
+ * `leadingRow`), the second under the first. Measured — the header grows at Large text,
58
+ * a band's end slot can wrap — and followed with a ResizeObserver.
59
+ */
60
+ export declare function useGroupBandTops(tableRef: RefObject<HTMLTableElement | null>, enabled: boolean, signature: string): [number, number];
61
+ /**
62
+ * A band row. A `<th scope="rowgroup">` spanning the row — the header of the `<tbody>` it
63
+ * opens, which a screen reader announces as the reader enters the group — and sticky on
64
+ * the cell, not the `<tr>`, as the header's and the leading row's are. Opaque, under the
65
+ * sticky leading row's `z-[5]`: the first level `z-[4]` over the second's `z-[3]`, so a
66
+ * period arriving from below covers the day still pinned under the one before. The line
67
+ * over it is the cell's own `::before`, which moves with the stuck cell.
68
+ */
69
+ export declare function DataTableGroupBand({ level, colSpan, top, compact, label, end, }: {
70
+ level: 1 | 2;
71
+ colSpan: number;
72
+ top: number;
73
+ compact: boolean;
74
+ label: ReactNode;
75
+ end: ReactNode;
76
+ }): import("react").JSX.Element;
77
+ /**
78
+ * The load-more footer. Observed against the viewport (an implicit root sees through the
79
+ * clipping scroller, so it works in a bounded body, under `fillHeight`, and in a page that
80
+ * scrolls), and guarded: one call per row count, none while `loading`, so an observer
81
+ * that reports twice — or a footer still in view while the owner sets `loading` — loads
82
+ * once. When the rows arrive and the footer is still in view (a short slice), it loads
83
+ * again, until the scroller is full.
84
+ */
85
+ export declare function DataTableLoadMoreFooter({ loadMore, rowCount, labels, compact, as: Tag, }: {
86
+ loadMore: DataTableLoadMore;
87
+ rowCount: number;
88
+ labels: {
89
+ loadMore: string;
90
+ allLoaded: string;
91
+ loading: string;
92
+ };
93
+ compact: boolean;
94
+ as?: "div" | "li";
95
+ }): import("react").JSX.Element;
@@ -0,0 +1,165 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useLayoutEffect, useRef, useState } from "react";
4
+ import { cn } from "../lib/cn.js";
5
+ import { Button } from "./button.js";
6
+ import { Spinner } from "./spinner.js";
7
+ function groupRows(rows, levels, offset = 0) {
8
+ const [level, ...rest] = levels;
9
+ if (!level) return [];
10
+ const out = [];
11
+ rows.forEach((row, i) => {
12
+ const key = level.key(row);
13
+ const last = out[out.length - 1];
14
+ if (last && last.key === key) last.rows.push(row);
15
+ else out.push({ key, rows: [row], start: offset + i, children: null });
16
+ });
17
+ if (rest.length > 0) for (const g of out) g.children = groupRows(g.rows, rest.slice(0, 1), g.start);
18
+ return out;
19
+ }
20
+ function groupLevels(groupBy) {
21
+ if (!groupBy) return [];
22
+ const list = "key" in groupBy ? [groupBy] : groupBy;
23
+ return list.filter((l) => !!l).slice(0, 2);
24
+ }
25
+ function useGroupBandTops(tableRef, enabled, signature) {
26
+ const [tops, setTops] = useState([0, 0]);
27
+ useLayoutEffect(() => {
28
+ const table = tableRef.current;
29
+ if (!enabled || !table) return;
30
+ const parts = [
31
+ table.tHead,
32
+ table.querySelector("[data-leading-row='sticky']"),
33
+ table.querySelector("th[data-group-band='1']")
34
+ ];
35
+ const measure = () => {
36
+ const [head, lead, band] = parts.map((el) => el ? el.getBoundingClientRect().height : 0);
37
+ setTops((prev) => prev[0] === head + lead && prev[1] === head + lead + band ? prev : [head + lead, head + lead + band]);
38
+ };
39
+ measure();
40
+ if (typeof ResizeObserver === "undefined") return;
41
+ const observer = new ResizeObserver(measure);
42
+ for (const el of parts) if (el) observer.observe(el);
43
+ return () => observer.disconnect();
44
+ }, [tableRef, enabled, signature]);
45
+ return tops;
46
+ }
47
+ function DataTableGroupBand({
48
+ level,
49
+ colSpan,
50
+ top,
51
+ compact,
52
+ label,
53
+ end
54
+ }) {
55
+ return /* @__PURE__ */ jsx("tr", { "data-group-row": level, children: /* @__PURE__ */ jsx(
56
+ "th",
57
+ {
58
+ scope: "rowgroup",
59
+ colSpan,
60
+ "data-group-band": level,
61
+ style: { top },
62
+ className: cn(
63
+ "sticky p-0 text-start align-middle",
64
+ "before:pointer-events-none before:absolute before:inset-x-0 before:top-0 before:z-[1] before:h-px before:bg-[var(--border)] before:content-['']",
65
+ level === 1 ? "z-[4] bg-surface-2 font-semibold text-primary" : "z-[3] bg-surface font-medium text-secondary",
66
+ level === 1 ? compact ? "text-xs" : "text-sm" : "text-xs"
67
+ ),
68
+ children: /* @__PURE__ */ jsx("div", { className: "w-0 min-w-full", children: /* @__PURE__ */ jsxs(
69
+ "div",
70
+ {
71
+ className: cn(
72
+ "sticky start-0 flex w-[var(--dt-visible-w,100%)] max-w-full flex-wrap items-baseline justify-between gap-x-3",
73
+ compact ? "px-2 py-1" : level === 1 ? "px-3 py-2" : "px-3 py-1.5"
74
+ ),
75
+ children: [
76
+ /* @__PURE__ */ jsx("span", { className: "min-w-0", children: label }),
77
+ end != null && end !== false && /* @__PURE__ */ jsx("span", { className: "ms-auto tabular-nums", children: end })
78
+ ]
79
+ }
80
+ ) })
81
+ }
82
+ ) });
83
+ }
84
+ function DataTableLoadMoreFooter({
85
+ loadMore,
86
+ rowCount,
87
+ labels,
88
+ compact,
89
+ as: Tag = "div"
90
+ }) {
91
+ const { hasMore, onLoadMore, loading = false, auto = true, label, endLabel } = loadMore;
92
+ const rootRef = useRef(null);
93
+ const endRef = useRef(null);
94
+ const firedAt = useRef(null);
95
+ const buttonFocused = useRef(false);
96
+ const latest = useRef({ onLoadMore, loading, rowCount });
97
+ useEffect(() => {
98
+ latest.current = { onLoadMore, loading, rowCount };
99
+ });
100
+ useEffect(() => {
101
+ const el = rootRef.current;
102
+ if (!auto || !hasMore || loading || !el || typeof IntersectionObserver === "undefined") return;
103
+ const io = new IntersectionObserver(
104
+ (entries) => {
105
+ const now = latest.current;
106
+ if (!entries.some((e) => e.isIntersecting) || now.loading || firedAt.current === now.rowCount) return;
107
+ firedAt.current = now.rowCount;
108
+ now.onLoadMore();
109
+ },
110
+ { rootMargin: "200px" }
111
+ );
112
+ io.observe(el);
113
+ return () => io.disconnect();
114
+ }, [auto, hasMore, loading, rowCount]);
115
+ useEffect(() => {
116
+ if (!hasMore && buttonFocused.current) {
117
+ buttonFocused.current = false;
118
+ endRef.current?.focus();
119
+ }
120
+ }, [hasMore]);
121
+ const end = endLabel === void 0 ? labels.allLoaded : endLabel;
122
+ return /* @__PURE__ */ jsxs(
123
+ Tag,
124
+ {
125
+ ref: (el) => {
126
+ rootRef.current = el;
127
+ },
128
+ "data-table-load-more": "",
129
+ className: cn(
130
+ "sticky start-0 flex w-[var(--dt-visible-w,100%)] max-w-full flex-col items-center border-t border-subtle text-center",
131
+ compact ? "px-2 py-2" : "px-3 py-3"
132
+ ),
133
+ children: [
134
+ hasMore ? /* @__PURE__ */ jsx(
135
+ Button,
136
+ {
137
+ variant: "secondary",
138
+ size: "sm",
139
+ "aria-busy": loading || void 0,
140
+ onFocus: () => buttonFocused.current = true,
141
+ onBlur: () => buttonFocused.current = false,
142
+ onClick: () => {
143
+ if (loading) return;
144
+ firedAt.current = rowCount;
145
+ onLoadMore();
146
+ },
147
+ children: loading ? /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-2", children: [
148
+ /* @__PURE__ */ jsx(Spinner, { label: null, className: "size-4" }),
149
+ /* @__PURE__ */ jsx("bdi", { children: labels.loading })
150
+ ] }) : label ?? labels.loadMore
151
+ }
152
+ ) : end != null && end !== false ? /* @__PURE__ */ jsx("p", { ref: endRef, tabIndex: -1, className: "text-xs text-muted outline-none", children: end }) : null,
153
+ /* @__PURE__ */ jsx("span", { role: "status", className: "sr-only", children: loading ? labels.loading : "" })
154
+ ]
155
+ }
156
+ );
157
+ }
158
+ export {
159
+ DataTableGroupBand,
160
+ DataTableLoadMoreFooter,
161
+ groupLevels,
162
+ groupRows,
163
+ useGroupBandTops
164
+ };
165
+ //# sourceMappingURL=data-table-groups.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/data-table-groups.tsx"],"sourcesContent":["import { useEffect, useLayoutEffect, useRef, useState } from \"react\";\nimport type { ReactNode, RefObject } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { Button } from \"./button\";\nimport { Spinner } from \"./spinner\";\n\n/**\n * One level of {@link DataTableProps.groupBy} (0.48, keksdose dev #605): the rows are cut\n * into consecutive runs of equal `key` — the rows must already be ordered so equal keys\n * are contiguous, as for `mobileGroupBy` — and each run is headed by a full-width band.\n *\n * The same object drives the phone: `mobileGroupBy` takes a level too, so\n * `groupBy={[period, day]} mobileGroupBy={period}` is one definition for both layouts.\n */\nexport interface DataTableGroupBy<T> {\n /** The group a row belongs to. */\n key: (row: T) => string;\n /** The band's label. Default: the key itself. */\n label?: (key: string, rows: T[]) => ReactNode;\n /**\n * The band's end slot — a count, a total (\"176 · +8'782.30 · −3'200.10\"). `rows` are\n * the group's rows the table holds; an app whose total covers rows not yet loaded\n * looks it up by `key` instead.\n */\n end?: (key: string, rows: T[]) => ReactNode;\n}\n\n/**\n * {@link DataTableProps.loadMore} (0.48, keksdose dev #605): a footer in place of the\n * pager that loads the next slice of rows — on its own as it scrolls into view (an\n * IntersectionObserver), and through a button that is always there for the keyboard and\n * the screen reader. The owner appends the rows; the table renders every row it is given.\n */\nexport interface DataTableLoadMore {\n /** Whether there is anything left to load. False draws the end state. */\n hasMore: boolean;\n /** Load the next slice. Called at most once per row count while `loading` is false. */\n onLoadMore: () => void;\n /** A load is in flight: the button says \"Loading…\" and ignores presses. */\n loading?: boolean;\n /** The button's words — what it loads (\"Load August 2026\"). Default: the kit's \"Load more\". */\n label?: ReactNode;\n /** What the footer says once `hasMore` is false. Default: the kit's \"All rows loaded\";\n * `null` draws nothing. */\n endLabel?: ReactNode;\n /** Load as the footer scrolls into view. Default true; false leaves only the button. */\n auto?: boolean;\n}\n\n/** A run of rows under one band; `start` is its first row's index in the slice. */\nexport interface RowGroup<T> {\n key: string;\n rows: T[];\n start: number;\n /** The second level's runs inside this one, or null with one level. */\n children: RowGroup<T>[] | null;\n}\n\n/** Consecutive runs by `level.key`, nested once for a second level. */\nexport function groupRows<T>(rows: T[], levels: readonly DataTableGroupBy<T>[], offset = 0): RowGroup<T>[] {\n const [level, ...rest] = levels;\n if (!level) return [];\n const out: RowGroup<T>[] = [];\n rows.forEach((row, i) => {\n const key = level.key(row);\n const last = out[out.length - 1];\n if (last && last.key === key) last.rows.push(row);\n else out.push({ key, rows: [row], start: offset + i, children: null });\n });\n if (rest.length > 0) for (const g of out) g.children = groupRows(g.rows, rest.slice(0, 1), g.start);\n return out;\n}\n\n/** `groupBy` as a list of at most two levels. */\nexport function groupLevels<T>(\n groupBy: DataTableGroupBy<T> | readonly [DataTableGroupBy<T>, DataTableGroupBy<T>?] | undefined,\n): DataTableGroupBy<T>[] {\n if (!groupBy) return [];\n const list = \"key\" in groupBy ? [groupBy] : (groupBy as readonly (DataTableGroupBy<T> | undefined)[]);\n return list.filter((l): l is DataTableGroupBy<T> => !!l).slice(0, 2);\n}\n\n/**\n * The bands' sticky offsets: the first level under the header (and under a sticky\n * `leadingRow`), the second under the first. Measured — the header grows at Large text,\n * a band's end slot can wrap — and followed with a ResizeObserver.\n */\nexport function useGroupBandTops(\n tableRef: RefObject<HTMLTableElement | null>,\n enabled: boolean,\n signature: string,\n): [number, number] {\n const [tops, setTops] = useState<[number, number]>([0, 0]);\n useLayoutEffect(() => {\n const table = tableRef.current;\n if (!enabled || !table) return;\n const parts = [\n table.tHead,\n table.querySelector<HTMLElement>(\"[data-leading-row='sticky']\"),\n table.querySelector<HTMLElement>(\"th[data-group-band='1']\"),\n ];\n const measure = () => {\n const [head, lead, band] = parts.map((el) => (el ? el.getBoundingClientRect().height : 0));\n setTops((prev) => (prev[0] === head + lead && prev[1] === head + lead + band ? prev : [head + lead, head + lead + band]));\n };\n measure();\n if (typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver(measure);\n for (const el of parts) if (el) observer.observe(el);\n return () => observer.disconnect();\n }, [tableRef, enabled, signature]);\n return tops;\n}\n\n/**\n * A band row. A `<th scope=\"rowgroup\">` spanning the row — the header of the `<tbody>` it\n * opens, which a screen reader announces as the reader enters the group — and sticky on\n * the cell, not the `<tr>`, as the header's and the leading row's are. Opaque, under the\n * sticky leading row's `z-[5]`: the first level `z-[4]` over the second's `z-[3]`, so a\n * period arriving from below covers the day still pinned under the one before. The line\n * over it is the cell's own `::before`, which moves with the stuck cell.\n */\nexport function DataTableGroupBand({\n level,\n colSpan,\n top,\n compact,\n label,\n end,\n}: {\n level: 1 | 2;\n colSpan: number;\n top: number;\n compact: boolean;\n label: ReactNode;\n end: ReactNode;\n}) {\n return (\n <tr data-group-row={level}>\n <th\n scope=\"rowgroup\"\n colSpan={colSpan}\n data-group-band={level}\n style={{ top }}\n className={cn(\n \"sticky p-0 text-start align-middle\",\n \"before:pointer-events-none before:absolute before:inset-x-0 before:top-0 before:z-[1] before:h-px before:bg-[var(--border)] before:content-['']\",\n level === 1 ? \"z-[4] bg-surface-2 font-semibold text-primary\" : \"z-[3] bg-surface font-medium text-secondary\",\n level === 1 ? (compact ? \"text-xs\" : \"text-sm\") : \"text-xs\",\n )}\n >\n {/* Pinned to what the scroller shows, as the expansion row's panel is: the end\n slot stays at the visible end of a table wider than its scroller. */}\n <div className=\"w-0 min-w-full\">\n <div\n className={cn(\n \"sticky start-0 flex w-[var(--dt-visible-w,100%)] max-w-full flex-wrap items-baseline justify-between gap-x-3\",\n compact ? \"px-2 py-1\" : level === 1 ? \"px-3 py-2\" : \"px-3 py-1.5\",\n )}\n >\n <span className=\"min-w-0\">{label}</span>\n {end != null && end !== false && <span className=\"ms-auto tabular-nums\">{end}</span>}\n </div>\n </div>\n </th>\n </tr>\n );\n}\n\n/**\n * The load-more footer. Observed against the viewport (an implicit root sees through the\n * clipping scroller, so it works in a bounded body, under `fillHeight`, and in a page that\n * scrolls), and guarded: one call per row count, none while `loading`, so an observer\n * that reports twice — or a footer still in view while the owner sets `loading` — loads\n * once. When the rows arrive and the footer is still in view (a short slice), it loads\n * again, until the scroller is full.\n */\nexport function DataTableLoadMoreFooter({\n loadMore,\n rowCount,\n labels,\n compact,\n as: Tag = \"div\",\n}: {\n loadMore: DataTableLoadMore;\n rowCount: number;\n labels: { loadMore: string; allLoaded: string; loading: string };\n compact: boolean;\n as?: \"div\" | \"li\";\n}) {\n const { hasMore, onLoadMore, loading = false, auto = true, label, endLabel } = loadMore;\n const rootRef = useRef<HTMLElement | null>(null);\n const endRef = useRef<HTMLParagraphElement | null>(null);\n const firedAt = useRef<number | null>(null);\n const buttonFocused = useRef(false);\n // What the observer's callback acts on: the latest props, not the ones it was made with.\n const latest = useRef({ onLoadMore, loading, rowCount });\n useEffect(() => {\n latest.current = { onLoadMore, loading, rowCount };\n });\n\n // A fresh observer whenever the rows or the in-flight flag change: its first report is\n // the footer's position AFTER the new rows are laid out. Acting on a remembered \"in\n // view\" instead fired a second load the moment the rows arrived, while the footer was\n // still where the old last row had been (headless Chromium caught it).\n useEffect(() => {\n const el = rootRef.current;\n if (!auto || !hasMore || loading || !el || typeof IntersectionObserver === \"undefined\") return;\n const io = new IntersectionObserver(\n (entries) => {\n const now = latest.current;\n if (!entries.some((e) => e.isIntersecting) || now.loading || firedAt.current === now.rowCount) return;\n firedAt.current = now.rowCount;\n now.onLoadMore();\n },\n { rootMargin: \"200px\" },\n );\n io.observe(el);\n return () => io.disconnect();\n }, [auto, hasMore, loading, rowCount]);\n\n // A focused button that goes away (nothing left) hands its focus to the end line.\n useEffect(() => {\n if (!hasMore && buttonFocused.current) {\n buttonFocused.current = false;\n endRef.current?.focus();\n }\n }, [hasMore]);\n\n const end = endLabel === undefined ? labels.allLoaded : endLabel;\n return (\n <Tag\n ref={(el: HTMLElement | null) => {\n rootRef.current = el;\n }}\n data-table-load-more=\"\"\n className={cn(\n \"sticky start-0 flex w-[var(--dt-visible-w,100%)] max-w-full flex-col items-center border-t border-subtle text-center\",\n compact ? \"px-2 py-2\" : \"px-3 py-3\",\n )}\n >\n {hasMore ? (\n <Button\n variant=\"secondary\"\n size=\"sm\"\n aria-busy={loading || undefined}\n onFocus={() => (buttonFocused.current = true)}\n onBlur={() => (buttonFocused.current = false)}\n onClick={() => {\n if (loading) return;\n firedAt.current = rowCount;\n onLoadMore();\n }}\n >\n {loading ? (\n <span className=\"inline-flex items-center gap-2\">\n <Spinner label={null} className=\"size-4\" />\n <bdi>{labels.loading}</bdi>\n </span>\n ) : (\n (label ?? labels.loadMore)\n )}\n </Button>\n ) : end != null && end !== false ? (\n <p ref={endRef} tabIndex={-1} className=\"text-xs text-muted outline-none\">\n {end}\n </p>\n ) : null}\n {/* Mounted throughout, so a screen reader hears the load start. */}\n <span role=\"status\" className=\"sr-only\">\n {loading ? labels.loading : \"\"}\n </span>\n </Tag>\n );\n}\n"],"mappings":"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","names":[]}
@@ -9,6 +9,7 @@ import { Popover } from "./popover.js";
9
9
  import { Tooltip } from "./tooltip.js";
10
10
  import { isTableOwnColumn } from "./data-table-row-actions.js";
11
11
  import { columnLabel, logicalAlign, splitCellProps } from "./data-table-shared.js";
12
+ import { GROW_HEAD_CLASS } from "./data-table-fit.js";
12
13
  const END_ALIGN = /(^|\s)text-end(?=\s|$)/;
13
14
  function DataTableHead({
14
15
  visibleColumns,
@@ -90,6 +91,7 @@ function DataTableHead({
90
91
  // Each <th> carries its own bg so the row doesn't render
91
92
  // transparent over the data rows underneath.
92
93
  "sticky top-0 z-10 bg-surface-2 backdrop-blur-sm",
94
+ col.grow && GROW_HEAD_CLASS,
93
95
  logicalAlign(ownHead.className),
94
96
  headClass
95
97
  )
@@ -157,7 +159,7 @@ function DataTableHead({
157
159
  ]
158
160
  }
159
161
  ),
160
- resizable && !isTableOwnColumn(col.key) && /* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- a
162
+ resizable && !isTableOwnColumn(col.key) && !col.grow && /* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- a
161
163
  pointer-only drag handle for a visual preference (column width); it is
162
164
  deliberately not a tab stop, and no content or action depends on it. */
163
165
  /* @__PURE__ */ jsx(