@pilotiq/pilotiq 0.6.2 → 0.7.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 (197) hide show
  1. package/.turbo/turbo-build.log +6 -2
  2. package/CHANGELOG.md +614 -0
  3. package/CLAUDE.md +6 -5
  4. package/dist/Column.d.ts +35 -0
  5. package/dist/Column.d.ts.map +1 -1
  6. package/dist/Column.js +41 -0
  7. package/dist/Column.js.map +1 -1
  8. package/dist/Page.d.ts +13 -4
  9. package/dist/Page.d.ts.map +1 -1
  10. package/dist/Page.js +9 -2
  11. package/dist/Page.js.map +1 -1
  12. package/dist/Pilotiq.d.ts +84 -0
  13. package/dist/Pilotiq.d.ts.map +1 -1
  14. package/dist/Pilotiq.js +66 -0
  15. package/dist/Pilotiq.js.map +1 -1
  16. package/dist/Resource.d.ts +26 -0
  17. package/dist/Resource.d.ts.map +1 -1
  18. package/dist/Resource.js +9 -0
  19. package/dist/Resource.js.map +1 -1
  20. package/dist/actions/exportFactory.js +1 -1
  21. package/dist/actions/exportFactory.js.map +1 -1
  22. package/dist/columns/SelectColumn.d.ts +32 -5
  23. package/dist/columns/SelectColumn.d.ts.map +1 -1
  24. package/dist/columns/SelectColumn.js +37 -7
  25. package/dist/columns/SelectColumn.js.map +1 -1
  26. package/dist/defaultPages.d.ts.map +1 -1
  27. package/dist/defaultPages.js +3 -0
  28. package/dist/defaultPages.js.map +1 -1
  29. package/dist/elements/Form.d.ts +17 -0
  30. package/dist/elements/Form.d.ts.map +1 -1
  31. package/dist/elements/Form.js +17 -0
  32. package/dist/elements/Form.js.map +1 -1
  33. package/dist/elements/Table.d.ts +26 -0
  34. package/dist/elements/Table.d.ts.map +1 -1
  35. package/dist/elements/Table.js +15 -1
  36. package/dist/elements/Table.js.map +1 -1
  37. package/dist/elements/TableGroup.d.ts +84 -0
  38. package/dist/elements/TableGroup.d.ts.map +1 -1
  39. package/dist/elements/TableGroup.js +103 -0
  40. package/dist/elements/TableGroup.js.map +1 -1
  41. package/dist/elements/dispatchForm.d.ts.map +1 -1
  42. package/dist/elements/dispatchForm.js +36 -6
  43. package/dist/elements/dispatchForm.js.map +1 -1
  44. package/dist/elements/dispatchTable.d.ts +12 -0
  45. package/dist/elements/dispatchTable.d.ts.map +1 -1
  46. package/dist/elements/dispatchTable.js +103 -28
  47. package/dist/elements/dispatchTable.js.map +1 -1
  48. package/dist/fields/Field.d.ts +7 -2
  49. package/dist/fields/Field.d.ts.map +1 -1
  50. package/dist/fields/Field.js +8 -3
  51. package/dist/fields/Field.js.map +1 -1
  52. package/dist/fields/RepeaterField.d.ts +65 -0
  53. package/dist/fields/RepeaterField.d.ts.map +1 -1
  54. package/dist/fields/RepeaterField.js +48 -0
  55. package/dist/fields/RepeaterField.js.map +1 -1
  56. package/dist/orm/modelDefaults.d.ts.map +1 -1
  57. package/dist/orm/modelDefaults.js +19 -0
  58. package/dist/orm/modelDefaults.js.map +1 -1
  59. package/dist/pageData.d.ts +20 -0
  60. package/dist/pageData.d.ts.map +1 -1
  61. package/dist/pageData.js +242 -34
  62. package/dist/pageData.js.map +1 -1
  63. package/dist/react/AppShell.d.ts +17 -1
  64. package/dist/react/AppShell.d.ts.map +1 -1
  65. package/dist/react/AppShell.js +34 -3
  66. package/dist/react/AppShell.js.map +1 -1
  67. package/dist/react/PendingSuggestionApplierRegistry.d.ts +34 -0
  68. package/dist/react/PendingSuggestionApplierRegistry.d.ts.map +1 -0
  69. package/dist/react/PendingSuggestionApplierRegistry.js +51 -0
  70. package/dist/react/PendingSuggestionApplierRegistry.js.map +1 -0
  71. package/dist/react/PendingSuggestionOverlayRegistry.d.ts +46 -0
  72. package/dist/react/PendingSuggestionOverlayRegistry.d.ts.map +1 -0
  73. package/dist/react/PendingSuggestionOverlayRegistry.js +16 -0
  74. package/dist/react/PendingSuggestionOverlayRegistry.js.map +1 -0
  75. package/dist/react/PendingSuggestionsContext.d.ts +153 -0
  76. package/dist/react/PendingSuggestionsContext.d.ts.map +1 -0
  77. package/dist/react/PendingSuggestionsContext.js +46 -0
  78. package/dist/react/PendingSuggestionsContext.js.map +1 -0
  79. package/dist/react/SchemaRenderer.d.ts.map +1 -1
  80. package/dist/react/SchemaRenderer.js +312 -39
  81. package/dist/react/SchemaRenderer.js.map +1 -1
  82. package/dist/react/cells/EditableCell.d.ts +8 -0
  83. package/dist/react/cells/EditableCell.d.ts.map +1 -1
  84. package/dist/react/cells/EditableCell.js +6 -2
  85. package/dist/react/cells/EditableCell.js.map +1 -1
  86. package/dist/react/fields/CheckboxListInput.d.ts.map +1 -1
  87. package/dist/react/fields/CheckboxListInput.js +29 -2
  88. package/dist/react/fields/CheckboxListInput.js.map +1 -1
  89. package/dist/react/fields/ColorInput.d.ts.map +1 -1
  90. package/dist/react/fields/ColorInput.js +28 -2
  91. package/dist/react/fields/ColorInput.js.map +1 -1
  92. package/dist/react/fields/DateTimeInput.d.ts.map +1 -1
  93. package/dist/react/fields/DateTimeInput.js +28 -2
  94. package/dist/react/fields/DateTimeInput.js.map +1 -1
  95. package/dist/react/fields/FieldShell.d.ts.map +1 -1
  96. package/dist/react/fields/FieldShell.js +161 -3
  97. package/dist/react/fields/FieldShell.js.map +1 -1
  98. package/dist/react/fields/FileUploadInput.d.ts.map +1 -1
  99. package/dist/react/fields/FileUploadInput.js +27 -2
  100. package/dist/react/fields/FileUploadInput.js.map +1 -1
  101. package/dist/react/fields/KeyValueInput.d.ts.map +1 -1
  102. package/dist/react/fields/KeyValueInput.js +33 -2
  103. package/dist/react/fields/KeyValueInput.js.map +1 -1
  104. package/dist/react/fields/RadioInput.d.ts.map +1 -1
  105. package/dist/react/fields/RadioInput.js +28 -2
  106. package/dist/react/fields/RadioInput.js.map +1 -1
  107. package/dist/react/fields/SelectFieldInput.d.ts.map +1 -1
  108. package/dist/react/fields/SelectFieldInput.js +31 -2
  109. package/dist/react/fields/SelectFieldInput.js.map +1 -1
  110. package/dist/react/fields/SliderInput.d.ts.map +1 -1
  111. package/dist/react/fields/SliderInput.js +26 -2
  112. package/dist/react/fields/SliderInput.js.map +1 -1
  113. package/dist/react/fields/TagsInput.d.ts.map +1 -1
  114. package/dist/react/fields/TagsInput.js +26 -2
  115. package/dist/react/fields/TagsInput.js.map +1 -1
  116. package/dist/react/fields/ToggleFieldInput.d.ts.map +1 -1
  117. package/dist/react/fields/ToggleFieldInput.js +29 -2
  118. package/dist/react/fields/ToggleFieldInput.js.map +1 -1
  119. package/dist/react/index.d.ts +3 -0
  120. package/dist/react/index.d.ts.map +1 -1
  121. package/dist/react/index.js +3 -0
  122. package/dist/react/index.js.map +1 -1
  123. package/dist/routes.d.ts.map +1 -1
  124. package/dist/routes.js +55 -2
  125. package/dist/routes.js.map +1 -1
  126. package/dist/schema/Section.d.ts +16 -0
  127. package/dist/schema/Section.d.ts.map +1 -1
  128. package/dist/schema/Section.js +16 -0
  129. package/dist/schema/Section.js.map +1 -1
  130. package/dist/schema/Wizard.d.ts +45 -0
  131. package/dist/schema/Wizard.d.ts.map +1 -1
  132. package/dist/schema/Wizard.js +50 -0
  133. package/dist/schema/Wizard.js.map +1 -1
  134. package/dist/schema/resolveSchema.d.ts +8 -0
  135. package/dist/schema/resolveSchema.d.ts.map +1 -1
  136. package/dist/schema/resolveSchema.js +70 -1
  137. package/dist/schema/resolveSchema.js.map +1 -1
  138. package/dist/sessionFilters.d.ts.map +1 -1
  139. package/dist/sessionFilters.js +12 -1
  140. package/dist/sessionFilters.js.map +1 -1
  141. package/dist/styles/file-upload.css +13 -0
  142. package/dist/vite.d.ts.map +1 -1
  143. package/dist/vite.js +19 -12
  144. package/dist/vite.js.map +1 -1
  145. package/package.json +6 -4
  146. package/src/Column.test.ts +36 -0
  147. package/src/Column.ts +54 -0
  148. package/src/Page.ts +13 -4
  149. package/src/Pilotiq.ts +109 -0
  150. package/src/Resource.ts +29 -0
  151. package/src/actions/exportFactory.ts +1 -1
  152. package/src/columns/SelectColumn.ts +46 -8
  153. package/src/columns/editableColumns.test.ts +45 -0
  154. package/src/defaultPages.ts +3 -0
  155. package/src/elements/Form.ts +19 -0
  156. package/src/elements/Table.ts +35 -1
  157. package/src/elements/TableGroup.test.ts +111 -0
  158. package/src/elements/TableGroup.ts +135 -0
  159. package/src/elements/dispatchForm.ts +34 -7
  160. package/src/elements/dispatchTable.test.ts +267 -0
  161. package/src/elements/dispatchTable.ts +111 -32
  162. package/src/fields/Field.test.ts +15 -0
  163. package/src/fields/Field.ts +8 -3
  164. package/src/fields/RepeaterField.ts +104 -0
  165. package/src/fields/RepeaterRelationship.test.ts +173 -0
  166. package/src/nestedRelationManagerData.test.ts +21 -0
  167. package/src/orm/modelDefaults.ts +21 -0
  168. package/src/pageData.ts +267 -47
  169. package/src/react/AppShell.tsx +55 -4
  170. package/src/react/PendingSuggestionApplierRegistry.ts +80 -0
  171. package/src/react/PendingSuggestionOverlayRegistry.ts +54 -0
  172. package/src/react/PendingSuggestionsContext.tsx +172 -0
  173. package/src/react/SchemaRenderer.tsx +504 -95
  174. package/src/react/cells/EditableCell.tsx +11 -2
  175. package/src/react/fields/CheckboxListInput.tsx +23 -2
  176. package/src/react/fields/ColorInput.tsx +22 -2
  177. package/src/react/fields/DateTimeInput.tsx +22 -2
  178. package/src/react/fields/FieldShell.tsx +167 -3
  179. package/src/react/fields/FileUploadInput.tsx +21 -2
  180. package/src/react/fields/KeyValueInput.tsx +32 -2
  181. package/src/react/fields/RadioInput.tsx +23 -2
  182. package/src/react/fields/SelectFieldInput.tsx +25 -2
  183. package/src/react/fields/SliderInput.tsx +20 -2
  184. package/src/react/fields/TagsInput.tsx +20 -2
  185. package/src/react/fields/ToggleFieldInput.tsx +23 -2
  186. package/src/react/index.ts +18 -0
  187. package/src/relationManagerData.test.ts +451 -2
  188. package/src/routes.ts +58 -2
  189. package/src/schema/Section.ts +17 -0
  190. package/src/schema/Wizard.ts +67 -0
  191. package/src/schema/containers.test.ts +90 -0
  192. package/src/schema/resolveSchema.test.ts +50 -0
  193. package/src/schema/resolveSchema.ts +79 -1
  194. package/src/sessionFilters.test.ts +23 -0
  195. package/src/sessionFilters.ts +11 -1
  196. package/src/styles/file-upload.css +13 -0
  197. package/src/vite.ts +19 -12
@@ -32,7 +32,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '
32
32
  import { Table as DataTable, TableBody, TableCell, TableFooter, TableHead, TableHeader, TableRow, } from './ui/table.js';
33
33
  import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from './ui/dropdown-menu.js';
34
34
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from './ui/tooltip.js';
35
- import { FilterIcon, CircleIcon, InboxIcon, GripVerticalIcon, ChevronDownIcon, CopyIcon, CheckIcon, XIcon, InfoIcon, TriangleAlertIcon, CircleCheckIcon, CircleAlertIcon, } from 'lucide-react';
35
+ import { FilterIcon, CircleIcon, InboxIcon, GripVerticalIcon, ChevronDownIcon, CopyIcon, CheckIcon, XIcon, InfoIcon, TriangleAlertIcon, CircleCheckIcon, CircleAlertIcon, Columns3Icon, } from 'lucide-react';
36
36
  import { useNavigate } from './navigate.js';
37
37
  import { parseDateRangeValue, encodeDateRangeValue, } from '../filters/DateRangeFilter.js';
38
38
  import { parseMultiSelectValue, encodeMultiSelectValue, } from '../filters/MultiSelectFilter.js';
@@ -1094,16 +1094,59 @@ function SectionRenderer({ el, index }) {
1094
1094
  return (_jsxs("section", { className: `flex flex-col ${compact ? 'gap-2' : 'gap-3'} rounded-lg border ${surfaceClass} ${padding} ${layoutClasses(el)}`.trim(), children: [(title || description || collapsible || badge || afterHeader.length > 0) && (_jsxs("header", { className: "flex items-start justify-between gap-2", children: [_jsxs("div", { className: "flex items-start gap-2", children: [Icon && _jsx(Icon, { className: "size-4 mt-0.5 text-muted-foreground", "aria-hidden": "true" }), _jsxs("div", { children: [_jsxs("div", { className: "flex items-center gap-2", children: [title && _jsx("h3", { className: `${titleSize} font-semibold`, children: title }), badge && (_jsx("span", { className: "rounded-full bg-muted px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide text-muted-foreground", children: badge }))] }), description && _jsx("p", { className: "text-xs text-muted-foreground mt-0.5", children: description })] })] }), _jsxs("div", { className: "flex items-center gap-2", children: [afterHeader.length > 0 && (_jsx("div", { className: "flex items-center gap-1", children: afterHeader.map((a, i) => renderElement(a, i)) })), collapsible && (_jsx("button", { type: "button", onClick: () => setCollapsed(c => !c), className: "text-xs text-muted-foreground hover:text-foreground", children: collapsed ? 'Expand' : 'Collapse' }))] })] })), !collapsed && el.children && el.children.length > 0 && (_jsx("div", { className: gridClass, children: el.children.map((c, i) => renderElement(c, i)) }))] }, index));
1095
1095
  }
1096
1096
  // ─── Wizard (Plan #8) ───────────────────────────────────────
1097
+ /**
1098
+ * Resolve the initial active step for `WizardRenderer`. Priority:
1099
+ * 1. URL `?<queryKey>=N` (1-based — wizards expose human-friendly indexes
1100
+ * when `Wizard.persistStepInQueryString()` is enabled).
1101
+ * 2. `localStorage[<storageKey>]` (0-based, set by the persist effect).
1102
+ * 3. `startOnStep` configured on the Wizard.
1103
+ *
1104
+ * SSR-safe: returns `startOnStep` when `window` is undefined.
1105
+ */
1106
+ function readInitialWizardStep(total, startOnStep, storageKey, queryKey) {
1107
+ if (typeof window === 'undefined')
1108
+ return startOnStep;
1109
+ if (queryKey) {
1110
+ try {
1111
+ const raw = new URL(window.location.href).searchParams.get(queryKey);
1112
+ if (raw !== null && raw !== '') {
1113
+ const n = Number(raw) - 1;
1114
+ if (Number.isFinite(n) && n >= 0 && n < total)
1115
+ return n;
1116
+ }
1117
+ }
1118
+ catch { /* ignore */ }
1119
+ }
1120
+ if (storageKey) {
1121
+ try {
1122
+ const stored = window.localStorage.getItem(storageKey);
1123
+ if (stored !== null) {
1124
+ const n = Number(stored);
1125
+ if (Number.isFinite(n) && n >= 0 && n < total)
1126
+ return n;
1127
+ }
1128
+ }
1129
+ catch { /* ignore */ }
1130
+ }
1131
+ return startOnStep;
1132
+ }
1097
1133
  /**
1098
1134
  * Multi-step form layout. Tracks active step in `useState`, optionally
1099
- * persisted to localStorage. On Next click, POSTs `{ step, values }` to
1100
- * the form's `wizardUrl` (stamped by the route handler when the form
1101
- * has a Wizard descendant). 200 → advance; 422 → stamp inline errors;
1102
- * absent `wizardUrl` → advance immediately (no validation).
1135
+ * persisted to localStorage and/or the URL query string. On Next click,
1136
+ * POSTs `{ step, values }` to the form's `wizardUrl` (stamped by the
1137
+ * route handler when the form has a Wizard descendant). 200 → advance;
1138
+ * 422 → stamp inline errors; absent `wizardUrl` → advance immediately
1139
+ * (no validation).
1103
1140
  *
1104
1141
  * Inactive steps render hidden (display:none) rather than unmounted so
1105
1142
  * controlled inputs preserve their values across step transitions and
1106
1143
  * cross-step `$get` works on the resolved meta.
1144
+ *
1145
+ * Nav buttons honor `Wizard.submitAction() / nextAction() / previousAction()`
1146
+ * — chrome (label / icon / color / size / outlined / iconOnly / tooltip /
1147
+ * disabled rules) carries through to the rendered button while the click
1148
+ * behavior stays hardwired (advance / recede / submit-form). Bare wizards
1149
+ * keep the built-in defaults.
1107
1150
  */
1108
1151
  function WizardRenderer({ el, index }) {
1109
1152
  const formState = useFormState();
@@ -1114,37 +1157,47 @@ function WizardRenderer({ el, index }) {
1114
1157
  const startOnStep = Math.max(0, Math.min(Math.max(0, steps.length - 1), Number(el['startOnStep'] ?? 0)));
1115
1158
  const persist = el['persist'] !== false;
1116
1159
  const storageKey = persist && formId ? `pilotiq.wizard.${formId}.step` : undefined;
1117
- const [active, setActive] = useState(startOnStep);
1160
+ const queryKey = typeof el['persistStepInQueryString'] === 'string'
1161
+ ? String(el['persistStepInQueryString'])
1162
+ : undefined;
1163
+ const submitActionMeta = el['submitAction'];
1164
+ const nextActionMeta = el['nextAction'];
1165
+ const previousActionMeta = el['previousAction'];
1166
+ // Initial-step resolution priority: URL (?<key>=N, 1-based) > localStorage >
1167
+ // startOnStep. URL wins on first paint so deep links land on the right step
1168
+ // before localStorage can override. Lazy initializer — resolution runs once.
1169
+ const [active, setActive] = useState(() => readInitialWizardStep(steps.length, startOnStep, storageKey, queryKey));
1118
1170
  const [advancing, setAdvancing] = useState(false);
1119
1171
  const [advanceError, setAdvanceError] = useState(null);
1120
- // Hydrate persisted step from localStorage after mount.
1172
+ // Persist active step changes to localStorage (when enabled).
1121
1173
  useEffect(() => {
1122
1174
  if (!storageKey)
1123
1175
  return;
1124
1176
  if (typeof window === 'undefined')
1125
1177
  return;
1126
1178
  try {
1127
- const stored = window.localStorage.getItem(storageKey);
1128
- if (stored !== null) {
1129
- const n = Number(stored);
1130
- if (Number.isFinite(n) && n >= 0 && n < steps.length)
1131
- setActive(n);
1132
- }
1179
+ window.localStorage.setItem(storageKey, String(active));
1133
1180
  }
1134
1181
  catch { /* ignore */ }
1135
- // eslint-disable-next-line react-hooks/exhaustive-deps
1136
- }, [storageKey]);
1137
- // Persist active step changes.
1182
+ }, [storageKey, active]);
1183
+ // Mirror active step to the URL via replaceState — purely client-side state
1184
+ // sync, no SPA re-fetch. 1-based externally; cleared when on the first step
1185
+ // so bare URLs don't grow ?step=1 noise.
1138
1186
  useEffect(() => {
1139
- if (!storageKey)
1187
+ if (!queryKey)
1140
1188
  return;
1141
1189
  if (typeof window === 'undefined')
1142
1190
  return;
1143
1191
  try {
1144
- window.localStorage.setItem(storageKey, String(active));
1192
+ const url = new URL(window.location.href);
1193
+ if (active === 0)
1194
+ url.searchParams.delete(queryKey);
1195
+ else
1196
+ url.searchParams.set(queryKey, String(active + 1));
1197
+ window.history.replaceState(window.history.state, '', url.toString());
1145
1198
  }
1146
1199
  catch { /* ignore */ }
1147
- }, [storageKey, active]);
1200
+ }, [queryKey, active]);
1148
1201
  if (steps.length === 0) {
1149
1202
  return (_jsx("div", { className: "rounded-lg border border-dashed p-8 text-center text-sm text-muted-foreground", children: "No steps configured." }, index));
1150
1203
  }
@@ -1211,9 +1264,43 @@ function WizardRenderer({ el, index }) {
1211
1264
  : isDone ? 'bg-muted-foreground/20 text-foreground'
1212
1265
  : 'bg-muted text-muted-foreground',
1213
1266
  ].join(' '), children: Icon ? _jsx(Icon, { className: "size-3", "aria-hidden": "true" }) : i + 1 }), _jsx("span", { className: "font-medium", children: String(s['label'] ?? `Step ${i + 1}`) })] }), i < steps.length - 1 && _jsx("span", { className: "h-px w-6 bg-border", "aria-hidden": "true" })] }, i));
1214
- }) }), Boolean(steps[active]?.['description']) && (_jsx("p", { className: "text-sm text-muted-foreground", children: String(steps[active]['description']) })), steps.map((s, i) => (_jsx("div", { className: i === active ? 'flex flex-col gap-4' : 'hidden', "aria-hidden": i === active ? undefined : true, children: (s.children ?? []).map((c, ci) => renderElement(c, ci)) }, i))), advanceError && (_jsx("p", { className: "text-sm text-destructive", role: "alert", children: advanceError })), _jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx("button", { type: "button", disabled: isFirst || advancing, onClick: () => advance(active - 1), className: "rounded-md border border-border px-3 py-1.5 text-sm font-medium text-foreground hover:bg-muted disabled:opacity-50 disabled:cursor-not-allowed", children: "Back" }), isLast
1215
- ? _jsx("span", { className: "text-xs text-muted-foreground", children: "Submit the form to finish." })
1216
- : _jsx("button", { type: "button", disabled: advancing, onClick: () => advance(active + 1), className: "rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:opacity-90 disabled:opacity-50 disabled:cursor-not-allowed", children: advancing ? 'Validating…' : 'Next' })] })] }, index));
1267
+ }) }), Boolean(steps[active]?.['description']) && (_jsx("p", { className: "text-sm text-muted-foreground", children: String(steps[active]['description']) })), steps.map((s, i) => (_jsx("div", { className: i === active ? 'flex flex-col gap-4' : 'hidden', "aria-hidden": i === active ? undefined : true, children: (s.children ?? []).map((c, ci) => renderElement(c, ci)) }, i))), advanceError && (_jsx("p", { className: "text-sm text-destructive", role: "alert", children: advanceError })), _jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx(WizardNavButton, { actionMeta: previousActionMeta, fallbackLabel: "Back", disabled: isFirst || advancing, onClick: () => advance(active - 1) }), isLast
1268
+ ? (submitActionMeta
1269
+ ? _jsx(WizardNavButton, { actionMeta: submitActionMeta, fallbackLabel: "Submit", type: "submit", disabled: advancing })
1270
+ : _jsx("span", { className: "text-xs text-muted-foreground", children: "Submit the form to finish." }))
1271
+ : _jsx(WizardNavButton, { actionMeta: nextActionMeta, fallbackLabel: advancing ? 'Validating…' : 'Next', disabled: advancing, onClick: () => advance(active + 1) })] })] }, index));
1272
+ }
1273
+ /**
1274
+ * Renders one wizard nav slot (Back / Next / Submit). Falls back to plain
1275
+ * built-in chrome (border button for Back, primary button for Next/Submit)
1276
+ * when no `actionMeta` is supplied; otherwise reads the resolved Action's
1277
+ * chrome (`label / icon / color / size / outlined / iconOnly / tooltip /
1278
+ * disabled`) and applies it to a button whose click is hardwired by the
1279
+ * surrounding wizard. `type="submit"` lets the Submit slot trigger the
1280
+ * surrounding form's onSubmit dispatcher (no `onClick` needed).
1281
+ *
1282
+ * Hidden actions (`.visible(false)` resolved-away) drop the slot entirely
1283
+ * — the resolver returns `undefined` for hidden Action elements, which
1284
+ * arrives here as `actionMeta == null` so we fall through to the default
1285
+ * chrome. Use `Wizard.skippable()` semantics to hide nav buttons when
1286
+ * appropriate; for permanent removal subclass the wizard.
1287
+ */
1288
+ function WizardNavButton({ actionMeta, fallbackLabel, type = 'button', disabled, onClick, }) {
1289
+ // Bare default — keep historical chrome for back-compat (un-customized
1290
+ // wizards look identical to before this change).
1291
+ if (!actionMeta) {
1292
+ const isPrimary = type === 'submit' || fallbackLabel !== 'Back';
1293
+ return (_jsx("button", { type: type, disabled: disabled, onClick: onClick, className: isPrimary
1294
+ ? 'rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:opacity-90 disabled:opacity-50 disabled:cursor-not-allowed'
1295
+ : 'rounded-md border border-border px-3 py-1.5 text-sm font-medium text-foreground hover:bg-muted disabled:opacity-50 disabled:cursor-not-allowed', children: fallbackLabel }));
1296
+ }
1297
+ const ownDisabled = Boolean(actionMeta['disabled']);
1298
+ const label = String(actionMeta['label'] ?? fallbackLabel);
1299
+ const tooltip = actionMeta['tooltip'] ? String(actionMeta['tooltip']) : undefined;
1300
+ const iconOnly = Boolean(actionMeta['iconOnly']);
1301
+ const className = actionButtonClass(actionMeta, {});
1302
+ const node = (_jsxs("button", { type: type, disabled: disabled || ownDisabled, onClick: onClick, className: `${className} disabled:opacity-50 disabled:cursor-not-allowed`, "aria-label": iconOnly ? label : undefined, children: [renderActionIcon(actionMeta), !iconOnly && _jsx("span", { children: label }), renderActionBadge(actionMeta)] }));
1303
+ return _jsx(_Fragment, { children: withTooltip(node, tooltip) });
1217
1304
  }
1218
1305
  // ─── Top-level dispatch ─────────────────────────────────────
1219
1306
  const TEXT_COLOR_CLASSES = {
@@ -1965,6 +2052,7 @@ function buildTableQuery(state, override, pathname, filterValues = {}, prefix) {
1965
2052
  prefixK(prefix, 'page'),
1966
2053
  prefixK(prefix, 'perPage'),
1967
2054
  prefixK(prefix, 'group'),
2055
+ prefixK(prefix, 'groupKey'),
1968
2056
  ...Object.keys(filterValues).map(n => prefixK(prefix, n)),
1969
2057
  ]);
1970
2058
  for (const [k, v] of currentParams) {
@@ -1988,6 +2076,12 @@ function buildTableQuery(state, override, pathname, filterValues = {}, prefix) {
1988
2076
  params.set(prefixK(prefix, 'page'), String(merged.page));
1989
2077
  if (merged.group !== undefined)
1990
2078
  params.set(prefixK(prefix, 'group'), merged.group);
2079
+ // groupKey is sparse — only writes when the override sets a non-empty
2080
+ // value. Drill-out (chip ×) passes `''` to clear; the foreign-param
2081
+ // dedupe set above already filtered the stale value out, so an empty
2082
+ // override produces a URL without the key.
2083
+ if (merged.groupKey)
2084
+ params.set(prefixK(prefix, 'groupKey'), merged.groupKey);
1991
2085
  const qs = params.toString();
1992
2086
  // Always anchor to a real pathname — Vike's client-side router treats
1993
2087
  // a bare `?qs` href as a fresh URL with empty pathname, which routes
@@ -2687,6 +2781,43 @@ function RecordCellLink({ href, navigate, children, }) {
2687
2781
  };
2688
2782
  return (_jsx("a", { href: href, onClick: onClick, className: "block px-2 py-2 text-inherit no-underline hover:text-inherit focus:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded", children: children }));
2689
2783
  }
2784
+ /**
2785
+ * "Drilled into <Label>: <Value>" chip above the table when a group
2786
+ * heading has been clicked. The × clears `?<prefix>groupKey=`, returning
2787
+ * the table to its banded view. Real `<a href>` with `useNavigate()`
2788
+ * intercept on plain left-click so cmd-click / middle-click open a
2789
+ * fresh tab (rare but valid for sharing the banded view URL).
2790
+ */
2791
+ function ActiveGroupKeyChip({ label, value, displayValue, clearHref, navigate, }) {
2792
+ const onClick = (e) => {
2793
+ if (e.button !== 0)
2794
+ return;
2795
+ if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey)
2796
+ return;
2797
+ e.preventDefault();
2798
+ void navigate(clearHref);
2799
+ };
2800
+ return (_jsxs("div", { className: "flex items-center gap-2 rounded-md border bg-muted/40 px-3 py-2 text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: "Drilled into" }), _jsxs("span", { className: "font-medium text-foreground", children: [label ? `${label}: ` : '', displayValue || value] }), _jsx("a", { href: clearHref, onClick: onClick, "aria-label": "Clear drill-in", className: "ms-auto text-muted-foreground hover:text-foreground", children: "\u00D7" })] }));
2801
+ }
2802
+ /**
2803
+ * Group-heading text wrapped in a real `<a href>` that SPA-navs into the
2804
+ * drilled-in URL. Plain left-click intercepts for `useNavigate()`;
2805
+ * cmd/ctrl/shift-click + middle-click fall through to the browser so
2806
+ * "open in new tab" semantics work. Visually inherits the heading
2807
+ * styling — the link adds underline-on-hover affordance without
2808
+ * disturbing the surrounding text-transform / size.
2809
+ */
2810
+ function GroupHeadingLink({ href, navigate, children, }) {
2811
+ const onClick = (e) => {
2812
+ if (e.button !== 0)
2813
+ return;
2814
+ if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey)
2815
+ return;
2816
+ e.preventDefault();
2817
+ void navigate(href);
2818
+ };
2819
+ return (_jsx("a", { href: href, onClick: onClick, className: "inline-flex items-center gap-1 text-inherit no-underline hover:underline focus:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded", children: children }));
2820
+ }
2690
2821
  /**
2691
2822
  * List-page tab strip — Filament-style query shortcuts above the table
2692
2823
  * ("All / Drafts / Published / Archived"). Each trigger is a real `<a>`
@@ -2818,6 +2949,26 @@ function SortByPicker({ columns, active, onChange, }) {
2818
2949
  return (_jsxs(React.Fragment, { children: [_jsxs(SelectItem, { value: `${name}:asc`, children: [label, " (A\u2192Z)"] }), _jsxs(SelectItem, { value: `${name}:desc`, children: [label, " (Z\u2192A)"] })] }, name));
2819
2950
  }) })] }));
2820
2951
  }
2952
+ /**
2953
+ * Toolbar dropdown for `Column.toggleable()` columns. Lists every
2954
+ * toggleable column with a checkbox; toggling writes through to a
2955
+ * caller-supplied `onToggle` (the `TableRendererBody` owns the state
2956
+ * + the localStorage round-trip). Mounted only when at least one
2957
+ * column is toggleable.
2958
+ */
2959
+ function ColumnsToggleDropdown({ columns, hidden, onToggle, }) {
2960
+ if (columns.length === 0)
2961
+ return null;
2962
+ return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { render: (props) => (_jsxs("button", { ...props, type: "button", className: "inline-flex h-9 items-center gap-1.5 rounded-md border border-input bg-background px-3 text-sm font-medium text-foreground hover:bg-accent", "aria-label": "Show or hide columns", children: [_jsx(Columns3Icon, { className: "h-4 w-4", "aria-hidden": "true" }), _jsx("span", { children: "Columns" })] })) }), _jsx(DropdownMenuContent, { align: "end", className: "min-w-[12rem]", children: columns.map((col, i) => {
2963
+ const name = String(col['name'] ?? '');
2964
+ const label = String(col['label'] ?? name);
2965
+ const isHidden = hidden.has(name);
2966
+ return (_jsxs(DropdownMenuItem, {
2967
+ // Suppress menu-close so users can toggle multiple columns
2968
+ // without re-opening the dropdown.
2969
+ closeOnClick: false, onClick: () => onToggle(name, !isHidden), children: [_jsx("span", { className: "inline-flex w-4 items-center justify-center", children: !isHidden && _jsx(CheckIcon, { className: "h-4 w-4", "aria-hidden": "true" }) }), _jsx("span", { children: label })] }, i));
2970
+ }) })] }));
2971
+ }
2821
2972
  /**
2822
2973
  * Lookup tables for responsive grid column-counts in `contentLayout:
2823
2974
  * 'cards'`. Tailwind's JIT scanner needs **literal** class strings; we
@@ -3003,6 +3154,11 @@ function TableRendererBody({ el }) {
3003
3154
  const navigate = useNavigate();
3004
3155
  const children = el.children ?? [];
3005
3156
  const columns = children.filter(c => c.type === 'column');
3157
+ // `Column.toggleable()` columns — sourced from the resolved meta. The
3158
+ // user's per-table visibility map is owned + persisted below; the full
3159
+ // `columns` list stays available for the toolbar dropdown so hidden
3160
+ // columns can be re-shown without a roundtrip.
3161
+ const toggleableColumns = columns.filter(c => c['toggleable'] !== undefined);
3006
3162
  // Actions and ActionGroups share placement — both show up in the
3007
3163
  // header/bulk/row toolbars depending on their `placement` field.
3008
3164
  const actionLike = children.filter(c => c.type === 'action' || c.type === 'actionGroup' || c.type === 'slotComponent');
@@ -3011,6 +3167,7 @@ function TableRendererBody({ el }) {
3011
3167
  const hasRecordClasses = Boolean(el['recordClasses']);
3012
3168
  const pollInterval = typeof el['pollInterval'] === 'number' ? el['pollInterval'] : undefined;
3013
3169
  const defaultGroup = typeof el['defaultGroup'] === 'string' ? el['defaultGroup'] : undefined;
3170
+ const activeGroupKey = typeof el['activeGroupKey'] === 'string' ? el['activeGroupKey'] : undefined;
3014
3171
  const summaries = el['summaries'];
3015
3172
  const groupSummaries = el['groupSummaries'];
3016
3173
  const groupOptions = el['groups'] ?? [];
@@ -3026,6 +3183,11 @@ function TableRendererBody({ el }) {
3026
3183
  })
3027
3184
  : undefined;
3028
3185
  const groupColumnLabel = activeGroupMeta?.label;
3186
+ // Heading text becomes a real `<a href>` when the active group opts in
3187
+ // via `.scopable()`. Synthesized bare-column groups can't be scopable
3188
+ // (no builder call ran).
3189
+ const groupHeadingScopable = activeGroupMeta !== undefined
3190
+ && activeGroupMeta.scopable === true;
3029
3191
  // Auto-refresh: re-visit current URL on a timer so sort/filter/pagination
3030
3192
  // state survives. Pause while the document is hidden — background tabs
3031
3193
  // shouldn't keep hammering the server.
@@ -3074,6 +3236,67 @@ function TableRendererBody({ el }) {
3074
3236
  const perPage = el['perPage'];
3075
3237
  const searchable = Boolean(el['searchable']);
3076
3238
  const currentPath = el['currentPath'] ?? '';
3239
+ // `Column.toggleable()` user-visibility map. Persisted per-table at
3240
+ // `pilotiq.table.<currentPath>.columns.<name>` ('1' = hidden,
3241
+ // '0' = visible). On first paint, fall back to `meta.toggleable.initiallyHidden`.
3242
+ // SSR returns the meta default — the localStorage hydrate happens
3243
+ // inside the effect so server + first client render match.
3244
+ const columnsVisibilityKey = (name) => `pilotiq.table.${currentPath}.columns.${name}`;
3245
+ const initialHidden = () => {
3246
+ const out = new Set();
3247
+ for (const col of toggleableColumns) {
3248
+ const cfg = col['toggleable'];
3249
+ if (cfg?.initiallyHidden)
3250
+ out.add(String(col['name']));
3251
+ }
3252
+ return out;
3253
+ };
3254
+ const [hiddenColumns, setHiddenColumns] = useState(initialHidden);
3255
+ useEffect(() => {
3256
+ if (typeof window === 'undefined')
3257
+ return;
3258
+ if (toggleableColumns.length === 0)
3259
+ return;
3260
+ const next = new Set();
3261
+ for (const col of toggleableColumns) {
3262
+ const name = String(col['name']);
3263
+ const cfg = col['toggleable'];
3264
+ try {
3265
+ const stored = window.localStorage.getItem(columnsVisibilityKey(name));
3266
+ if (stored === '1')
3267
+ next.add(name);
3268
+ else if (stored === '0') { /* visible */ }
3269
+ else if (cfg?.initiallyHidden)
3270
+ next.add(name);
3271
+ }
3272
+ catch {
3273
+ if (cfg?.initiallyHidden)
3274
+ next.add(name);
3275
+ }
3276
+ }
3277
+ setHiddenColumns(next);
3278
+ // eslint-disable-next-line react-hooks/exhaustive-deps
3279
+ }, [currentPath, toggleableColumns.length]);
3280
+ const toggleColumnHidden = (name, nextHidden) => {
3281
+ setHiddenColumns(prev => {
3282
+ const next = new Set(prev);
3283
+ if (nextHidden)
3284
+ next.add(name);
3285
+ else
3286
+ next.delete(name);
3287
+ if (typeof window !== 'undefined') {
3288
+ try {
3289
+ window.localStorage.setItem(columnsVisibilityKey(name), nextHidden ? '1' : '0');
3290
+ }
3291
+ catch { /* private mode / quota — silent */ }
3292
+ }
3293
+ return next;
3294
+ });
3295
+ };
3296
+ // Filtered column list used by every render path (header, body cells,
3297
+ // group + footer summaries, empty-state colSpan). Non-toggleable
3298
+ // columns always survive.
3299
+ const visibleColumns = columns.filter(c => !hiddenColumns.has(String(c['name'])));
3077
3300
  // Tier-3 — when the table opts into `Table.queryStringIdentifier(...)`,
3078
3301
  // every URL key (search / sort / page / perPage / group / filter names)
3079
3302
  // gets prefixed with `${id}_` so multiple tables on one page don't
@@ -3156,6 +3379,7 @@ function TableRendererBody({ el }) {
3156
3379
  ...(urlGroup !== undefined ? { group: urlGroup }
3157
3380
  : defaultGroup !== undefined ? { group: defaultGroup }
3158
3381
  : {}),
3382
+ ...(activeGroupKey !== undefined ? { groupKey: activeGroupKey } : {}),
3159
3383
  };
3160
3384
  // Snapshot active filter values for sort/pagination href construction.
3161
3385
  // Filter form submits already carry these (selects are inside the
@@ -3166,6 +3390,12 @@ function TableRendererBody({ el }) {
3166
3390
  if (typeof v === 'string' && v !== '')
3167
3391
  activeFilters[String(f['name'])] = v;
3168
3392
  }
3393
+ // Drill-in / drill-out URL builders for the group heading link and the
3394
+ // active-key chip's clear button. Drill-in sets `?<prefix>groupKey=v`
3395
+ // and resets `page`; drill-out clears it. Both round-trip foreign
3396
+ // params (other tables' state) through `buildTableQuery`.
3397
+ const buildGroupKeyHref = (value) => buildTableQuery(state, { groupKey: value, page: 1 }, currentPath, activeFilters, queryPrefix);
3398
+ const drillOutHref = () => buildTableQuery(state, { groupKey: '', page: 1 }, currentPath, activeFilters, queryPrefix);
3169
3399
  // Track which row ids are currently checked. Keyed by id (string), not
3170
3400
  // by index, so pagination and re-renders don't drop selection state.
3171
3401
  const [selected, setSelected] = useState(() => new Set());
@@ -3328,7 +3558,8 @@ function TableRendererBody({ el }) {
3328
3558
  // Only modal + collapsible mount a toolbar widget; the always-visible
3329
3559
  // strip modes don't add anything to the header bar.
3330
3560
  const showFiltersInToolbar = hasFilters && (filtersInModal || filtersCollapsible);
3331
- const showHeaderBar = searchable || headerActions.length > 0 || showFiltersInToolbar || hasGroupPicker || hasSortPicker;
3561
+ const hasColumnsToggle = toggleableColumns.length > 0;
3562
+ const showHeaderBar = searchable || headerActions.length > 0 || showFiltersInToolbar || hasGroupPicker || hasSortPicker || hasColumnsToggle;
3332
3563
  const hasBulkActions = bulkActions.length > 0;
3333
3564
  const hasRowActions = rowActions.length > 0;
3334
3565
  // Drag-to-reorder is enabled only when the visible rows ARE the
@@ -3348,7 +3579,7 @@ function TableRendererBody({ el }) {
3348
3579
  !searchActive &&
3349
3580
  currentPage === 1;
3350
3581
  const reorderColumnVisible = reorderableColumn !== undefined;
3351
- const totalCols = columns.length
3582
+ const totalCols = visibleColumns.length
3352
3583
  + (hasBulkActions ? 1 : 0)
3353
3584
  + (hasRowActions ? 1 : 0)
3354
3585
  + (reorderColumnVisible ? 1 : 0);
@@ -3365,7 +3596,7 @@ function TableRendererBody({ el }) {
3365
3596
  // pre-2026-05-04 behavior for tables that haven't opted in.
3366
3597
  const activeEmpty = (hasFilterOrSearch && filteredEmptyState) ? filteredEmptyState : emptyState;
3367
3598
  const EmptyIcon = activeEmpty?.icon ? (resolveIcon(activeEmpty.icon) ?? InboxIcon) : InboxIcon;
3368
- return (_jsxs("div", { className: "flex flex-col gap-3", children: [(tableHeading || tableDescription) && (_jsxs("div", { className: "flex flex-col gap-1", children: [tableHeading && _jsx("h2", { className: "text-lg font-semibold", children: tableHeading }), tableDescription && _jsx("p", { className: "text-sm text-muted-foreground", children: tableDescription })] })), showHeaderBar && (_jsxs("div", { className: "flex flex-col-reverse gap-2 sm:flex-row sm:items-center sm:justify-between", children: [(searchable || showFiltersInToolbar || hasGroupPicker || hasSortPicker) ? (_jsxs("div", { className: "flex items-center gap-2", children: [searchable && (_jsxs("form", { method: "get", action: currentPath || undefined, className: "flex items-end gap-2", children: [_jsx(SearchFormHiddenInputs, { prefix: queryPrefix }), _jsx(Input, { type: "search", name: prefixK(queryPrefix, 'search'), defaultValue: search ?? '', placeholder: "Search\u2026", className: "h-9 w-64" }), _jsx("button", { type: "submit", className: "sr-only", tabIndex: -1, "aria-hidden": "true", children: "Apply" })] })), hasFilters && filtersInModal && (_jsx(FilterPopover, { filters: filters, prefix: queryPrefix })), hasFilters && filtersCollapsible && (_jsx(FilterStripToggle, { filters: filters, open: filtersOpen, onToggle: toggleFiltersOpen })), hasGroupPicker && (_jsx(TableGroupPicker, { options: groupOptions, active: defaultGroup, onChange: (value) => {
3599
+ return (_jsxs("div", { className: "flex flex-col gap-3", children: [(tableHeading || tableDescription) && (_jsxs("div", { className: "flex flex-col gap-1", children: [tableHeading && _jsx("h2", { className: "text-lg font-semibold", children: tableHeading }), tableDescription && _jsx("p", { className: "text-sm text-muted-foreground", children: tableDescription })] })), showHeaderBar && (_jsxs("div", { className: "flex flex-col-reverse gap-2 sm:flex-row sm:items-center sm:justify-between", children: [(searchable || showFiltersInToolbar || hasGroupPicker || hasSortPicker || hasColumnsToggle) ? (_jsxs("div", { className: "flex items-center gap-2", children: [searchable && (_jsxs("form", { method: "get", action: currentPath || undefined, className: "flex items-end gap-2", children: [_jsx(SearchFormHiddenInputs, { prefix: queryPrefix }), _jsx(Input, { type: "search", name: prefixK(queryPrefix, 'search'), defaultValue: search ?? '', placeholder: "Search\u2026", className: "h-9 w-64" }), _jsx("button", { type: "submit", className: "sr-only", tabIndex: -1, "aria-hidden": "true", children: "Apply" })] })), hasFilters && filtersInModal && (_jsx(FilterPopover, { filters: filters, prefix: queryPrefix })), hasFilters && filtersCollapsible && (_jsx(FilterStripToggle, { filters: filters, open: filtersOpen, onToggle: toggleFiltersOpen })), hasGroupPicker && (_jsx(TableGroupPicker, { options: groupOptions, active: defaultGroup, onChange: (value) => {
3369
3600
  // value === '' → explicit "None" (clears defaultGroup);
3370
3601
  // value !== '' → switch to that column.
3371
3602
  const href = buildTableQuery(state, { page: 1, group: value }, currentPath, activeFilters, queryPrefix);
@@ -3373,7 +3604,23 @@ function TableRendererBody({ el }) {
3373
3604
  } })), hasSortPicker && (_jsx(SortByPicker, { columns: sortableColumns, active: currentSort, onChange: (column, direction) => {
3374
3605
  const href = buildTableQuery(state, { sort: { column, direction }, page: 1 }, currentPath, activeFilters, queryPrefix);
3375
3606
  navigate(href);
3376
- } }))] })) : _jsx("span", {}), headerActions.length > 0 && (_jsx("div", { className: "flex items-center gap-2", children: headerActions.map((a, i) => renderActionLike(a, i)) }))] })), hasFilters && filtersInModal && _jsx(ActiveFiltersBar, { filters: filters, prefix: queryPrefix }), hasFilters && filtersAbove && filtersOpen && (_jsx(FilterStrip, { filters: filters, prefix: queryPrefix })), hasBulkActions && someChecked && (_jsxs("div", { className: "flex items-center justify-between gap-2 rounded-md border bg-muted/40 px-3 py-2 text-sm", children: [_jsxs("span", { className: "text-muted-foreground", children: [selected.size, " selected"] }), _jsxs("div", { className: "flex items-center gap-2", children: [bulkActions.map((a, i) => renderActionLike(a, i, { ids: Array.from(selected) })), _jsx("button", { type: "button", onClick: () => setSelected(new Set()), className: "text-xs text-muted-foreground hover:text-foreground", children: "Clear" })] })] })), isCardsLayout ? (_jsx(CardsLayoutBody, { el: el, columns: columns, rows: rows, visibleIds: visibleIds, selected: selected, toggleRow: toggleRow, hasBulkActions: hasBulkActions, hasRowActions: hasRowActions, rowActions: rowActions, hasRecordUrl: hasRecordUrl, hasRecordClasses: hasRecordClasses, striped: striped, activeEmpty: activeEmpty, EmptyIcon: EmptyIcon, hasFilterOrSearch: hasFilterOrSearch, defaultGroup: defaultGroup, groupColumnLabel: groupColumnLabel, groupCollapsible: groupCollapsible, collapsedGroups: collapsedGroups, toggleGroupCollapsed: toggleGroupCollapsed, cardsPerRow: cardsPerRow, navigate: navigate })) : (_jsx("div", { className: "rounded-xl border bg-card overflow-hidden", children: _jsxs(DataTable, { children: [_jsx(TableHeader, { className: "bg-muted", children: _jsxs(TableRow, { children: [reorderColumnVisible && (_jsx(TableHead, { className: "w-9 px-2", "aria-label": "Reorder" })), hasBulkActions && (_jsx(TableHead, { className: "w-9 px-3", children: _jsx(Checkbox, { "aria-label": "Select all rows", checked: allChecked, onCheckedChange: () => toggleAll() }) })), columns.map((col, i) => {
3607
+ } })), toggleableColumns.length > 0 && (_jsx(ColumnsToggleDropdown, { columns: toggleableColumns, hidden: hiddenColumns, onToggle: toggleColumnHidden }))] })) : _jsx("span", {}), headerActions.length > 0 && (_jsx("div", { className: "flex items-center gap-2", children: headerActions.map((a, i) => renderActionLike(a, i)) }))] })), hasFilters && filtersInModal && _jsx(ActiveFiltersBar, { filters: filters, prefix: queryPrefix }), hasFilters && filtersAbove && filtersOpen && (_jsx(FilterStrip, { filters: filters, prefix: queryPrefix })), activeGroupKey !== undefined && (_jsx(ActiveGroupKeyChip, { label: groupColumnLabel ?? defaultGroup ?? '', value: activeGroupKey, displayValue: (() => {
3608
+ // Prefer a row-resolved `_groupTitle` (server stamped via
3609
+ // `getTitleFromRecordUsing`) so the chip reads the same as
3610
+ // a banded heading. Falls back to the raw bucket key when
3611
+ // no row matched — empty drilled-in pages still show what
3612
+ // they're drilled into.
3613
+ for (const r of rows) {
3614
+ const obj = r;
3615
+ if (String(obj['_groupValue'] ?? '') !== activeGroupKey)
3616
+ continue;
3617
+ const t = obj['_groupTitle'];
3618
+ if (typeof t === 'string' && t !== '')
3619
+ return t;
3620
+ break;
3621
+ }
3622
+ return activeGroupKey;
3623
+ })(), clearHref: drillOutHref(), navigate: navigate })), hasBulkActions && someChecked && (_jsxs("div", { className: "flex items-center justify-between gap-2 rounded-md border bg-muted/40 px-3 py-2 text-sm", children: [_jsxs("span", { className: "text-muted-foreground", children: [selected.size, " selected"] }), _jsxs("div", { className: "flex items-center gap-2", children: [bulkActions.map((a, i) => renderActionLike(a, i, { ids: Array.from(selected) })), _jsx("button", { type: "button", onClick: () => setSelected(new Set()), className: "text-xs text-muted-foreground hover:text-foreground", children: "Clear" })] })] })), isCardsLayout ? (_jsx(CardsLayoutBody, { el: el, columns: columns, rows: rows, visibleIds: visibleIds, selected: selected, toggleRow: toggleRow, hasBulkActions: hasBulkActions, hasRowActions: hasRowActions, rowActions: rowActions, hasRecordUrl: hasRecordUrl, hasRecordClasses: hasRecordClasses, striped: striped, activeEmpty: activeEmpty, EmptyIcon: EmptyIcon, hasFilterOrSearch: hasFilterOrSearch, defaultGroup: defaultGroup, groupColumnLabel: groupColumnLabel, groupCollapsible: groupCollapsible, collapsedGroups: collapsedGroups, toggleGroupCollapsed: toggleGroupCollapsed, cardsPerRow: cardsPerRow, navigate: navigate, groupHeadingScopable: groupHeadingScopable, buildGroupKeyHref: buildGroupKeyHref })) : (_jsx("div", { className: "rounded-xl border bg-card overflow-hidden", children: _jsxs(DataTable, { children: [_jsx(TableHeader, { className: "bg-muted", children: _jsxs(TableRow, { children: [reorderColumnVisible && (_jsx(TableHead, { className: "w-9 px-2", "aria-label": "Reorder" })), hasBulkActions && (_jsx(TableHead, { className: "w-9 px-3", children: _jsx(Checkbox, { "aria-label": "Select all rows", checked: allChecked, onCheckedChange: () => toggleAll() }) })), visibleColumns.map((col, i) => {
3377
3624
  const name = String(col['name'] ?? '');
3378
3625
  const label = String(col['label'] ?? name);
3379
3626
  const sortable = Boolean(col['sortable']);
@@ -3428,16 +3675,28 @@ function TableRendererBody({ el }) {
3428
3675
  .filter(Boolean)
3429
3676
  .join(' ')
3430
3677
  .trim();
3431
- return (_jsxs(React.Fragment, { children: [showGroupHeader && (_jsx(TableRow, { className: "bg-muted/40 hover:bg-muted/40", children: _jsx(TableCell, { colSpan: totalCols, className: "px-3 py-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground", children: groupCollapsible ? (_jsxs("button", { type: "button", className: "flex w-full items-center gap-2 text-left", onClick: () => toggleGroupCollapsed(groupValue), "aria-expanded": !isInCollapsedGroup, children: [_jsx(ChevronDownIcon, { className: [
3432
- 'size-4 transition-transform',
3433
- isInCollapsedGroup ? '-rotate-90' : '',
3434
- ].filter(Boolean).join(' ') }), _jsx(GroupHeaderText, { label: groupColumnLabel, value: groupValue, title: groupTitle, description: groupDescription })] })) : (_jsx(GroupHeaderText, { label: groupColumnLabel, value: groupValue, title: groupTitle, description: groupDescription })) }) }, `group-${id}`)), isInCollapsedGroup ? null : (_jsxs(TableRow, { "data-state": isSelected ? 'selected' : undefined, className: [
3678
+ return (_jsxs(React.Fragment, { children: [showGroupHeader && (_jsx(TableRow, { className: "bg-muted/40 hover:bg-muted/40", children: _jsx(TableCell, { colSpan: totalCols, className: "px-3 py-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground", children: (() => {
3679
+ const drillable = groupHeadingScopable
3680
+ && groupValue !== undefined
3681
+ && groupValue !== '';
3682
+ const headingText = (_jsx(GroupHeaderText, { label: groupColumnLabel, value: groupValue, title: groupTitle, description: groupDescription }));
3683
+ const headingNode = drillable
3684
+ ? _jsx(GroupHeadingLink, { href: buildGroupKeyHref(groupValue), navigate: navigate, children: headingText })
3685
+ : headingText;
3686
+ if (groupCollapsible) {
3687
+ return (_jsxs("div", { className: "flex w-full items-center gap-2", children: [_jsx("button", { type: "button", className: "inline-flex items-center", onClick: () => toggleGroupCollapsed(groupValue), "aria-expanded": !isInCollapsedGroup, "aria-label": isInCollapsedGroup ? 'Expand group' : 'Collapse group', children: _jsx(ChevronDownIcon, { className: [
3688
+ 'size-4 transition-transform',
3689
+ isInCollapsedGroup ? '-rotate-90' : '',
3690
+ ].filter(Boolean).join(' ') }) }), headingNode] }));
3691
+ }
3692
+ return headingNode;
3693
+ })() }) }, `group-${id}`)), isInCollapsedGroup ? null : (_jsxs(TableRow, { "data-state": isSelected ? 'selected' : undefined, className: [
3435
3694
  rowClassName,
3436
3695
  dragId === id ? 'opacity-50' : '',
3437
3696
  dropAt === ri && dragId !== null ? 'border-t-2 border-t-primary' : '',
3438
3697
  ].filter(Boolean).join(' ') || undefined, draggable: reorderEnabled || undefined, onDragStart: reorderEnabled ? onRowDragStart(id) : undefined, onDragOver: reorderEnabled ? onRowDragOver(ri) : undefined, onDrop: reorderEnabled ? onRowDrop : undefined, onDragEnd: reorderEnabled ? onRowDragEnd : undefined, children: [reorderColumnVisible && (_jsx(TableCell, { className: "w-9 px-2", children: _jsx("span", { "aria-label": reorderEnabled ? 'Drag to reorder' : 'Reorder paused — clear filters and sort to enable', className: reorderEnabled
3439
3698
  ? 'inline-flex cursor-grab text-muted-foreground hover:text-foreground active:cursor-grabbing'
3440
- : 'inline-flex cursor-not-allowed text-muted-foreground/40', children: _jsx(GripVerticalIcon, { className: "size-4" }) }) })), hasBulkActions && (_jsx(TableCell, { className: "w-9 px-3", children: _jsx(Checkbox, { "aria-label": `Select row ${id}`, checked: isSelected, onCheckedChange: () => toggleRow(id) }) })), columns.map((col, ci) => {
3699
+ : 'inline-flex cursor-not-allowed text-muted-foreground/40', children: _jsx(GripVerticalIcon, { className: "size-4" }) }) })), hasBulkActions && (_jsx(TableCell, { className: "w-9 px-3", children: _jsx(Checkbox, { "aria-label": `Select row ${id}`, checked: isSelected, onCheckedChange: () => toggleRow(id) }) })), visibleColumns.map((col, ci) => {
3441
3700
  const name = String(col['name'] ?? '');
3442
3701
  const value = recordObj[name];
3443
3702
  const align = col['alignment'] === 'center' ? 'text-center'
@@ -3459,7 +3718,9 @@ function TableRendererBody({ el }) {
3459
3718
  : null;
3460
3719
  if (EditableComp && editUrl !== undefined) {
3461
3720
  const cellDisabled = col['disabled'] === true || cellDisabledMap?.[name] === true;
3462
- return (_jsx(TableCell, { className: `text-sm text-foreground ${align} p-0`, style: widthStyle, children: _jsx(EditableComp, { url: editUrl, col: col, value: value, disabled: cellDisabled }) }, ci));
3721
+ const cellSelectOptionsMap = recordObj['_cellSelectOptions'];
3722
+ const rowOptions = cellSelectOptionsMap?.[name];
3723
+ return (_jsx(TableCell, { className: `text-sm text-foreground ${align} p-0`, style: widthStyle, children: _jsx(EditableComp, { url: editUrl, col: col, value: value, disabled: cellDisabled, ...(rowOptions ? { rowOptions } : {}) }) }, ci));
3463
3724
  }
3464
3725
  const cellContent = formatCell(value, col, recordObj);
3465
3726
  const colUrl = resolveColumnUrl(col, tableUrl, colUrls);
@@ -3480,7 +3741,7 @@ function TableRendererBody({ el }) {
3480
3741
  const perCol = groupSummaries[groupValue];
3481
3742
  if (!perCol || Object.keys(perCol).length === 0)
3482
3743
  return null;
3483
- return (_jsxs(TableRow, { className: "bg-muted/20 hover:bg-muted/20", children: [reorderColumnVisible && _jsx(TableCell, {}), hasBulkActions && _jsx(TableCell, {}), columns.map((col, ci) => {
3744
+ return (_jsxs(TableRow, { className: "bg-muted/20 hover:bg-muted/20", children: [reorderColumnVisible && _jsx(TableCell, {}), hasBulkActions && _jsx(TableCell, {}), visibleColumns.map((col, ci) => {
3484
3745
  const name = String(col['name'] ?? '');
3485
3746
  const align = col['alignment'] === 'center' ? 'text-center'
3486
3747
  : col['alignment'] === 'end' ? 'text-right'
@@ -3489,7 +3750,7 @@ function TableRendererBody({ el }) {
3489
3750
  return (_jsx(TableCell, { className: `text-xs font-medium ${align} px-2 py-1.5`, children: items?.map((s, i) => (_jsxs("div", { className: "leading-tight", children: [s.label && _jsxs("span", { className: "text-muted-foreground", children: [s.label, ": "] }), _jsx("span", { children: s.value })] }, i))) }, ci));
3490
3751
  }), hasRowActions && _jsx(TableCell, {})] }, `group-summary-${id}`));
3491
3752
  })()] }, id));
3492
- }) }), summaries && Object.keys(summaries).length > 0 && (_jsx(TableFooter, { children: _jsxs(TableRow, { children: [reorderColumnVisible && _jsx(TableCell, {}), hasBulkActions && _jsx(TableCell, {}), columns.map((col, ci) => {
3753
+ }) }), summaries && Object.keys(summaries).length > 0 && (_jsx(TableFooter, { children: _jsxs(TableRow, { children: [reorderColumnVisible && _jsx(TableCell, {}), hasBulkActions && _jsx(TableCell, {}), visibleColumns.map((col, ci) => {
3493
3754
  const name = String(col['name'] ?? '');
3494
3755
  const align = col['alignment'] === 'center' ? 'text-center'
3495
3756
  : col['alignment'] === 'end' ? 'text-right'
@@ -3516,7 +3777,7 @@ function TableRendererBody({ el }) {
3516
3777
  * configured per-card grid (`cardsPerRow`) re-applies inside every
3517
3778
  * section so the column count stays consistent.
3518
3779
  */
3519
- function CardsLayoutBody({ el, columns, rows, visibleIds, selected, toggleRow, hasBulkActions, hasRowActions, rowActions, hasRecordUrl, hasRecordClasses, striped, activeEmpty, EmptyIcon, hasFilterOrSearch, defaultGroup, groupColumnLabel, groupCollapsible, collapsedGroups, toggleGroupCollapsed, cardsPerRow, navigate, }) {
3780
+ function CardsLayoutBody({ el, columns, rows, visibleIds, selected, toggleRow, hasBulkActions, hasRowActions, rowActions, hasRecordUrl, hasRecordClasses, striped, activeEmpty, EmptyIcon, hasFilterOrSearch, defaultGroup, groupColumnLabel, groupCollapsible, collapsedGroups, toggleGroupCollapsed, cardsPerRow, navigate, groupHeadingScopable, buildGroupKeyHref, }) {
3520
3781
  void el; // keep prop for future telemetry; silences unused-prop lint
3521
3782
  void columns;
3522
3783
  void striped; // visual stripes don't apply to cards (each card has its own surface)
@@ -3549,10 +3810,22 @@ function CardsLayoutBody({ el, columns, rows, visibleIds, selected, toggleRow, h
3549
3810
  const collapsed = groupCollapsible
3550
3811
  && section.groupValue !== undefined
3551
3812
  && collapsedGroups[section.groupValue] === true;
3552
- return (_jsxs("div", { className: "flex flex-col gap-3", children: [section.groupValue !== undefined && (groupCollapsible ? (_jsxs("button", { type: "button", className: "flex w-full items-center gap-2 text-left text-xs font-semibold uppercase tracking-wider text-muted-foreground", onClick: () => toggleGroupCollapsed(section.groupValue), "aria-expanded": !collapsed, children: [_jsx(ChevronDownIcon, { className: [
3553
- 'size-4 transition-transform',
3554
- collapsed ? '-rotate-90' : '',
3555
- ].filter(Boolean).join(' ') }), _jsx(GroupHeaderText, { label: groupColumnLabel, value: section.groupValue, title: section.title, description: section.description })] })) : (_jsx("div", { className: "text-xs font-semibold uppercase tracking-wider text-muted-foreground", children: _jsx(GroupHeaderText, { label: groupColumnLabel, value: section.groupValue, title: section.title, description: section.description }) }))), !collapsed && (_jsx("div", { className: gridClass, children: section.indices.map((ri) => {
3813
+ return (_jsxs("div", { className: "flex flex-col gap-3", children: [section.groupValue !== undefined && (() => {
3814
+ const drillable = groupHeadingScopable === true
3815
+ && buildGroupKeyHref !== undefined
3816
+ && section.groupValue !== '';
3817
+ const headingText = (_jsx(GroupHeaderText, { label: groupColumnLabel, value: section.groupValue, title: section.title, description: section.description }));
3818
+ const headingNode = drillable
3819
+ ? _jsx(GroupHeadingLink, { href: buildGroupKeyHref(section.groupValue), navigate: navigate, children: headingText })
3820
+ : headingText;
3821
+ if (groupCollapsible) {
3822
+ return (_jsxs("div", { className: "flex w-full items-center gap-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground", children: [_jsx("button", { type: "button", className: "inline-flex items-center", onClick: () => toggleGroupCollapsed(section.groupValue), "aria-expanded": !collapsed, "aria-label": collapsed ? 'Expand group' : 'Collapse group', children: _jsx(ChevronDownIcon, { className: [
3823
+ 'size-4 transition-transform',
3824
+ collapsed ? '-rotate-90' : '',
3825
+ ].filter(Boolean).join(' ') }) }), headingNode] }));
3826
+ }
3827
+ return (_jsx("div", { className: "text-xs font-semibold uppercase tracking-wider text-muted-foreground", children: headingNode }));
3828
+ })(), !collapsed && (_jsx("div", { className: gridClass, children: section.indices.map((ri) => {
3556
3829
  const id = visibleIds[ri];
3557
3830
  const recordObj = rows[ri];
3558
3831
  const isSelected = selected.has(id);