@dashforge/tw 0.5.0-beta → 0.6.0-beta

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 (64) hide show
  1. package/CHANGELOG.md +268 -0
  2. package/dist/index.esm.js +1553 -4
  3. package/dist/src/components/Table/Table.d.ts +24 -0
  4. package/dist/src/components/Table/Table.d.ts.map +1 -0
  5. package/dist/src/components/Table/_internal/getNestedValue.d.ts +20 -0
  6. package/dist/src/components/Table/_internal/getNestedValue.d.ts.map +1 -0
  7. package/dist/src/components/Table/_internal/useColumnAutoDetect.d.ts +45 -0
  8. package/dist/src/components/Table/_internal/useColumnAutoDetect.d.ts.map +1 -0
  9. package/dist/src/components/Table/_internal/useControllableState.d.ts +23 -0
  10. package/dist/src/components/Table/_internal/useControllableState.d.ts.map +1 -0
  11. package/dist/src/components/Table/_internal/useDebouncedValue.d.ts +10 -0
  12. package/dist/src/components/Table/_internal/useDebouncedValue.d.ts.map +1 -0
  13. package/dist/src/components/Table/_internal/useTableFilter.d.ts +13 -0
  14. package/dist/src/components/Table/_internal/useTableFilter.d.ts.map +1 -0
  15. package/dist/src/components/Table/_internal/useTableSearch.d.ts +23 -0
  16. package/dist/src/components/Table/_internal/useTableSearch.d.ts.map +1 -0
  17. package/dist/src/components/Table/_internal/useTableSelection.d.ts +25 -0
  18. package/dist/src/components/Table/_internal/useTableSelection.d.ts.map +1 -0
  19. package/dist/src/components/Table/_internal/useTableSort.d.ts +20 -0
  20. package/dist/src/components/Table/_internal/useTableSort.d.ts.map +1 -0
  21. package/dist/src/components/Table/cells/RenderButton.d.ts +13 -0
  22. package/dist/src/components/Table/cells/RenderButton.d.ts.map +1 -0
  23. package/dist/src/components/Table/cells/RenderChip.d.ts +80 -0
  24. package/dist/src/components/Table/cells/RenderChip.d.ts.map +1 -0
  25. package/dist/src/components/Table/cells/RenderText.d.ts +21 -0
  26. package/dist/src/components/Table/cells/RenderText.d.ts.map +1 -0
  27. package/dist/src/components/Table/cells/RenderTwoLine.d.ts +20 -0
  28. package/dist/src/components/Table/cells/RenderTwoLine.d.ts.map +1 -0
  29. package/dist/src/components/Table/cells/RowActionsMenu.d.ts +36 -0
  30. package/dist/src/components/Table/cells/RowActionsMenu.d.ts.map +1 -0
  31. package/dist/src/components/Table/cells/index.d.ts +11 -0
  32. package/dist/src/components/Table/cells/index.d.ts.map +1 -0
  33. package/dist/src/components/Table/table.types.d.ts +304 -0
  34. package/dist/src/components/Table/table.types.d.ts.map +1 -0
  35. package/dist/src/components/Table/table.variants.d.ts +285 -0
  36. package/dist/src/components/Table/table.variants.d.ts.map +1 -0
  37. package/dist/src/index.d.ts +7 -1
  38. package/dist/src/index.d.ts.map +1 -1
  39. package/package.json +1 -1
  40. package/src/components/Table/Table.test.tsx +603 -0
  41. package/src/components/Table/Table.tsx +816 -0
  42. package/src/components/Table/_internal/getNestedValue.test.ts +50 -0
  43. package/src/components/Table/_internal/getNestedValue.ts +32 -0
  44. package/src/components/Table/_internal/themeIdentity.test.ts +84 -0
  45. package/src/components/Table/_internal/useColumnAutoDetect.test.ts +123 -0
  46. package/src/components/Table/_internal/useColumnAutoDetect.ts +100 -0
  47. package/src/components/Table/_internal/useControllableState.ts +48 -0
  48. package/src/components/Table/_internal/useDebouncedValue.ts +24 -0
  49. package/src/components/Table/_internal/useTableFilter.ts +37 -0
  50. package/src/components/Table/_internal/useTableSearch.test.ts +115 -0
  51. package/src/components/Table/_internal/useTableSearch.ts +66 -0
  52. package/src/components/Table/_internal/useTableSelection.test.ts +100 -0
  53. package/src/components/Table/_internal/useTableSelection.ts +100 -0
  54. package/src/components/Table/_internal/useTableSort.test.ts +150 -0
  55. package/src/components/Table/_internal/useTableSort.ts +141 -0
  56. package/src/components/Table/cells/RenderButton.tsx +23 -0
  57. package/src/components/Table/cells/RenderChip.tsx +103 -0
  58. package/src/components/Table/cells/RenderText.tsx +37 -0
  59. package/src/components/Table/cells/RenderTwoLine.tsx +34 -0
  60. package/src/components/Table/cells/RowActionsMenu.tsx +147 -0
  61. package/src/components/Table/cells/index.ts +14 -0
  62. package/src/components/Table/table.types.ts +337 -0
  63. package/src/components/Table/table.variants.ts +207 -0
  64. package/src/index.ts +43 -1
package/dist/index.esm.js CHANGED
@@ -7601,7 +7601,7 @@ function range(start, end) {
7601
7601
  }, (_, i)=>start + i);
7602
7602
  }
7603
7603
 
7604
- const DEFAULT_LABELS = {
7604
+ const DEFAULT_LABELS$1 = {
7605
7605
  first: 'First',
7606
7606
  prev: 'Previous',
7607
7607
  next: 'Next',
@@ -7649,7 +7649,7 @@ const DEFAULT_PAGE_SIZE_OPTIONS = [
7649
7649
  */ function Pagination(props) {
7650
7650
  var _slotProps_root, _slotProps_summary, _slotProps_summary1, _slotProps_list, _slotProps_navButton, _slotProps_navButton1, _slotProps_navButton2, _slotProps_navButton3, _slotProps_pageSizeSelector, _slotProps_jumpInput;
7651
7651
  const { page, pageSize, totalCount, onPageChange, onPageSizeChange, pageSizeOptions = DEFAULT_PAGE_SIZE_OPTIONS, siblingCount = 1, boundaryCount = 1, showFirstLast = true, showJumpInput = true, disabled = false, labels: labelsProp, variant = 'default', size = 'md', sx, slotProps } = props;
7652
- const labels = _extends({}, DEFAULT_LABELS, labelsProp);
7652
+ const labels = _extends({}, DEFAULT_LABELS$1, labelsProp);
7653
7653
  const v = paginationVariants({
7654
7654
  variant,
7655
7655
  size
@@ -7844,6 +7844,1555 @@ const DEFAULT_PAGE_SIZE_OPTIONS = [
7844
7844
  });
7845
7845
  }
7846
7846
 
7847
+ /**
7848
+ * `<Table>` variant recipe.
7849
+ *
7850
+ * Slots:
7851
+ * - `root` — outer wrapper
7852
+ * - `toolbar` — search + filter chips row above table
7853
+ * - `search` — search input
7854
+ * - `scroll` — `<div class="overflow-auto">` wrapping the table
7855
+ * - `table` — `<table>` element
7856
+ * - `thead` — `<thead>` (sticky positioning here)
7857
+ * - `tbody` — `<tbody>`
7858
+ * - `headerRow` — `<tr>` inside `<thead>`
7859
+ * - `headerCell` — `<th>` cell
7860
+ * - `headerCellButton` — `<button>` inside sortable `<th>`
7861
+ * - `row` — `<tr>` inside `<tbody>` (data row)
7862
+ * - `cell` — `<td>` data cell
7863
+ * - `selectionCell` — first `<td>` when selection mode active
7864
+ * - `expandToggleCell` — second `<td>` when expandable active
7865
+ * - `expandedRow` — full-width `<tr>` rendered after an expanded row
7866
+ * - `rowActionsCell` — last `<td>` when `rowActions` provided
7867
+ * - `emptyState` — placeholder when rows is empty
7868
+ * - `loadingOverlay` — skeleton overlay when loading
7869
+ * - `bulkActionFooter` — sticky footer with bulk actions
7870
+ *
7871
+ * Variant axes:
7872
+ * - `variant` — visual row-division style:
7873
+ * - `plain` — minimal separators
7874
+ * - `lines` — Stripe-style thin horizontal dividers (default)
7875
+ * - `striped` — alternating row backgrounds (zebra)
7876
+ * - `bordered` — full cell borders
7877
+ * - `card` — each row rendered as a separate surface card
7878
+ * - `size` — `sm` / `md` (default) / `lg`
7879
+ * - `density` — `compact` (40px row) / `comfortable` (default 48px) / `spacious` (56px)
7880
+ *
7881
+ * Smart-default alignment (per column) is applied by the Table
7882
+ * component code, NOT here — it depends on the runtime-detected
7883
+ * value type, which the recipe doesn't see.
7884
+ *
7885
+ * **Dashforge identity rule** (memory: `feedback_dashforge_preset_is_identity`):
7886
+ * This recipe uses NO `dark:` variants on the neutral palette. The
7887
+ * `dashforgePreset()` CSS-variable swap auto-inverts neutral tokens
7888
+ * (`bg-neutral-50` is light surface in light mode, dark surface in
7889
+ * dark mode — same class, different physical color). Adding a
7890
+ * `dark:` variant on neutral palette = DOUBLE inversion = breaks
7891
+ * dark mode. The selected-row state uses the canonical LeftNav
7892
+ * `itemActive` pattern (`bg-primary-100 text-primary-900 font-medium`),
7893
+ * which works in both modes because the primary palette has no
7894
+ * inversion (both `bg` and `text` resolve to the same physical
7895
+ * colors regardless of mode → guaranteed contrast).
7896
+ */ const tableVariants = tv({
7897
+ slots: {
7898
+ root: 'w-full',
7899
+ toolbar: 'flex flex-wrap items-center gap-3 mb-3',
7900
+ search: [
7901
+ 'inline-flex items-center gap-2',
7902
+ 'w-full max-w-sm rounded-md',
7903
+ 'border border-neutral-300',
7904
+ // bg-neutral-50 auto-inverts via CSS var swap (light in light mode,
7905
+ // dark in dark mode). Same semantic as TextField wrapper.
7906
+ 'bg-neutral-50',
7907
+ 'focus-within:ring-2 focus-within:ring-primary-500',
7908
+ 'transition-colors motion-reduce:transition-none'
7909
+ ],
7910
+ scroll: 'relative w-full overflow-auto rounded-md',
7911
+ table: 'w-full caption-bottom text-left',
7912
+ // thead matches the page surface (auto-inverts). When sticky the
7913
+ // header sits opaque against the scroll content beneath it.
7914
+ thead: 'bg-neutral-50',
7915
+ tbody: '',
7916
+ headerRow: '',
7917
+ headerCell: [
7918
+ // Less prominent than the body — secondary-text intent
7919
+ // (auto-inverts: dark grey in light mode, light grey in dark).
7920
+ 'text-neutral-700',
7921
+ 'font-semibold whitespace-nowrap select-none',
7922
+ 'border-b border-neutral-200'
7923
+ ],
7924
+ headerCellButton: [
7925
+ 'group inline-flex items-center gap-1 select-none',
7926
+ // hover increases prominence (auto-inverts).
7927
+ 'hover:text-neutral-900',
7928
+ 'focus:outline-none focus-visible:underline',
7929
+ 'transition-colors motion-reduce:transition-none'
7930
+ ],
7931
+ row: [
7932
+ 'group/row transition-colors motion-reduce:transition-none',
7933
+ // bg-neutral-100 = "elevated above surface" in both modes via
7934
+ // auto-inversion. Slightly visible against the table bg.
7935
+ 'hover:bg-neutral-100',
7936
+ // Canonical LeftNav `itemActive` pattern — primary palette
7937
+ // doesn't invert, so dark-navy text on light-blue bg in BOTH
7938
+ // modes. Guaranteed contrast.
7939
+ 'data-[selected=true]:bg-primary-100',
7940
+ 'data-[selected=true]:text-primary-900',
7941
+ 'data-[selected=true]:font-medium'
7942
+ ],
7943
+ cell: [
7944
+ // Auto-inverts — dark text in light mode, light text in dark mode.
7945
+ 'text-neutral-900',
7946
+ 'border-b border-neutral-200',
7947
+ 'align-middle'
7948
+ ],
7949
+ selectionCell: 'w-10 text-center',
7950
+ expandToggleCell: 'w-10 text-center',
7951
+ // Expanded panel uses one tier of elevation above the page surface.
7952
+ expandedRow: 'bg-neutral-100',
7953
+ rowActionsCell: [
7954
+ 'w-12 text-right',
7955
+ // Row actions hidden until row is hovered or focus-within
7956
+ // (Pencil & Paper UX pattern: reveal-on-hover reduces visual density).
7957
+ 'opacity-0 group-hover/row:opacity-100 group-focus-within/row:opacity-100',
7958
+ 'transition-opacity motion-reduce:transition-none'
7959
+ ],
7960
+ emptyState: [
7961
+ 'py-12 text-center text-neutral-500'
7962
+ ],
7963
+ loadingOverlay: [
7964
+ 'pointer-events-none'
7965
+ ],
7966
+ bulkActionFooter: [
7967
+ 'sticky bottom-0 z-20',
7968
+ 'flex flex-wrap items-center justify-between gap-3',
7969
+ 'px-3 py-2 mt-0',
7970
+ 'border-t border-neutral-200',
7971
+ // Surface-elevated bg + shadow ladder.
7972
+ 'bg-neutral-50 shadow-md'
7973
+ ]
7974
+ },
7975
+ variants: {
7976
+ variant: {
7977
+ plain: {},
7978
+ lines: {
7979
+ },
7980
+ striped: {
7981
+ // bg-neutral-100/60 = subtle elevation tier (auto-inverts).
7982
+ row: 'odd:bg-neutral-100/60'
7983
+ },
7984
+ bordered: {
7985
+ table: 'border border-neutral-200',
7986
+ cell: 'border-r last:border-r-0 border-neutral-200',
7987
+ headerCell: 'border-r last:border-r-0 border-neutral-200'
7988
+ },
7989
+ card: {
7990
+ scroll: 'overflow-visible',
7991
+ table: 'border-separate border-spacing-y-2',
7992
+ row: [
7993
+ 'rounded-lg shadow-sm',
7994
+ // Each row becomes an elevated card above the surface.
7995
+ 'bg-neutral-50'
7996
+ ],
7997
+ cell: 'border-b-0 first:rounded-l-lg last:rounded-r-lg',
7998
+ headerCell: 'border-b-0'
7999
+ }
8000
+ },
8001
+ size: {
8002
+ sm: {
8003
+ headerCell: 'text-xs',
8004
+ cell: 'text-xs',
8005
+ search: 'text-xs'
8006
+ },
8007
+ md: {
8008
+ headerCell: 'text-sm',
8009
+ cell: 'text-sm',
8010
+ search: 'text-sm'
8011
+ },
8012
+ lg: {
8013
+ headerCell: 'text-base',
8014
+ cell: 'text-base',
8015
+ search: 'text-base'
8016
+ }
8017
+ },
8018
+ density: {
8019
+ // Pencil & Paper UX research: 40px / 48px / 56px row height.
8020
+ compact: {
8021
+ headerCell: 'px-2 py-1',
8022
+ cell: 'px-2 py-1'
8023
+ },
8024
+ comfortable: {
8025
+ headerCell: 'px-3 py-2',
8026
+ cell: 'px-3 py-2'
8027
+ },
8028
+ spacious: {
8029
+ headerCell: 'px-4 py-3',
8030
+ cell: 'px-4 py-3'
8031
+ }
8032
+ },
8033
+ stickyHeader: {
8034
+ true: {
8035
+ thead: 'sticky top-0 z-10'
8036
+ },
8037
+ false: {}
8038
+ }
8039
+ },
8040
+ defaultVariants: {
8041
+ variant: 'lines',
8042
+ size: 'md',
8043
+ density: 'comfortable',
8044
+ stickyHeader: true
8045
+ }
8046
+ });
8047
+
8048
+ /**
8049
+ * Generic "controllable" state hook — switches between controlled
8050
+ * (via parent-supplied value + setter) and uncontrolled (internal
8051
+ * `useState`).
8052
+ *
8053
+ * - If `controlledValue !== undefined`, the hook is in **controlled**
8054
+ * mode: the internal state is ignored; the returned setter calls
8055
+ * `onControlledChange` only.
8056
+ * - Otherwise it's in **uncontrolled** mode: internal `useState`,
8057
+ * defaulting to `defaultValue`. The optional `onControlledChange`
8058
+ * is still fired on every change so the parent can react.
8059
+ *
8060
+ * The control mode is determined per-render. Mixing modes (passing
8061
+ * `controlledValue` undefined sometimes) is a consumer bug — we don't
8062
+ * try to "remember" the last mode (React's rules-of-hooks make that
8063
+ * unsafe).
8064
+ */ function useControllableState(args) {
8065
+ const { controlledValue, defaultValue, onChange } = args;
8066
+ const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);
8067
+ const isControlled = controlledValue !== undefined;
8068
+ const value = isControlled ? controlledValue : uncontrolledValue;
8069
+ // Stable ref to `onChange` so the setter doesn't change identity
8070
+ // every render (consumer can pass an inline lambda safely).
8071
+ const onChangeRef = useRef(onChange);
8072
+ onChangeRef.current = onChange;
8073
+ const setValue = useCallback((next)=>{
8074
+ const resolved = typeof next === 'function' ? next(value) : next;
8075
+ if (!isControlled) {
8076
+ setUncontrolledValue(resolved);
8077
+ }
8078
+ onChangeRef.current == null ? void 0 : onChangeRef.current.call(onChangeRef, resolved);
8079
+ }, [
8080
+ isControlled,
8081
+ value
8082
+ ]);
8083
+ return [
8084
+ value,
8085
+ setValue
8086
+ ];
8087
+ }
8088
+
8089
+ /**
8090
+ * Returns a debounced version of `value` — only updates after `delayMs`
8091
+ * of no further changes. Used by the search input so we don't re-filter
8092
+ * the entire row set on every keystroke.
8093
+ *
8094
+ * `delayMs <= 0` short-circuits to the input value (no debounce —
8095
+ * useful for tests).
8096
+ */ function useDebouncedValue(value, delayMs) {
8097
+ const [debounced, setDebounced] = useState(value);
8098
+ useEffect(()=>{
8099
+ if (delayMs <= 0) {
8100
+ setDebounced(value);
8101
+ return;
8102
+ }
8103
+ const timer = setTimeout(()=>setDebounced(value), delayMs);
8104
+ return ()=>clearTimeout(timer);
8105
+ }, [
8106
+ value,
8107
+ delayMs
8108
+ ]);
8109
+ return debounced;
8110
+ }
8111
+
8112
+ /**
8113
+ * Walk a dotted path through an object and return the value, or
8114
+ * `undefined` if any segment is missing / nullish.
8115
+ *
8116
+ * Bug fix from the MUI-side Table (Sprint 4.1 motivation): the
8117
+ * existing wrapper typed `field: NestedKeyOf<T>` but performed a
8118
+ * flat `row[col.field]` lookup at runtime — nested keys silently
8119
+ * returned `undefined`. This helper makes the runtime match the
8120
+ * type contract.
8121
+ *
8122
+ * @example
8123
+ * ```ts
8124
+ * getNestedValue({ user: { name: 'Jane' } }, 'user.name') // 'Jane'
8125
+ * getNestedValue({ user: null }, 'user.name') // undefined
8126
+ * getNestedValue({}, 'a.b.c') // undefined
8127
+ * getNestedValue({ a: { b: 0 } }, 'a.b') // 0 (NOT undefined)
8128
+ * ```
8129
+ */ function getNestedValue(row, path) {
8130
+ if (!path) return undefined;
8131
+ const segments = path.split('.');
8132
+ let current = row;
8133
+ for (const segment of segments){
8134
+ if (current == null) return undefined;
8135
+ if (typeof current !== 'object') return undefined;
8136
+ current = current[segment];
8137
+ }
8138
+ return current;
8139
+ }
8140
+
8141
+ /**
8142
+ * Stringify any value into a search-comparable form. Handles all
8143
+ * the edge cases that bit the MUI-side Table:
8144
+ *
8145
+ * - `null` / `undefined` → `''`
8146
+ * - `string` → as-is
8147
+ * - `number` / `bigint` → `String(value)`
8148
+ * - `boolean` → `'true'` / `'false'`
8149
+ * - `Date` → ISO 8601
8150
+ * - `Array` / object → `JSON.stringify` (best effort)
8151
+ */ function stringifyForSearch(value) {
8152
+ if (value == null) return '';
8153
+ if (typeof value === 'string') return value;
8154
+ if (typeof value === 'number' || typeof value === 'bigint') return String(value);
8155
+ if (typeof value === 'boolean') return value ? 'true' : 'false';
8156
+ if (value instanceof Date) return value.toISOString();
8157
+ if (Array.isArray(value)) {
8158
+ try {
8159
+ return value.map(stringifyForSearch).join(' ');
8160
+ } catch (unused) {
8161
+ return '';
8162
+ }
8163
+ }
8164
+ if (typeof value === 'object') {
8165
+ try {
8166
+ return JSON.stringify(value);
8167
+ } catch (unused) {
8168
+ return '';
8169
+ }
8170
+ }
8171
+ return String(value);
8172
+ }
8173
+ /**
8174
+ * Filters `rows` by `query` against the columns flagged
8175
+ * `searchable: true`. Empty / whitespace-only query is a no-op
8176
+ * (returns the original rows).
8177
+ *
8178
+ * Case-insensitive substring match. The caller is responsible for
8179
+ * debouncing the input (use `useDebouncedValue`).
8180
+ */ function useTableSearch(rows, cols, query) {
8181
+ return useMemo(()=>{
8182
+ const trimmed = query.trim();
8183
+ if (!trimmed) return rows;
8184
+ const needle = trimmed.toLowerCase();
8185
+ const searchableCols = cols.filter((c)=>c.searchable);
8186
+ if (searchableCols.length === 0) return rows;
8187
+ return rows.filter((row)=>searchableCols.some((col)=>stringifyForSearch(getNestedValue(row, col.field)).toLowerCase().includes(needle)));
8188
+ }, [
8189
+ rows,
8190
+ cols,
8191
+ query
8192
+ ]);
8193
+ }
8194
+
8195
+ /**
8196
+ * Filters `rows` by the per-column filter model. v1 only supports
8197
+ * the `contains` operator (text substring) — additional ops
8198
+ * (`equals`, `in`, `gte`, `lte`, `between`, `before`, `after`)
8199
+ * land in v1-bis once the filter UI surface for each shape is
8200
+ * fleshed out.
8201
+ *
8202
+ * AND semantics across columns: a row must match EVERY active
8203
+ * filter. Within a single filter, the op decides the predicate.
8204
+ */ function useTableFilter(rows, filterModel) {
8205
+ return useMemo(()=>{
8206
+ if (!filterModel || filterModel.length === 0) return rows;
8207
+ return rows.filter((row)=>filterModel.every((filter)=>{
8208
+ const value = getNestedValue(row, filter.field);
8209
+ const stringValue = stringifyForSearch(value).toLowerCase();
8210
+ const needle = filter.value.trim().toLowerCase();
8211
+ if (!needle) return true; // empty filter == no-op
8212
+ switch(filter.op){
8213
+ case 'contains':
8214
+ return stringValue.includes(needle);
8215
+ default:
8216
+ return true;
8217
+ }
8218
+ }));
8219
+ }, [
8220
+ rows,
8221
+ filterModel
8222
+ ]);
8223
+ }
8224
+
8225
+ /**
8226
+ * Default comparator. Handles the common primitive types in a
8227
+ * sensible way:
8228
+ *
8229
+ * - `null` / `undefined` always sort LAST (stable across asc/desc)
8230
+ * - `number` / `bigint` → numeric comparison
8231
+ * - `string` → locale-aware compare via `Intl.Collator`
8232
+ * - `Date` / ISO-string-date → numeric compare on epoch ms
8233
+ * - `boolean` → `false < true`
8234
+ * - mixed types fall back to string compare
8235
+ *
8236
+ * The locale collator is created once per call (cheap; cached
8237
+ * upstream via memoization).
8238
+ */ function defaultCompare(a, b) {
8239
+ // Numbers
8240
+ if (typeof a === 'number' && typeof b === 'number') {
8241
+ return a - b;
8242
+ }
8243
+ if (typeof a === 'bigint' && typeof b === 'bigint') {
8244
+ return a < b ? -1 : a > b ? 1 : 0;
8245
+ }
8246
+ // Dates
8247
+ if (a instanceof Date && b instanceof Date) {
8248
+ return a.getTime() - b.getTime();
8249
+ }
8250
+ // Booleans
8251
+ if (typeof a === 'boolean' && typeof b === 'boolean') {
8252
+ return a === b ? 0 : a ? 1 : -1;
8253
+ }
8254
+ // Strings (incl. ISO date strings — natural lexical ordering matches chronology)
8255
+ if (typeof a === 'string' && typeof b === 'string') {
8256
+ return a.localeCompare(b, undefined, {
8257
+ numeric: true,
8258
+ sensitivity: 'base'
8259
+ });
8260
+ }
8261
+ // Fallback: stringify and compare
8262
+ return String(a).localeCompare(String(b), undefined, {
8263
+ numeric: true,
8264
+ sensitivity: 'base'
8265
+ });
8266
+ }
8267
+ /**
8268
+ * Applies the `sortModel` to `rows`. Returns a NEW sorted array
8269
+ * (does not mutate input). Stable sort guaranteed by the underlying
8270
+ * `Array.prototype.sort` in modern engines.
8271
+ *
8272
+ * Multi-column sort: items earlier in `sortModel` have higher
8273
+ * precedence. Equal values fall through to the next sort item; if
8274
+ * all sort items tie, the original order is preserved (stable).
8275
+ */ function useTableSort(rows, cols, sortModel) {
8276
+ return useMemo(()=>{
8277
+ if (!sortModel || sortModel.length === 0) return rows;
8278
+ const sortConfigs = sortModel.map((item)=>{
8279
+ const col = cols.find((c)=>c.field === item.field);
8280
+ if (!col || !col.sortable) return null;
8281
+ const customCompare = typeof col.sortable === 'function' ? col.sortable : undefined;
8282
+ return {
8283
+ item,
8284
+ col,
8285
+ customCompare
8286
+ };
8287
+ }).filter((x)=>x != null);
8288
+ if (sortConfigs.length === 0) return rows;
8289
+ // Decorate-sort-undecorate for stability across engines.
8290
+ //
8291
+ // Per sort item: null/undefined values always sort LAST, regardless
8292
+ // of direction. This invariant lives outside the comparator so it's
8293
+ // not inverted by `-cmp` when direction is `desc`. (Without this
8294
+ // separation, `desc` flipped null-handling and placed nulls FIRST,
8295
+ // which broke user expectations — fixed in the v1 implementation.)
8296
+ return [
8297
+ ...rows
8298
+ ].sort((a, b)=>{
8299
+ for (const { item, customCompare } of sortConfigs){
8300
+ const aVal = customCompare ? a : getNestedValue(a, item.field);
8301
+ const bVal = customCompare ? b : getNestedValue(b, item.field);
8302
+ // Null-last (direction-independent)
8303
+ const aNull = aVal == null;
8304
+ const bNull = bVal == null;
8305
+ if (aNull && bNull) continue;
8306
+ if (aNull) return 1;
8307
+ if (bNull) return -1;
8308
+ const cmp = customCompare ? customCompare(a, b) : defaultCompare(aVal, bVal);
8309
+ if (cmp !== 0) {
8310
+ return item.direction === 'desc' ? -cmp : cmp;
8311
+ }
8312
+ }
8313
+ return 0;
8314
+ });
8315
+ }, [
8316
+ rows,
8317
+ cols,
8318
+ sortModel
8319
+ ]);
8320
+ }
8321
+ /**
8322
+ * Three-state sort cycler: `none` → `asc` → `desc` → `none`.
8323
+ *
8324
+ * Used by the header click handler. Returns the next sort model
8325
+ * to apply, given the current model + the clicked field + whether
8326
+ * the user is multi-sorting (shift key).
8327
+ */ function cycleSortFor(current, field, multi) {
8328
+ const existing = current.find((s)=>s.field === field);
8329
+ const nextDirection = !existing ? 'asc' : existing.direction === 'asc' ? 'desc' : null;
8330
+ if (multi) {
8331
+ // Multi-sort: keep other entries, update / add / remove this one.
8332
+ const rest = current.filter((s)=>s.field !== field);
8333
+ if (nextDirection == null) return rest;
8334
+ return [
8335
+ ...rest,
8336
+ {
8337
+ field,
8338
+ direction: nextDirection
8339
+ }
8340
+ ];
8341
+ }
8342
+ // Single-sort: replace the whole model.
8343
+ if (nextDirection == null) return [];
8344
+ return [
8345
+ {
8346
+ field,
8347
+ direction: nextDirection
8348
+ }
8349
+ ];
8350
+ }
8351
+
8352
+ /**
8353
+ * Selection logic helper. Given the controllable selection state +
8354
+ * the setter (from `useControllableState`), exposes ergonomic
8355
+ * helpers the Table calls from its render path.
8356
+ *
8357
+ * - `single` mode: at most one row selected. Toggling another
8358
+ * replaces the previous.
8359
+ * - `multiple` mode: any subset selectable; toggling adds/removes.
8360
+ * - `none` mode: every toggle is a no-op (the Table shouldn't
8361
+ * render the checkbox cells anyway, but this is defensive).
8362
+ */ function useTableSelection(mode, selectedRowIds, setSelectedRowIds) {
8363
+ const selectedSet = useMemo(()=>new Set(selectedRowIds), [
8364
+ selectedRowIds
8365
+ ]);
8366
+ const isSelected = useCallback((rowId)=>selectedSet.has(rowId), [
8367
+ selectedSet
8368
+ ]);
8369
+ const toggleRow = useCallback((rowId)=>{
8370
+ if (mode === 'none') return;
8371
+ if (mode === 'single') {
8372
+ setSelectedRowIds(selectedSet.has(rowId) ? [] : [
8373
+ rowId
8374
+ ]);
8375
+ return;
8376
+ }
8377
+ // multiple
8378
+ setSelectedRowIds((prev)=>{
8379
+ const next = new Set(prev);
8380
+ if (next.has(rowId)) next.delete(rowId);
8381
+ else next.add(rowId);
8382
+ return [
8383
+ ...next
8384
+ ];
8385
+ });
8386
+ }, [
8387
+ mode,
8388
+ selectedSet,
8389
+ setSelectedRowIds
8390
+ ]);
8391
+ const toggleAll = useCallback((allRowIds)=>{
8392
+ if (mode !== 'multiple') return;
8393
+ const allSelected = allRowIds.every((id)=>selectedSet.has(id));
8394
+ setSelectedRowIds(allSelected ? [] : [
8395
+ ...allRowIds
8396
+ ]);
8397
+ }, [
8398
+ mode,
8399
+ selectedSet,
8400
+ setSelectedRowIds
8401
+ ]);
8402
+ const clearSelection = useCallback(()=>{
8403
+ setSelectedRowIds([]);
8404
+ }, [
8405
+ setSelectedRowIds
8406
+ ]);
8407
+ const isAllSelected = useCallback((allRowIds)=>{
8408
+ if (allRowIds.length === 0) return false;
8409
+ return allRowIds.every((id)=>selectedSet.has(id));
8410
+ }, [
8411
+ selectedSet
8412
+ ]);
8413
+ const isIndeterminate = useCallback((allRowIds)=>{
8414
+ if (allRowIds.length === 0) return false;
8415
+ const some = allRowIds.some((id)=>selectedSet.has(id));
8416
+ const all = allRowIds.every((id)=>selectedSet.has(id));
8417
+ return some && !all;
8418
+ }, [
8419
+ selectedSet
8420
+ ]);
8421
+ return {
8422
+ selectedRowIds,
8423
+ isSelected,
8424
+ toggleRow,
8425
+ toggleAll,
8426
+ clearSelection,
8427
+ isAllSelected,
8428
+ isIndeterminate
8429
+ };
8430
+ }
8431
+
8432
+ /**
8433
+ * Auto-detects the primitive type of each column by scanning the
8434
+ * first non-null value across the supplied rows. Drives smart
8435
+ * defaults for `align` and `monospace` on the cell render path.
8436
+ *
8437
+ * Scans only the first N rows (default 50) for performance — if the
8438
+ * dataset's first 50 rows are unrepresentative, the consumer can
8439
+ * override per-column via explicit `align` / `monospace` props.
8440
+ *
8441
+ * Returns `Map<columnField, inferredType>`.
8442
+ */ function useColumnAutoDetect(rows, cols, sampleSize = 50) {
8443
+ return useMemo(()=>{
8444
+ const result = new Map();
8445
+ const sample = rows.slice(0, sampleSize);
8446
+ for (const col of cols){
8447
+ const field = col.field;
8448
+ let inferred = 'unknown';
8449
+ for (const row of sample){
8450
+ const value = getNestedValue(row, field);
8451
+ if (value == null) continue;
8452
+ inferred = inferType(value);
8453
+ break; // first non-null wins
8454
+ }
8455
+ result.set(field, inferred);
8456
+ }
8457
+ return result;
8458
+ }, [
8459
+ rows,
8460
+ cols,
8461
+ sampleSize
8462
+ ]);
8463
+ }
8464
+ function inferType(value) {
8465
+ if (typeof value === 'number') return 'number';
8466
+ if (typeof value === 'boolean') return 'boolean';
8467
+ if (value instanceof Date) return 'date';
8468
+ if (typeof value === 'string') {
8469
+ // Heuristic: ISO date string (YYYY-MM-DD[T...]) → 'date'
8470
+ if (/^\d{4}-\d{2}-\d{2}/.test(value)) return 'date';
8471
+ return 'string';
8472
+ }
8473
+ return 'unknown';
8474
+ }
8475
+ /**
8476
+ * Resolves the effective `align` for a column, honoring explicit
8477
+ * `col.align` first and falling back to the inferred type.
8478
+ */ function resolveAlign(col, inferred) {
8479
+ if (col.align) return col.align;
8480
+ switch(inferred){
8481
+ case 'number':
8482
+ return 'right';
8483
+ case 'boolean':
8484
+ return 'center';
8485
+ default:
8486
+ return 'left';
8487
+ }
8488
+ }
8489
+ /**
8490
+ * Resolves the effective `tabularNums` flag for a column.
8491
+ *
8492
+ * `tabular-nums` is a `font-variant-numeric` rule that makes every
8493
+ * digit occupy the same width — it does NOT change the font family.
8494
+ * Auto-applied for `number` and `date` columns so currency values
8495
+ * and ISO dates align cleanly. Explicit `col.tabularNums` wins.
8496
+ */ function resolveTabularNums(col, inferred) {
8497
+ if (col.tabularNums !== undefined) return col.tabularNums;
8498
+ return inferred === 'number' || inferred === 'date';
8499
+ }
8500
+ /**
8501
+ * Resolves the effective `monospace` flag for a column.
8502
+ *
8503
+ * `font-mono` changes the font FAMILY to a monospace stack, which
8504
+ * would override the consumer's theme `font-sans`. The library
8505
+ * never auto-applies it — only an explicit `col.monospace: true`
8506
+ * opts in. Consumers wanting a custom mono stack configure it
8507
+ * through Tailwind's `theme.extend.fontFamily.mono` in their own
8508
+ * preset (the dashforgePreset does not own the `fontFamily` axis).
8509
+ */ function resolveMonospace(col) {
8510
+ return col.monospace === true;
8511
+ }
8512
+
8513
+ const DEFAULT_LABELS = {
8514
+ searchPlaceholder: 'Search...',
8515
+ noData: 'No data',
8516
+ loading: 'Loading…',
8517
+ ariaSortNone: 'Not sorted. Click to sort ascending.',
8518
+ ariaSortAscending: 'Sorted ascending. Click to sort descending.',
8519
+ ariaSortDescending: 'Sorted descending. Click to clear sort.',
8520
+ ariaSelectRow: 'Select row',
8521
+ ariaSelectAllRows: 'Select all rows',
8522
+ ariaExpandRow: 'Expand row',
8523
+ ariaCollapseRow: 'Collapse row',
8524
+ selectedCount: '{count} selected',
8525
+ clearSelection: 'Clear selection',
8526
+ filterColumn: 'Filter',
8527
+ filterApply: 'Apply',
8528
+ filterClear: 'Clear',
8529
+ density: 'Density',
8530
+ densityCompact: 'Compact',
8531
+ densityComfortable: 'Comfortable',
8532
+ densitySpacious: 'Spacious'
8533
+ };
8534
+ const DEFAULT_GET_ROW_ID = (_row, index)=>String(index);
8535
+ /**
8536
+ * Dashforge TW `<Table>` — declarative-first, market-grounded table.
8537
+ *
8538
+ * Design references:
8539
+ * - Visual: Stripe (clean line dividers, hover row actions, sticky header)
8540
+ * - Density: Pencil & Paper UX research (compact / comfortable / spacious)
8541
+ * - A11Y: W3C WAI Table tutorial (semantic `<table>`, scope, aria-sort)
8542
+ * - Column model: extended from Dashforge MUI Table existing API
8543
+ *
8544
+ * Smart defaults: column types auto-detected → numbers right-aligned
8545
+ * + tabular-nums, dates monospace, booleans center-aligned. Override
8546
+ * via explicit `align` / `monospace` per column.
8547
+ *
8548
+ * State (sort / search / filter / selection / expansion) is internal
8549
+ * by default; pass the controlled prop + matching change handler to
8550
+ * lift state up — same pattern as `<TextField>` value.
8551
+ *
8552
+ * i18n: pass `header: t('users.name')` for column headers; pass
8553
+ * `labels={{ searchPlaceholder: t('...'), ... }}` for internal default
8554
+ * strings.
8555
+ */ function Table(props) {
8556
+ var _slotProps_root, _slotProps_toolbar, _slotProps_search, _slotProps_scroll, _slotProps_table, _slotProps_thead, _slotProps_headerRow, _slotProps_headerCell, _slotProps_selectionCell, _slotProps_headerCell1, _slotProps_expandToggleCell, _slotProps_headerCell2, _slotProps_tbody, _slotProps_row, _slotProps_cell, _slotProps_emptyState, _slotProps_bulkActionFooter;
8557
+ const { rows, cols, getRowId = DEFAULT_GET_ROW_ID, // State props (controllable)
8558
+ sortModel: sortModelProp, onSortChange, defaultSortModel, enableSearch = false, searchQuery: searchQueryProp, onSearchQueryChange, searchPlaceholder, searchDebounceMs = 200, filterModel: filterModelProp, onFilterChange, rowSelection = 'none', selectedRowIds: selectedRowIdsProp, onSelectionChange, bulkActions, expandable, rowActions, loading = false, loadingRowCount = 5, emptyState, stickyHeader = true, caption, showCaption = false, access: tableAccess, labels: labelsProp, variant, size, density, sx, slotProps } = props;
8559
+ const labels = _extends({}, DEFAULT_LABELS, labelsProp);
8560
+ const tableAccessState = useAccessState(tableAccess);
8561
+ // ───── Controllable state machinery ─────
8562
+ const [sortModel, setSortModel] = useControllableState({
8563
+ controlledValue: sortModelProp,
8564
+ defaultValue: defaultSortModel != null ? defaultSortModel : [],
8565
+ onChange: onSortChange
8566
+ });
8567
+ const [searchQuery, setSearchQuery] = useControllableState({
8568
+ controlledValue: searchQueryProp,
8569
+ defaultValue: '',
8570
+ onChange: onSearchQueryChange
8571
+ });
8572
+ const filterModel = filterModelProp != null ? filterModelProp : [];
8573
+ const [selectedRowIds, setSelectedRowIds] = useControllableState({
8574
+ controlledValue: selectedRowIdsProp,
8575
+ defaultValue: [],
8576
+ onChange: onSelectionChange
8577
+ });
8578
+ const [expandedRowIds, setExpandedRowIds] = useControllableState({
8579
+ controlledValue: expandable == null ? void 0 : expandable.expandedRowIds,
8580
+ defaultValue: [],
8581
+ onChange: expandable == null ? void 0 : expandable.onExpandChange
8582
+ });
8583
+ // ───── RBAC: filter out hidden columns ─────
8584
+ const visibleCols = useMemo(()=>cols.filter((col)=>!col.access || col.access.onUnauthorized !== 'hide'), [
8585
+ cols
8586
+ ]);
8587
+ // ───── Smart-default column types ─────
8588
+ const columnTypes = useColumnAutoDetect(rows, visibleCols);
8589
+ // ───── Pipeline: search → filter → sort ─────
8590
+ const debouncedSearchQuery = useDebouncedValue(searchQuery, searchDebounceMs);
8591
+ const searchedRows = useTableSearch(rows, visibleCols, debouncedSearchQuery);
8592
+ const filteredRows = useTableFilter(searchedRows, filterModel);
8593
+ const sortedRows = useTableSort(filteredRows, visibleCols, sortModel);
8594
+ // ───── Selection helpers ─────
8595
+ const visibleRowIds = useMemo(()=>sortedRows.map((row, idx)=>getRowId(row, idx)), [
8596
+ sortedRows,
8597
+ getRowId
8598
+ ]);
8599
+ const selection = useTableSelection(rowSelection, selectedRowIds, setSelectedRowIds);
8600
+ const selectedRows = useMemo(()=>sortedRows.filter((row, idx)=>selection.isSelected(getRowId(row, idx))), [
8601
+ sortedRows,
8602
+ selection,
8603
+ getRowId
8604
+ ]);
8605
+ // ───── Variant recipe ─────
8606
+ const v = tableVariants({
8607
+ variant,
8608
+ size,
8609
+ density,
8610
+ stickyHeader
8611
+ });
8612
+ // ───── Render ─────
8613
+ if (!tableAccessState.visible) return null;
8614
+ const isTableInteractive = !tableAccessState.disabled;
8615
+ const showSelectionColumn = rowSelection !== 'none';
8616
+ const showExpandColumn = expandable != null;
8617
+ const showRowActionsColumn = rowActions != null;
8618
+ const totalColumnCount = visibleCols.length + (showSelectionColumn ? 1 : 0) + (showExpandColumn ? 1 : 0) + (showRowActionsColumn ? 1 : 0);
8619
+ return jsxs("div", {
8620
+ className: cn(v.root(), sx, slotProps == null ? void 0 : (_slotProps_root = slotProps.root) == null ? void 0 : _slotProps_root.className),
8621
+ "data-disabled": !isTableInteractive ? 'true' : undefined,
8622
+ children: [
8623
+ enableSearch && jsx("div", {
8624
+ className: cn(v.toolbar(), slotProps == null ? void 0 : (_slotProps_toolbar = slotProps.toolbar) == null ? void 0 : _slotProps_toolbar.className),
8625
+ children: jsxs("label", {
8626
+ className: cn(v.search(), slotProps == null ? void 0 : (_slotProps_search = slotProps.search) == null ? void 0 : _slotProps_search.className),
8627
+ children: [
8628
+ jsx("span", {
8629
+ className: "sr-only",
8630
+ children: labels.searchPlaceholder
8631
+ }),
8632
+ jsx(SearchIcon, {
8633
+ className: "ml-2 shrink-0 text-neutral-500"
8634
+ }),
8635
+ jsx("input", {
8636
+ type: "text",
8637
+ value: searchQuery,
8638
+ onChange: (e)=>setSearchQuery(e.target.value),
8639
+ placeholder: searchPlaceholder != null ? searchPlaceholder : labels.searchPlaceholder,
8640
+ disabled: !isTableInteractive,
8641
+ className: cn('flex-1 bg-transparent border-0 outline-none', 'px-2 py-1.5', // placeholder color auto-inverts via the CSS-var preset;
8642
+ // no `dark:` variant needed on neutral palette.
8643
+ 'placeholder:text-neutral-400', 'disabled:opacity-50 disabled:cursor-not-allowed')
8644
+ })
8645
+ ]
8646
+ })
8647
+ }),
8648
+ jsx("div", {
8649
+ className: cn(v.scroll(), slotProps == null ? void 0 : (_slotProps_scroll = slotProps.scroll) == null ? void 0 : _slotProps_scroll.className),
8650
+ children: jsxs("table", {
8651
+ className: cn(v.table(), slotProps == null ? void 0 : (_slotProps_table = slotProps.table) == null ? void 0 : _slotProps_table.className),
8652
+ children: [
8653
+ caption != null && jsx("caption", {
8654
+ className: cn(!showCaption && 'sr-only', 'py-2 text-sm text-neutral-500'),
8655
+ children: caption
8656
+ }),
8657
+ jsx("thead", {
8658
+ className: cn(v.thead(), slotProps == null ? void 0 : (_slotProps_thead = slotProps.thead) == null ? void 0 : _slotProps_thead.className),
8659
+ children: jsxs("tr", {
8660
+ className: cn(v.headerRow(), slotProps == null ? void 0 : (_slotProps_headerRow = slotProps.headerRow) == null ? void 0 : _slotProps_headerRow.className),
8661
+ children: [
8662
+ showSelectionColumn && jsx("th", {
8663
+ scope: "col",
8664
+ className: cn(v.headerCell(), v.selectionCell(), slotProps == null ? void 0 : (_slotProps_headerCell = slotProps.headerCell) == null ? void 0 : _slotProps_headerCell.className, slotProps == null ? void 0 : (_slotProps_selectionCell = slotProps.selectionCell) == null ? void 0 : _slotProps_selectionCell.className),
8665
+ children: rowSelection === 'multiple' && jsx("input", {
8666
+ type: "checkbox",
8667
+ "aria-label": labels.ariaSelectAllRows,
8668
+ checked: selection.isAllSelected(visibleRowIds),
8669
+ ref: (el)=>{
8670
+ if (el) el.indeterminate = selection.isIndeterminate(visibleRowIds);
8671
+ },
8672
+ onChange: ()=>selection.toggleAll(visibleRowIds),
8673
+ disabled: !isTableInteractive,
8674
+ className: "cursor-pointer"
8675
+ })
8676
+ }),
8677
+ showExpandColumn && jsx("th", {
8678
+ scope: "col",
8679
+ "aria-hidden": "true",
8680
+ className: cn(v.headerCell(), v.expandToggleCell(), slotProps == null ? void 0 : (_slotProps_headerCell1 = slotProps.headerCell) == null ? void 0 : _slotProps_headerCell1.className, slotProps == null ? void 0 : (_slotProps_expandToggleCell = slotProps.expandToggleCell) == null ? void 0 : _slotProps_expandToggleCell.className)
8681
+ }),
8682
+ visibleCols.map((col)=>{
8683
+ var _slotProps_headerCell;
8684
+ return jsx(HeaderCell, {
8685
+ col: col,
8686
+ sortModel: sortModel,
8687
+ onSortClick: (e)=>handleSortClick(col, e, sortModel, setSortModel),
8688
+ inferredType: columnTypes.get(col.field),
8689
+ labels: labels,
8690
+ className: cn(v.headerCell(), slotProps == null ? void 0 : (_slotProps_headerCell = slotProps.headerCell) == null ? void 0 : _slotProps_headerCell.className),
8691
+ buttonClassName: v.headerCellButton(),
8692
+ disabled: !isTableInteractive
8693
+ }, col.field);
8694
+ }),
8695
+ showRowActionsColumn && jsx("th", {
8696
+ scope: "col",
8697
+ "aria-hidden": "true",
8698
+ className: cn(v.headerCell(), 'w-12', slotProps == null ? void 0 : (_slotProps_headerCell2 = slotProps.headerCell) == null ? void 0 : _slotProps_headerCell2.className)
8699
+ })
8700
+ ]
8701
+ })
8702
+ }),
8703
+ jsx("tbody", {
8704
+ className: cn(v.tbody(), slotProps == null ? void 0 : (_slotProps_tbody = slotProps.tbody) == null ? void 0 : _slotProps_tbody.className),
8705
+ children: loading ? renderLoadingRows({
8706
+ count: loadingRowCount,
8707
+ visibleCols,
8708
+ showSelectionColumn,
8709
+ showExpandColumn,
8710
+ showRowActionsColumn,
8711
+ rowClass: cn(v.row(), slotProps == null ? void 0 : (_slotProps_row = slotProps.row) == null ? void 0 : _slotProps_row.className),
8712
+ cellClass: cn(v.cell(), slotProps == null ? void 0 : (_slotProps_cell = slotProps.cell) == null ? void 0 : _slotProps_cell.className)
8713
+ }) : sortedRows.length === 0 ? renderEmptyState({
8714
+ totalColumnCount,
8715
+ emptyState,
8716
+ fallback: labels.noData,
8717
+ cellClass: cn(v.cell(), v.emptyState(), slotProps == null ? void 0 : (_slotProps_emptyState = slotProps.emptyState) == null ? void 0 : _slotProps_emptyState.className)
8718
+ }) : sortedRows.map((row, idx)=>{
8719
+ var _slotProps_row, _slotProps_cell, _slotProps_cell1, _slotProps_selectionCell, _slotProps_cell2, _slotProps_expandToggleCell, _slotProps_cell3, _slotProps_rowActionsCell, _slotProps_expandedRow;
8720
+ const rowId = getRowId(row, idx);
8721
+ const isSelected = selection.isSelected(rowId);
8722
+ const isExpanded = expandedRowIds.includes(rowId);
8723
+ return jsx(Row, {
8724
+ row: row,
8725
+ rowId: rowId,
8726
+ rowIndex: idx,
8727
+ isSelected: isSelected,
8728
+ isExpanded: isExpanded,
8729
+ visibleCols: visibleCols,
8730
+ columnTypes: columnTypes,
8731
+ rowSelection: rowSelection,
8732
+ showSelectionColumn: showSelectionColumn,
8733
+ showExpandColumn: showExpandColumn,
8734
+ showRowActionsColumn: showRowActionsColumn,
8735
+ expandable: expandable,
8736
+ rowActions: rowActions,
8737
+ labels: labels,
8738
+ toggleRow: selection.toggleRow,
8739
+ toggleExpand: (id)=>setExpandedRowIds((prev)=>prev.includes(id) ? prev.filter((p)=>p !== id) : [
8740
+ ...prev,
8741
+ id
8742
+ ]),
8743
+ isInteractive: isTableInteractive,
8744
+ rowClass: cn(v.row(), slotProps == null ? void 0 : (_slotProps_row = slotProps.row) == null ? void 0 : _slotProps_row.className),
8745
+ cellClass: cn(v.cell(), slotProps == null ? void 0 : (_slotProps_cell = slotProps.cell) == null ? void 0 : _slotProps_cell.className),
8746
+ selectionCellClass: cn(v.cell(), v.selectionCell(), slotProps == null ? void 0 : (_slotProps_cell1 = slotProps.cell) == null ? void 0 : _slotProps_cell1.className, slotProps == null ? void 0 : (_slotProps_selectionCell = slotProps.selectionCell) == null ? void 0 : _slotProps_selectionCell.className),
8747
+ expandToggleCellClass: cn(v.cell(), v.expandToggleCell(), slotProps == null ? void 0 : (_slotProps_cell2 = slotProps.cell) == null ? void 0 : _slotProps_cell2.className, slotProps == null ? void 0 : (_slotProps_expandToggleCell = slotProps.expandToggleCell) == null ? void 0 : _slotProps_expandToggleCell.className),
8748
+ rowActionsCellClass: cn(v.cell(), v.rowActionsCell(), slotProps == null ? void 0 : (_slotProps_cell3 = slotProps.cell) == null ? void 0 : _slotProps_cell3.className, slotProps == null ? void 0 : (_slotProps_rowActionsCell = slotProps.rowActionsCell) == null ? void 0 : _slotProps_rowActionsCell.className),
8749
+ expandedRowClass: cn(v.expandedRow(), slotProps == null ? void 0 : (_slotProps_expandedRow = slotProps.expandedRow) == null ? void 0 : _slotProps_expandedRow.className),
8750
+ totalColumnCount: totalColumnCount
8751
+ }, rowId);
8752
+ })
8753
+ })
8754
+ ]
8755
+ })
8756
+ }),
8757
+ bulkActions && selectedRows.length > 0 && jsxs("div", {
8758
+ className: cn(v.bulkActionFooter(), slotProps == null ? void 0 : (_slotProps_bulkActionFooter = slotProps.bulkActionFooter) == null ? void 0 : _slotProps_bulkActionFooter.className),
8759
+ role: "region",
8760
+ "aria-label": labels.selectedCount.replace('{count}', String(selectedRows.length)),
8761
+ children: [
8762
+ jsx("span", {
8763
+ className: "text-sm text-neutral-700",
8764
+ children: labels.selectedCount.replace('{count}', String(selectedRows.length))
8765
+ }),
8766
+ jsxs("div", {
8767
+ className: "flex items-center gap-2",
8768
+ children: [
8769
+ bulkActions(selectedRows),
8770
+ jsx("button", {
8771
+ type: "button",
8772
+ onClick: selection.clearSelection,
8773
+ className: cn(// text-neutral-500 → text-neutral-900 on hover; both auto-invert.
8774
+ 'text-sm text-neutral-500 hover:text-neutral-900', 'underline-offset-2 hover:underline', 'focus:outline-none focus-visible:underline'),
8775
+ children: labels.clearSelection
8776
+ })
8777
+ ]
8778
+ })
8779
+ ]
8780
+ })
8781
+ ]
8782
+ });
8783
+ }
8784
+ // ───── Header cell (with sort button) ─────
8785
+ function HeaderCell(props) {
8786
+ const { col, sortModel, onSortClick, inferredType, labels, className, buttonClassName, disabled } = props;
8787
+ const align = resolveAlign(col, inferredType);
8788
+ const alignClass = align === 'right' ? 'text-right' : align === 'center' ? 'text-center' : 'text-left';
8789
+ const sortEntry = sortModel.find((s)=>s.field === col.field);
8790
+ const ariaSort = (sortEntry == null ? void 0 : sortEntry.direction) === 'asc' ? 'ascending' : (sortEntry == null ? void 0 : sortEntry.direction) === 'desc' ? 'descending' : 'none';
8791
+ const headerContent = typeof col.header === 'function' ? col.header() : col.header;
8792
+ const cellStyle = {
8793
+ width: col.width != null ? `${col.width}px` : undefined,
8794
+ minWidth: col.minWidth != null ? `${col.minWidth}px` : undefined,
8795
+ flex: col.flex
8796
+ };
8797
+ if (col.sortable) {
8798
+ return jsx("th", {
8799
+ scope: "col",
8800
+ "aria-sort": ariaSort,
8801
+ className: cn(className, alignClass),
8802
+ style: cellStyle,
8803
+ children: jsxs("button", {
8804
+ type: "button",
8805
+ onClick: onSortClick,
8806
+ disabled: disabled,
8807
+ "aria-label": ariaSort === 'ascending' ? labels.ariaSortAscending : ariaSort === 'descending' ? labels.ariaSortDescending : labels.ariaSortNone,
8808
+ className: cn(buttonClassName, align === 'right' && 'flex-row-reverse', 'disabled:cursor-not-allowed disabled:opacity-50'),
8809
+ children: [
8810
+ jsx("span", {
8811
+ children: headerContent
8812
+ }),
8813
+ jsx(SortIcon, {
8814
+ direction: sortEntry == null ? void 0 : sortEntry.direction
8815
+ })
8816
+ ]
8817
+ })
8818
+ });
8819
+ }
8820
+ return jsx("th", {
8821
+ scope: "col",
8822
+ className: cn(className, alignClass),
8823
+ style: cellStyle,
8824
+ children: headerContent
8825
+ });
8826
+ }
8827
+ // ───── Data row ─────
8828
+ function Row(props) {
8829
+ const { row, rowId, rowIndex, isSelected, isExpanded, visibleCols, columnTypes, rowSelection, showSelectionColumn, showExpandColumn, showRowActionsColumn, expandable, rowActions, labels, toggleRow, toggleExpand, isInteractive, rowClass, cellClass, selectionCellClass, expandToggleCellClass, rowActionsCellClass, expandedRowClass, totalColumnCount } = props;
8830
+ return jsxs(Fragment$1, {
8831
+ children: [
8832
+ jsxs("tr", {
8833
+ "data-selected": isSelected ? 'true' : 'false',
8834
+ "aria-selected": rowSelection !== 'none' ? isSelected : undefined,
8835
+ className: rowClass,
8836
+ children: [
8837
+ showSelectionColumn && jsx("td", {
8838
+ className: selectionCellClass,
8839
+ children: jsx("input", {
8840
+ type: "checkbox",
8841
+ "aria-label": labels.ariaSelectRow,
8842
+ checked: isSelected,
8843
+ onChange: ()=>toggleRow(rowId),
8844
+ disabled: !isInteractive,
8845
+ className: "cursor-pointer"
8846
+ })
8847
+ }),
8848
+ showExpandColumn && jsx("td", {
8849
+ className: expandToggleCellClass,
8850
+ children: jsx("button", {
8851
+ type: "button",
8852
+ onClick: ()=>toggleExpand(rowId),
8853
+ "aria-label": isExpanded ? labels.ariaCollapseRow : labels.ariaExpandRow,
8854
+ "aria-expanded": isExpanded,
8855
+ disabled: !isInteractive,
8856
+ className: cn('inline-flex items-center justify-center h-6 w-6 rounded', // text-neutral-500 + hover bump; both auto-invert via CSS-var preset.
8857
+ 'text-neutral-500 hover:text-neutral-900', 'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500', 'transition-transform motion-reduce:transition-none', isExpanded && 'rotate-90'),
8858
+ children: jsx(ChevronRightIcon, {})
8859
+ })
8860
+ }),
8861
+ visibleCols.map((col)=>jsx(DataCell, {
8862
+ col: col,
8863
+ row: row,
8864
+ rowIndex: rowIndex,
8865
+ isSelected: isSelected,
8866
+ isExpanded: isExpanded,
8867
+ inferredType: columnTypes.get(col.field),
8868
+ cellClass: cellClass
8869
+ }, col.field)),
8870
+ showRowActionsColumn && rowActions && jsx("td", {
8871
+ className: rowActionsCellClass,
8872
+ children: rowActions(row)
8873
+ })
8874
+ ]
8875
+ }),
8876
+ isExpanded && expandable && jsx("tr", {
8877
+ className: expandedRowClass,
8878
+ children: jsx("td", {
8879
+ colSpan: totalColumnCount,
8880
+ className: "p-3",
8881
+ children: expandable.render(row)
8882
+ })
8883
+ })
8884
+ ]
8885
+ });
8886
+ }
8887
+ // ───── Data cell ─────
8888
+ function DataCell(props) {
8889
+ const { col, row, rowIndex, isSelected, isExpanded, inferredType, cellClass } = props;
8890
+ const value = getNestedValue(row, col.field);
8891
+ const align = resolveAlign(col, inferredType);
8892
+ // tabularNums = digit alignment via font-variant-numeric — does NOT
8893
+ // change the font family. Auto-true for number / date columns.
8894
+ const tabularNums = resolveTabularNums(col, inferredType);
8895
+ // monospace = font-family override (font-mono). Library never
8896
+ // auto-picks a font family — only explicit consumer opt-in. The
8897
+ // consumer's theme font-sans is preserved by default.
8898
+ const monospace = resolveMonospace(col);
8899
+ const alignClass = align === 'right' ? 'text-right' : align === 'center' ? 'text-center' : 'text-left';
8900
+ // Per-column RBAC state for the cellRenderer context
8901
+ const columnAccessState = col.access ? {
8902
+ hidden: col.access.onUnauthorized === 'hide',
8903
+ disabled: col.access.onUnauthorized === 'disable',
8904
+ readonly: col.access.onUnauthorized === 'readonly'
8905
+ } : null;
8906
+ const content = col.cellRenderer ? col.cellRenderer({
8907
+ row,
8908
+ value,
8909
+ rowIndex,
8910
+ isSelected,
8911
+ isExpanded,
8912
+ access: columnAccessState
8913
+ }) : renderDefaultCell(value);
8914
+ return jsx("td", {
8915
+ className: cn(cellClass, alignClass, tabularNums && 'tabular-nums', monospace && 'font-mono'),
8916
+ style: {
8917
+ width: col.width != null ? `${col.width}px` : undefined,
8918
+ minWidth: col.minWidth != null ? `${col.minWidth}px` : undefined
8919
+ },
8920
+ children: content
8921
+ });
8922
+ }
8923
+ // ───── Helpers ─────
8924
+ function renderDefaultCell(value) {
8925
+ if (value == null) return null;
8926
+ if (typeof value === 'boolean') return value ? '✓' : '✗';
8927
+ if (value instanceof Date) return value.toISOString().slice(0, 10);
8928
+ if (typeof value === 'object') return JSON.stringify(value);
8929
+ return value;
8930
+ }
8931
+ function handleSortClick(col, event, current, setSortModel) {
8932
+ if (!col.sortable) return;
8933
+ const multi = event.shiftKey;
8934
+ setSortModel((prev)=>cycleSortFor(prev, col.field, multi));
8935
+ }
8936
+ function renderLoadingRows(args) {
8937
+ const { count, visibleCols, showSelectionColumn, showExpandColumn, showRowActionsColumn, rowClass, cellClass } = args;
8938
+ return Array.from({
8939
+ length: count
8940
+ }).map((_, i)=>jsxs("tr", {
8941
+ className: rowClass,
8942
+ "aria-busy": "true",
8943
+ children: [
8944
+ showSelectionColumn && jsx("td", {
8945
+ className: cellClass,
8946
+ children: jsx(Skeleton, {
8947
+ variant: "rectangle",
8948
+ width: "16px",
8949
+ height: "16px"
8950
+ })
8951
+ }),
8952
+ showExpandColumn && jsx("td", {
8953
+ className: cellClass,
8954
+ children: jsx(Skeleton, {
8955
+ variant: "rectangle",
8956
+ width: "16px",
8957
+ height: "16px"
8958
+ })
8959
+ }),
8960
+ visibleCols.map((col)=>jsx("td", {
8961
+ className: cellClass,
8962
+ children: jsx(Skeleton, {
8963
+ variant: "text",
8964
+ width: "80%"
8965
+ })
8966
+ }, col.field)),
8967
+ showRowActionsColumn && jsx("td", {
8968
+ className: cellClass,
8969
+ children: jsx(Skeleton, {
8970
+ variant: "rectangle",
8971
+ width: "24px",
8972
+ height: "24px"
8973
+ })
8974
+ })
8975
+ ]
8976
+ }, `loading-${i}`));
8977
+ }
8978
+ function renderEmptyState(args) {
8979
+ var _args_emptyState;
8980
+ return jsx("tr", {
8981
+ children: jsx("td", {
8982
+ colSpan: args.totalColumnCount,
8983
+ className: args.cellClass,
8984
+ children: (_args_emptyState = args.emptyState) != null ? _args_emptyState : args.fallback
8985
+ })
8986
+ });
8987
+ }
8988
+ // ───── Icons ─────
8989
+ function SortIcon(props) {
8990
+ return jsx("svg", {
8991
+ width: "0.75em",
8992
+ height: "0.75em",
8993
+ viewBox: "0 0 16 16",
8994
+ fill: "currentColor",
8995
+ "aria-hidden": "true",
8996
+ className: "shrink-0 opacity-60 group-hover:opacity-100",
8997
+ children: props.direction === 'asc' ? jsx("path", {
8998
+ d: "M8 4l4 6H4z"
8999
+ }) : props.direction === 'desc' ? jsx("path", {
9000
+ d: "M8 12L4 6h8z"
9001
+ }) : jsxs(Fragment$1, {
9002
+ children: [
9003
+ jsx("path", {
9004
+ d: "M8 3l3 4H5z",
9005
+ opacity: "0.5"
9006
+ }),
9007
+ jsx("path", {
9008
+ d: "M8 13l-3-4h6z",
9009
+ opacity: "0.5"
9010
+ })
9011
+ ]
9012
+ })
9013
+ });
9014
+ }
9015
+ function ChevronRightIcon() {
9016
+ return jsx("svg", {
9017
+ width: "0.875em",
9018
+ height: "0.875em",
9019
+ viewBox: "0 0 16 16",
9020
+ fill: "none",
9021
+ stroke: "currentColor",
9022
+ strokeWidth: "1.5",
9023
+ strokeLinecap: "round",
9024
+ strokeLinejoin: "round",
9025
+ "aria-hidden": "true",
9026
+ children: jsx("path", {
9027
+ d: "M6 4l4 4-4 4"
9028
+ })
9029
+ });
9030
+ }
9031
+ function SearchIcon(props) {
9032
+ return jsxs("svg", {
9033
+ width: "0.875em",
9034
+ height: "0.875em",
9035
+ viewBox: "0 0 16 16",
9036
+ fill: "none",
9037
+ stroke: "currentColor",
9038
+ strokeWidth: "1.5",
9039
+ strokeLinecap: "round",
9040
+ strokeLinejoin: "round",
9041
+ "aria-hidden": "true",
9042
+ className: props.className,
9043
+ children: [
9044
+ jsx("circle", {
9045
+ cx: "7",
9046
+ cy: "7",
9047
+ r: "4.5"
9048
+ }),
9049
+ jsx("path", {
9050
+ d: "M11 11l3 3"
9051
+ })
9052
+ ]
9053
+ });
9054
+ }
9055
+
9056
+ /**
9057
+ * Plain text cell renderer. Wraps the value in a `<span>` and
9058
+ * optionally truncates on overflow. Use this for the most common
9059
+ * case: render a single string value.
9060
+ *
9061
+ * The Table component does NOT use this by default for plain
9062
+ * string columns — it just renders `{value}` directly. Use
9063
+ * `RenderText` when you need the truncate / muted styling.
9064
+ */ function RenderText(props) {
9065
+ const { value, truncate, muted, className } = props;
9066
+ return jsx("span", {
9067
+ className: cn(truncate && 'block max-w-full truncate', // Neutral palette auto-inverts via dashforgePreset() CSS-var swap.
9068
+ muted && 'text-neutral-500', className),
9069
+ children: value
9070
+ });
9071
+ }
9072
+
9073
+ /**
9074
+ * Two-line cell renderer: a bold primary line stacked above a muted
9075
+ * secondary line. Useful for `Name + email`, `Title + ID`, etc.
9076
+ *
9077
+ * Replaces the MUI-side `RenderTwoLineCell` without the magic-number
9078
+ * vertical-align fudges (`mt: 0.8 / 0.2`) — flexbox handles the
9079
+ * vertical layout cleanly.
9080
+ *
9081
+ * Dashforge identity rule: text colors auto-invert via the
9082
+ * `dashforgePreset()` CSS-variable swap. No `dark:` variants needed
9083
+ * on neutral palette.
9084
+ */ function RenderTwoLine(props) {
9085
+ const { primary, secondary, className } = props;
9086
+ return jsxs("div", {
9087
+ className: cn('flex flex-col leading-tight', className),
9088
+ children: [
9089
+ jsx("span", {
9090
+ className: "font-medium text-neutral-900",
9091
+ children: primary
9092
+ }),
9093
+ jsx("span", {
9094
+ className: "text-xs text-neutral-500 mt-0.5",
9095
+ children: secondary
9096
+ })
9097
+ ]
9098
+ });
9099
+ }
9100
+
9101
+ /**
9102
+ * Internal mini-chip used by the cell renderer library. Not exported
9103
+ * from `@dashforge/tw` as a standalone component (per Sprint 4.1
9104
+ * scope decision — a standalone `<Chip>` may ship in Sprint 5 if
9105
+ * demand emerges).
9106
+ *
9107
+ * Variants:
9108
+ * - `color`: 7 intents (neutral / primary / success / warning /
9109
+ * danger / info / secondary)
9110
+ * - `variant`: `soft` (default) / `solid` / `outline`
9111
+ * - `size`: `sm` / `md`
9112
+ */ const chipVariants = tv({
9113
+ base: [
9114
+ 'inline-flex items-center gap-1 rounded-full font-medium',
9115
+ 'whitespace-nowrap'
9116
+ ],
9117
+ variants: {
9118
+ color: {
9119
+ neutral: '',
9120
+ primary: '',
9121
+ success: '',
9122
+ warning: '',
9123
+ danger: '',
9124
+ info: '',
9125
+ secondary: ''
9126
+ },
9127
+ variant: {
9128
+ soft: '',
9129
+ solid: '',
9130
+ outline: ''
9131
+ },
9132
+ size: {
9133
+ sm: 'h-5 px-2 text-xs',
9134
+ md: 'h-6 px-2.5 text-xs'
9135
+ }
9136
+ },
9137
+ compoundVariants: [
9138
+ // soft = subtle bg + dark text.
9139
+ // Neutral row uses NO `dark:` variants — the dashforgePreset()
9140
+ // CSS-var swap auto-inverts the neutral palette already.
9141
+ // Color rows below KEEP `dark:` variants because color palettes
9142
+ // don't auto-invert: the `dark:` shift is an intentional design
9143
+ // choice (darker bg + lighter text in dark mode for visual cohesion).
9144
+ {
9145
+ variant: 'soft',
9146
+ color: 'neutral',
9147
+ class: 'bg-neutral-100 text-neutral-700'
9148
+ },
9149
+ {
9150
+ variant: 'soft',
9151
+ color: 'primary',
9152
+ class: 'bg-primary-100 dark:bg-primary-950 text-primary-700 dark:text-primary-300'
9153
+ },
9154
+ {
9155
+ variant: 'soft',
9156
+ color: 'success',
9157
+ class: 'bg-success-100 dark:bg-success-950 text-success-700 dark:text-success-300'
9158
+ },
9159
+ {
9160
+ variant: 'soft',
9161
+ color: 'warning',
9162
+ class: 'bg-warning-100 dark:bg-warning-950 text-warning-700 dark:text-warning-300'
9163
+ },
9164
+ {
9165
+ variant: 'soft',
9166
+ color: 'danger',
9167
+ class: 'bg-danger-100 dark:bg-danger-950 text-danger-700 dark:text-danger-300'
9168
+ },
9169
+ {
9170
+ variant: 'soft',
9171
+ color: 'info',
9172
+ class: 'bg-info-100 dark:bg-info-950 text-info-700 dark:text-info-300'
9173
+ },
9174
+ {
9175
+ variant: 'soft',
9176
+ color: 'secondary',
9177
+ class: 'bg-secondary-100 dark:bg-secondary-950 text-secondary-700 dark:text-secondary-300'
9178
+ },
9179
+ // solid = full bg + light text
9180
+ // solid neutral: bg + text both anchored to neutral palette
9181
+ // (auto-inverts). `text-white` would not invert and would break
9182
+ // contrast in light mode against a `bg-neutral-700` dark bg, so
9183
+ // we stick to `text-neutral-50` (light text in light mode, dark
9184
+ // in dark — wait, that's wrong too). Actually `text-neutral-50`
9185
+ // auto-inverts: light text in light mode (against dark bg) is
9186
+ // what we want; in dark mode `bg-neutral-700` becomes light AND
9187
+ // `text-neutral-50` becomes dark — contrast preserved.
9188
+ {
9189
+ variant: 'solid',
9190
+ color: 'neutral',
9191
+ class: 'bg-neutral-700 text-neutral-50'
9192
+ },
9193
+ {
9194
+ variant: 'solid',
9195
+ color: 'primary',
9196
+ class: 'bg-primary-500 text-white'
9197
+ },
9198
+ {
9199
+ variant: 'solid',
9200
+ color: 'success',
9201
+ class: 'bg-success-500 text-white'
9202
+ },
9203
+ {
9204
+ variant: 'solid',
9205
+ color: 'warning',
9206
+ class: 'bg-warning-500 text-white'
9207
+ },
9208
+ {
9209
+ variant: 'solid',
9210
+ color: 'danger',
9211
+ class: 'bg-danger-500 text-white'
9212
+ },
9213
+ {
9214
+ variant: 'solid',
9215
+ color: 'info',
9216
+ class: 'bg-info-500 text-white'
9217
+ },
9218
+ {
9219
+ variant: 'solid',
9220
+ color: 'secondary',
9221
+ class: 'bg-secondary-500 text-white'
9222
+ },
9223
+ // outline = border + colored text
9224
+ // outline neutral: both border + text auto-invert via CSS-var swap.
9225
+ {
9226
+ variant: 'outline',
9227
+ color: 'neutral',
9228
+ class: 'border border-neutral-300 text-neutral-700'
9229
+ },
9230
+ {
9231
+ variant: 'outline',
9232
+ color: 'primary',
9233
+ class: 'border border-primary-500 text-primary-700 dark:text-primary-300'
9234
+ },
9235
+ {
9236
+ variant: 'outline',
9237
+ color: 'success',
9238
+ class: 'border border-success-500 text-success-700 dark:text-success-300'
9239
+ },
9240
+ {
9241
+ variant: 'outline',
9242
+ color: 'warning',
9243
+ class: 'border border-warning-500 text-warning-700 dark:text-warning-300'
9244
+ },
9245
+ {
9246
+ variant: 'outline',
9247
+ color: 'danger',
9248
+ class: 'border border-danger-500 text-danger-700 dark:text-danger-300'
9249
+ },
9250
+ {
9251
+ variant: 'outline',
9252
+ color: 'info',
9253
+ class: 'border border-info-500 text-info-700 dark:text-info-300'
9254
+ },
9255
+ {
9256
+ variant: 'outline',
9257
+ color: 'secondary',
9258
+ class: 'border border-secondary-500 text-secondary-700 dark:text-secondary-300'
9259
+ }
9260
+ ],
9261
+ defaultVariants: {
9262
+ color: 'neutral',
9263
+ variant: 'soft',
9264
+ size: 'sm'
9265
+ }
9266
+ });
9267
+ function RenderChip(props) {
9268
+ const { children, color, variant, size, className } = props;
9269
+ return jsx("span", {
9270
+ className: cn(chipVariants({
9271
+ color,
9272
+ variant,
9273
+ size
9274
+ }), className),
9275
+ children: children
9276
+ });
9277
+ }
9278
+
9279
+ /**
9280
+ * Inline button cell renderer. Thin wrapper over `<Button>` with
9281
+ * the size narrowed to `sm` by default (table density expects compact
9282
+ * controls).
9283
+ */ function RenderButton(props) {
9284
+ const { label, onClick, variant = 'ghost', size = 'sm' } = props, rest = _object_without_properties_loose(props, [
9285
+ "label",
9286
+ "onClick",
9287
+ "variant",
9288
+ "size"
9289
+ ]);
9290
+ return jsx(Button, _extends({
9291
+ variant: variant,
9292
+ size: size,
9293
+ onClick: onClick
9294
+ }, rest, {
9295
+ children: label
9296
+ }));
9297
+ }
9298
+
9299
+ /**
9300
+ * 3-dot row actions menu. Renders a trigger button that opens a
9301
+ * Popover (Sprint 3 component) with a list of action menu items.
9302
+ *
9303
+ * Replaces the MUI-side `RowActions.tsx` (IconButton + Menu) with
9304
+ * the TW equivalents. Per-action RBAC honored: hidden actions are
9305
+ * filtered out, disabled actions render disabled.
9306
+ */ function RowActionsMenu(props) {
9307
+ const { row, actions, trigger, ariaLabel = 'Row actions' } = props;
9308
+ const [open, setOpen] = useState(false);
9309
+ // Filter actions by RBAC (hidden actions removed entirely)
9310
+ const visibleActions = actions.filter((action)=>{
9311
+ if (!action.access) return true;
9312
+ return action.access.onUnauthorized !== 'hide';
9313
+ });
9314
+ if (visibleActions.length === 0) return null;
9315
+ return jsx(Popover, {
9316
+ open: open,
9317
+ onOpenChange: setOpen,
9318
+ side: "bottom",
9319
+ align: "end",
9320
+ content: jsx("ul", {
9321
+ className: "flex flex-col gap-0.5 min-w-[140px]",
9322
+ role: "menu",
9323
+ children: visibleActions.map((action, idx)=>jsx(RowActionMenuItem, {
9324
+ action: action,
9325
+ row: row,
9326
+ onClose: ()=>setOpen(false)
9327
+ }, idx))
9328
+ }),
9329
+ children: trigger != null ? trigger : jsx("button", {
9330
+ type: "button",
9331
+ "aria-label": ariaLabel,
9332
+ "aria-haspopup": "menu",
9333
+ "aria-expanded": open,
9334
+ className: cn('inline-flex items-center justify-center h-8 w-8 rounded-md', // Neutral palette auto-inverts via the dashforgePreset()
9335
+ // CSS-var swap — no `dark:` variant needed.
9336
+ 'text-neutral-500 hover:text-neutral-900', 'hover:bg-neutral-100', 'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500', 'transition-colors motion-reduce:transition-none'),
9337
+ children: jsxs("svg", {
9338
+ width: "1em",
9339
+ height: "1em",
9340
+ viewBox: "0 0 16 16",
9341
+ fill: "currentColor",
9342
+ "aria-hidden": "true",
9343
+ children: [
9344
+ jsx("circle", {
9345
+ cx: "8",
9346
+ cy: "3",
9347
+ r: "1.4"
9348
+ }),
9349
+ jsx("circle", {
9350
+ cx: "8",
9351
+ cy: "8",
9352
+ r: "1.4"
9353
+ }),
9354
+ jsx("circle", {
9355
+ cx: "8",
9356
+ cy: "13",
9357
+ r: "1.4"
9358
+ })
9359
+ ]
9360
+ })
9361
+ })
9362
+ });
9363
+ }
9364
+ function RowActionMenuItem(props) {
9365
+ const { action, row, onClose } = props;
9366
+ const accessState = useAccessState(action.access);
9367
+ const isDisabled = action.disabled || accessState.disabled || accessState.readonly;
9368
+ return jsx("li", {
9369
+ role: "none",
9370
+ children: jsxs("button", {
9371
+ type: "button",
9372
+ role: "menuitem",
9373
+ disabled: isDisabled,
9374
+ onClick: ()=>{
9375
+ onClose();
9376
+ action.onClick(row);
9377
+ },
9378
+ className: cn('flex items-center gap-2 w-full text-left', 'px-3 py-1.5 rounded-md text-sm', // Neutral palette auto-inverts; danger/warning are color
9379
+ // palettes (no auto-invert) so they keep their `dark:` shift
9380
+ // intentionally — design choice, not a bug.
9381
+ 'hover:bg-neutral-100', 'focus:outline-none focus-visible:bg-neutral-100', 'transition-colors motion-reduce:transition-none', 'disabled:opacity-50 disabled:pointer-events-none', action.color === 'danger' && 'text-danger-700 dark:text-danger-300', action.color === 'warning' && 'text-warning-700 dark:text-warning-300'),
9382
+ children: [
9383
+ action.icon && jsx("span", {
9384
+ className: "inline-flex shrink-0",
9385
+ "aria-hidden": "true",
9386
+ children: action.icon
9387
+ }),
9388
+ jsx("span", {
9389
+ children: action.label
9390
+ })
9391
+ ]
9392
+ })
9393
+ });
9394
+ }
9395
+
7847
9396
  /**
7848
9397
  * @dashforge/tw
7849
9398
  *
@@ -7871,6 +9420,6 @@ const DEFAULT_PAGE_SIZE_OPTIONS = [
7871
9420
  */ // ───── Components ─────
7872
9421
  /**
7873
9422
  * Package version (synced with `package.json` at publish time).
7874
- */ const VERSION = '0.5.0-beta';
9423
+ */ const VERSION = '0.6.0-beta';
7875
9424
 
7876
- export { Accordion, AppShell, AspectRatio, Autocomplete, Box, Breadcrumbs, Button, Checkbox, ConfirmDialogProvider, Container, DateTimePicker, Dialog, Divider, Grid, LeftNav, NumberField, OTPField, Pagination, Popover, RadioGroup, Skeleton, SnackbarProvider, Stack, Switch, Tabs, TextField, Textarea, Tooltip, TopBar, Typography, VERSION, VisuallyHidden, accordionVariants, appShellVariants, autocompleteVariants, boxVariants, breadcrumbsVariants, buttonVariants, checkboxVariants, cn, confirmDialogVariants, containerVariants, dateTimePickerVariants, dialogVariants, dividerLineVariants, dividerVariants, gridVariants, isoToInputValue, leftNavVariants, numberFieldVariants, otpFieldVariants, paginationVariants, popoverVariants, radioGroupVariants, skeletonVariants, snackbarVariants, stackVariants, switchVariants, tabsVariants, textFieldVariants, textareaVariants, tooltipVariants, topBarVariants, typographyVariants, useAccessState, useConfirm, useSnackbar };
9425
+ export { Accordion, AppShell, AspectRatio, Autocomplete, Box, Breadcrumbs, Button, Checkbox, ConfirmDialogProvider, Container, DateTimePicker, Dialog, Divider, Grid, LeftNav, NumberField, OTPField, Pagination, Popover, RadioGroup, RenderButton, RenderChip, RenderText, RenderTwoLine, RowActionsMenu, Skeleton, SnackbarProvider, Stack, Switch, Table, Tabs, TextField, Textarea, Tooltip, TopBar, Typography, VERSION, VisuallyHidden, accordionVariants, appShellVariants, autocompleteVariants, boxVariants, breadcrumbsVariants, buttonVariants, checkboxVariants, cn, confirmDialogVariants, containerVariants, dateTimePickerVariants, dialogVariants, dividerLineVariants, dividerVariants, getNestedValue, gridVariants, isoToInputValue, leftNavVariants, numberFieldVariants, otpFieldVariants, paginationVariants, popoverVariants, radioGroupVariants, skeletonVariants, snackbarVariants, stackVariants, switchVariants, tableVariants, tabsVariants, textFieldVariants, textareaVariants, tooltipVariants, topBarVariants, typographyVariants, useAccessState, useConfirm, useSnackbar };