@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.
- package/CHANGELOG.md +268 -0
- package/dist/index.esm.js +1553 -4
- package/dist/src/components/Table/Table.d.ts +24 -0
- package/dist/src/components/Table/Table.d.ts.map +1 -0
- package/dist/src/components/Table/_internal/getNestedValue.d.ts +20 -0
- package/dist/src/components/Table/_internal/getNestedValue.d.ts.map +1 -0
- package/dist/src/components/Table/_internal/useColumnAutoDetect.d.ts +45 -0
- package/dist/src/components/Table/_internal/useColumnAutoDetect.d.ts.map +1 -0
- package/dist/src/components/Table/_internal/useControllableState.d.ts +23 -0
- package/dist/src/components/Table/_internal/useControllableState.d.ts.map +1 -0
- package/dist/src/components/Table/_internal/useDebouncedValue.d.ts +10 -0
- package/dist/src/components/Table/_internal/useDebouncedValue.d.ts.map +1 -0
- package/dist/src/components/Table/_internal/useTableFilter.d.ts +13 -0
- package/dist/src/components/Table/_internal/useTableFilter.d.ts.map +1 -0
- package/dist/src/components/Table/_internal/useTableSearch.d.ts +23 -0
- package/dist/src/components/Table/_internal/useTableSearch.d.ts.map +1 -0
- package/dist/src/components/Table/_internal/useTableSelection.d.ts +25 -0
- package/dist/src/components/Table/_internal/useTableSelection.d.ts.map +1 -0
- package/dist/src/components/Table/_internal/useTableSort.d.ts +20 -0
- package/dist/src/components/Table/_internal/useTableSort.d.ts.map +1 -0
- package/dist/src/components/Table/cells/RenderButton.d.ts +13 -0
- package/dist/src/components/Table/cells/RenderButton.d.ts.map +1 -0
- package/dist/src/components/Table/cells/RenderChip.d.ts +80 -0
- package/dist/src/components/Table/cells/RenderChip.d.ts.map +1 -0
- package/dist/src/components/Table/cells/RenderText.d.ts +21 -0
- package/dist/src/components/Table/cells/RenderText.d.ts.map +1 -0
- package/dist/src/components/Table/cells/RenderTwoLine.d.ts +20 -0
- package/dist/src/components/Table/cells/RenderTwoLine.d.ts.map +1 -0
- package/dist/src/components/Table/cells/RowActionsMenu.d.ts +36 -0
- package/dist/src/components/Table/cells/RowActionsMenu.d.ts.map +1 -0
- package/dist/src/components/Table/cells/index.d.ts +11 -0
- package/dist/src/components/Table/cells/index.d.ts.map +1 -0
- package/dist/src/components/Table/table.types.d.ts +304 -0
- package/dist/src/components/Table/table.types.d.ts.map +1 -0
- package/dist/src/components/Table/table.variants.d.ts +285 -0
- package/dist/src/components/Table/table.variants.d.ts.map +1 -0
- package/dist/src/index.d.ts +7 -1
- package/dist/src/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/Table/Table.test.tsx +603 -0
- package/src/components/Table/Table.tsx +816 -0
- package/src/components/Table/_internal/getNestedValue.test.ts +50 -0
- package/src/components/Table/_internal/getNestedValue.ts +32 -0
- package/src/components/Table/_internal/themeIdentity.test.ts +84 -0
- package/src/components/Table/_internal/useColumnAutoDetect.test.ts +123 -0
- package/src/components/Table/_internal/useColumnAutoDetect.ts +100 -0
- package/src/components/Table/_internal/useControllableState.ts +48 -0
- package/src/components/Table/_internal/useDebouncedValue.ts +24 -0
- package/src/components/Table/_internal/useTableFilter.ts +37 -0
- package/src/components/Table/_internal/useTableSearch.test.ts +115 -0
- package/src/components/Table/_internal/useTableSearch.ts +66 -0
- package/src/components/Table/_internal/useTableSelection.test.ts +100 -0
- package/src/components/Table/_internal/useTableSelection.ts +100 -0
- package/src/components/Table/_internal/useTableSort.test.ts +150 -0
- package/src/components/Table/_internal/useTableSort.ts +141 -0
- package/src/components/Table/cells/RenderButton.tsx +23 -0
- package/src/components/Table/cells/RenderChip.tsx +103 -0
- package/src/components/Table/cells/RenderText.tsx +37 -0
- package/src/components/Table/cells/RenderTwoLine.tsx +34 -0
- package/src/components/Table/cells/RowActionsMenu.tsx +147 -0
- package/src/components/Table/cells/index.ts +14 -0
- package/src/components/Table/table.types.ts +337 -0
- package/src/components/Table/table.variants.ts +207 -0
- 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.
|
|
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 };
|