@dashforge/tw 0.5.0-beta → 0.7.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 (105) hide show
  1. package/CHANGELOG.md +526 -0
  2. package/THEME-AUDIT.md +175 -0
  3. package/dist/index.esm.js +2504 -51
  4. package/dist/src/components/Accordion/accordion.variants.d.ts.map +1 -1
  5. package/dist/src/components/Box/box.variants.d.ts.map +1 -1
  6. package/dist/src/components/DataGrid/DataGrid.d.ts +29 -0
  7. package/dist/src/components/DataGrid/DataGrid.d.ts.map +1 -0
  8. package/dist/src/components/DataGrid/dataGrid.types.d.ts +171 -0
  9. package/dist/src/components/DataGrid/dataGrid.types.d.ts.map +1 -0
  10. package/dist/src/components/DataGrid/dataGrid.variants.d.ts +216 -0
  11. package/dist/src/components/DataGrid/dataGrid.variants.d.ts.map +1 -0
  12. package/dist/src/components/Dialog/dialog.variants.d.ts.map +1 -1
  13. package/dist/src/components/Divider/divider.variants.d.ts.map +1 -1
  14. package/dist/src/components/Pagination/Pagination.d.ts.map +1 -1
  15. package/dist/src/components/Pagination/pagination.variants.d.ts.map +1 -1
  16. package/dist/src/components/Popover/popover.variants.d.ts.map +1 -1
  17. package/dist/src/components/Skeleton/skeleton.variants.d.ts.map +1 -1
  18. package/dist/src/components/Table/Table.d.ts +24 -0
  19. package/dist/src/components/Table/Table.d.ts.map +1 -0
  20. package/dist/src/components/Table/cells/RenderButton.d.ts +13 -0
  21. package/dist/src/components/Table/cells/RenderButton.d.ts.map +1 -0
  22. package/dist/src/components/Table/cells/RenderChip.d.ts +80 -0
  23. package/dist/src/components/Table/cells/RenderChip.d.ts.map +1 -0
  24. package/dist/src/components/Table/cells/RenderText.d.ts +21 -0
  25. package/dist/src/components/Table/cells/RenderText.d.ts.map +1 -0
  26. package/dist/src/components/Table/cells/RenderTwoLine.d.ts +20 -0
  27. package/dist/src/components/Table/cells/RenderTwoLine.d.ts.map +1 -0
  28. package/dist/src/components/Table/cells/RowActionsMenu.d.ts +36 -0
  29. package/dist/src/components/Table/cells/RowActionsMenu.d.ts.map +1 -0
  30. package/dist/src/components/Table/cells/index.d.ts +11 -0
  31. package/dist/src/components/Table/cells/index.d.ts.map +1 -0
  32. package/dist/src/components/Table/table.types.d.ts +312 -0
  33. package/dist/src/components/Table/table.types.d.ts.map +1 -0
  34. package/dist/src/components/Table/table.variants.d.ts +285 -0
  35. package/dist/src/components/Table/table.variants.d.ts.map +1 -0
  36. package/dist/src/components/Tabs/tabs.variants.d.ts.map +1 -1
  37. package/dist/src/components/Tooltip/tooltip.variants.d.ts.map +1 -1
  38. package/dist/src/components/Typography/typography.variants.d.ts.map +1 -1
  39. package/dist/src/components/_shared/data/getNestedValue.d.ts +20 -0
  40. package/dist/src/components/_shared/data/getNestedValue.d.ts.map +1 -0
  41. package/dist/src/components/_shared/data/useColumnAutoDetect.d.ts +45 -0
  42. package/dist/src/components/_shared/data/useColumnAutoDetect.d.ts.map +1 -0
  43. package/dist/src/components/_shared/data/useControllableState.d.ts +23 -0
  44. package/dist/src/components/_shared/data/useControllableState.d.ts.map +1 -0
  45. package/dist/src/components/_shared/data/useDebouncedValue.d.ts +10 -0
  46. package/dist/src/components/_shared/data/useDebouncedValue.d.ts.map +1 -0
  47. package/dist/src/components/_shared/data/useTableFilter.d.ts +13 -0
  48. package/dist/src/components/_shared/data/useTableFilter.d.ts.map +1 -0
  49. package/dist/src/components/_shared/data/useTableSearch.d.ts +23 -0
  50. package/dist/src/components/_shared/data/useTableSearch.d.ts.map +1 -0
  51. package/dist/src/components/_shared/data/useTableSelection.d.ts +25 -0
  52. package/dist/src/components/_shared/data/useTableSelection.d.ts.map +1 -0
  53. package/dist/src/components/_shared/data/useTableSort.d.ts +20 -0
  54. package/dist/src/components/_shared/data/useTableSort.d.ts.map +1 -0
  55. package/dist/src/components/_shared/data/useVirtualizer.d.ts +107 -0
  56. package/dist/src/components/_shared/data/useVirtualizer.d.ts.map +1 -0
  57. package/dist/src/index.d.ts +10 -1
  58. package/dist/src/index.d.ts.map +1 -1
  59. package/package.json +3 -3
  60. package/src/components/Accordion/accordion.variants.ts +5 -3
  61. package/src/components/Box/Box.test.tsx +32 -11
  62. package/src/components/Box/box.variants.ts +23 -4
  63. package/src/components/DataGrid/DataGrid.test.tsx +496 -0
  64. package/src/components/DataGrid/DataGrid.tsx +831 -0
  65. package/src/components/DataGrid/dataGrid.types.ts +189 -0
  66. package/src/components/DataGrid/dataGrid.variants.ts +144 -0
  67. package/src/components/Dialog/dialog.variants.ts +10 -6
  68. package/src/components/Divider/Divider.test.tsx +3 -1
  69. package/src/components/Divider/Divider.tsx +1 -1
  70. package/src/components/Divider/divider.variants.ts +2 -1
  71. package/src/components/Pagination/Pagination.tsx +3 -1
  72. package/src/components/Pagination/pagination.variants.ts +19 -13
  73. package/src/components/Popover/popover.variants.ts +10 -4
  74. package/src/components/Skeleton/skeleton.variants.ts +2 -1
  75. package/src/components/Table/Table.test.tsx +603 -0
  76. package/src/components/Table/Table.tsx +816 -0
  77. package/src/components/Table/cells/RenderButton.tsx +23 -0
  78. package/src/components/Table/cells/RenderChip.tsx +103 -0
  79. package/src/components/Table/cells/RenderText.tsx +37 -0
  80. package/src/components/Table/cells/RenderTwoLine.tsx +34 -0
  81. package/src/components/Table/cells/RowActionsMenu.tsx +147 -0
  82. package/src/components/Table/cells/index.ts +14 -0
  83. package/src/components/Table/table.types.ts +346 -0
  84. package/src/components/Table/table.variants.ts +207 -0
  85. package/src/components/Tabs/tabs.variants.ts +19 -8
  86. package/src/components/Tooltip/tooltip.variants.ts +12 -2
  87. package/src/components/Typography/Typography.test.tsx +3 -1
  88. package/src/components/Typography/typography.variants.ts +6 -2
  89. package/src/components/_shared/data/getNestedValue.test.ts +50 -0
  90. package/src/components/_shared/data/getNestedValue.ts +32 -0
  91. package/src/components/_shared/data/useColumnAutoDetect.test.ts +123 -0
  92. package/src/components/_shared/data/useColumnAutoDetect.ts +100 -0
  93. package/src/components/_shared/data/useControllableState.ts +48 -0
  94. package/src/components/_shared/data/useDebouncedValue.ts +24 -0
  95. package/src/components/_shared/data/useTableFilter.ts +37 -0
  96. package/src/components/_shared/data/useTableSearch.test.ts +115 -0
  97. package/src/components/_shared/data/useTableSearch.ts +66 -0
  98. package/src/components/_shared/data/useTableSelection.test.ts +100 -0
  99. package/src/components/_shared/data/useTableSelection.ts +100 -0
  100. package/src/components/_shared/data/useTableSort.test.ts +150 -0
  101. package/src/components/_shared/data/useTableSort.ts +141 -0
  102. package/src/components/_shared/data/useVirtualizer.test.ts +158 -0
  103. package/src/components/_shared/data/useVirtualizer.ts +199 -0
  104. package/src/components/_shared/themeIdentity.test.ts +113 -0
  105. package/src/index.ts +59 -1
@@ -0,0 +1,23 @@
1
+ import type { MouseEvent, ReactNode } from 'react';
2
+ import { Button } from '../../Button/Button.js';
3
+ import type { ButtonProps } from '../../Button/button.types.js';
4
+
5
+ export interface RenderButtonProps
6
+ extends Pick<ButtonProps, 'variant' | 'color' | 'size' | 'disabled' | 'access'> {
7
+ label: ReactNode;
8
+ onClick?: (event: MouseEvent<HTMLButtonElement>) => void;
9
+ }
10
+
11
+ /**
12
+ * Inline button cell renderer. Thin wrapper over `<Button>` with
13
+ * the size narrowed to `sm` by default (table density expects compact
14
+ * controls).
15
+ */
16
+ export function RenderButton(props: RenderButtonProps) {
17
+ const { label, onClick, variant = 'ghost', size = 'sm', ...rest } = props;
18
+ return (
19
+ <Button variant={variant} size={size} onClick={onClick} {...rest}>
20
+ {label}
21
+ </Button>
22
+ );
23
+ }
@@ -0,0 +1,103 @@
1
+ import type { ReactNode } from 'react';
2
+ import { tv, type VariantProps } from 'tailwind-variants';
3
+ import { cn } from '../../../utils/cn.js';
4
+
5
+ /**
6
+ * Internal mini-chip used by the cell renderer library. Not exported
7
+ * from `@dashforge/tw` as a standalone component (per Sprint 4.1
8
+ * scope decision — a standalone `<Chip>` may ship in Sprint 5 if
9
+ * demand emerges).
10
+ *
11
+ * Variants:
12
+ * - `color`: 7 intents (neutral / primary / success / warning /
13
+ * danger / info / secondary)
14
+ * - `variant`: `soft` (default) / `solid` / `outline`
15
+ * - `size`: `sm` / `md`
16
+ */
17
+ const chipVariants = tv({
18
+ base: [
19
+ 'inline-flex items-center gap-1 rounded-full font-medium',
20
+ 'whitespace-nowrap',
21
+ ],
22
+ variants: {
23
+ color: {
24
+ neutral: '',
25
+ primary: '',
26
+ success: '',
27
+ warning: '',
28
+ danger: '',
29
+ info: '',
30
+ secondary: '',
31
+ },
32
+ variant: {
33
+ soft: '',
34
+ solid: '',
35
+ outline: '',
36
+ },
37
+ size: {
38
+ sm: 'h-5 px-2 text-xs',
39
+ md: 'h-6 px-2.5 text-xs',
40
+ },
41
+ },
42
+ compoundVariants: [
43
+ // soft = subtle bg + dark text.
44
+ // Neutral row uses NO `dark:` variants — the dashforgePreset()
45
+ // CSS-var swap auto-inverts the neutral palette already.
46
+ // Color rows below KEEP `dark:` variants because color palettes
47
+ // don't auto-invert: the `dark:` shift is an intentional design
48
+ // choice (darker bg + lighter text in dark mode for visual cohesion).
49
+ { variant: 'soft', color: 'neutral', class: 'bg-neutral-100 text-neutral-700' },
50
+ { variant: 'soft', color: 'primary', class: 'bg-primary-100 dark:bg-primary-950 text-primary-700 dark:text-primary-300' },
51
+ { variant: 'soft', color: 'success', class: 'bg-success-100 dark:bg-success-950 text-success-700 dark:text-success-300' },
52
+ { variant: 'soft', color: 'warning', class: 'bg-warning-100 dark:bg-warning-950 text-warning-700 dark:text-warning-300' },
53
+ { variant: 'soft', color: 'danger', class: 'bg-danger-100 dark:bg-danger-950 text-danger-700 dark:text-danger-300' },
54
+ { variant: 'soft', color: 'info', class: 'bg-info-100 dark:bg-info-950 text-info-700 dark:text-info-300' },
55
+ { variant: 'soft', color: 'secondary', class: 'bg-secondary-100 dark:bg-secondary-950 text-secondary-700 dark:text-secondary-300' },
56
+ // solid = full bg + light text
57
+ // solid neutral: bg + text both anchored to neutral palette
58
+ // (auto-inverts). `text-white` would not invert and would break
59
+ // contrast in light mode against a `bg-neutral-700` dark bg, so
60
+ // we stick to `text-neutral-50` (light text in light mode, dark
61
+ // in dark — wait, that's wrong too). Actually `text-neutral-50`
62
+ // auto-inverts: light text in light mode (against dark bg) is
63
+ // what we want; in dark mode `bg-neutral-700` becomes light AND
64
+ // `text-neutral-50` becomes dark — contrast preserved.
65
+ { variant: 'solid', color: 'neutral', class: 'bg-neutral-700 text-neutral-50' },
66
+ { variant: 'solid', color: 'primary', class: 'bg-primary-500 text-white' },
67
+ { variant: 'solid', color: 'success', class: 'bg-success-500 text-white' },
68
+ { variant: 'solid', color: 'warning', class: 'bg-warning-500 text-white' },
69
+ { variant: 'solid', color: 'danger', class: 'bg-danger-500 text-white' },
70
+ { variant: 'solid', color: 'info', class: 'bg-info-500 text-white' },
71
+ { variant: 'solid', color: 'secondary', class: 'bg-secondary-500 text-white' },
72
+ // outline = border + colored text
73
+ // outline neutral: both border + text auto-invert via CSS-var swap.
74
+ { variant: 'outline', color: 'neutral', class: 'border border-neutral-300 text-neutral-700' },
75
+ { variant: 'outline', color: 'primary', class: 'border border-primary-500 text-primary-700 dark:text-primary-300' },
76
+ { variant: 'outline', color: 'success', class: 'border border-success-500 text-success-700 dark:text-success-300' },
77
+ { variant: 'outline', color: 'warning', class: 'border border-warning-500 text-warning-700 dark:text-warning-300' },
78
+ { variant: 'outline', color: 'danger', class: 'border border-danger-500 text-danger-700 dark:text-danger-300' },
79
+ { variant: 'outline', color: 'info', class: 'border border-info-500 text-info-700 dark:text-info-300' },
80
+ { variant: 'outline', color: 'secondary', class: 'border border-secondary-500 text-secondary-700 dark:text-secondary-300' },
81
+ ],
82
+ defaultVariants: {
83
+ color: 'neutral',
84
+ variant: 'soft',
85
+ size: 'sm',
86
+ },
87
+ });
88
+
89
+ type ChipVariants = VariantProps<typeof chipVariants>;
90
+
91
+ export interface RenderChipProps extends ChipVariants {
92
+ children: ReactNode;
93
+ className?: string;
94
+ }
95
+
96
+ export function RenderChip(props: RenderChipProps) {
97
+ const { children, color, variant, size, className } = props;
98
+ return (
99
+ <span className={cn(chipVariants({ color, variant, size }), className)}>
100
+ {children}
101
+ </span>
102
+ );
103
+ }
@@ -0,0 +1,37 @@
1
+ import type { ReactNode } from 'react';
2
+ import { cn } from '../../../utils/cn.js';
3
+
4
+ export interface RenderTextProps {
5
+ value: ReactNode;
6
+ /** Truncate to one line with ellipsis on overflow. @default false */
7
+ truncate?: boolean;
8
+ /** Secondary text color (less prominent). @default false */
9
+ muted?: boolean;
10
+ /** Extra class merged via `tailwind-merge`. */
11
+ className?: string;
12
+ }
13
+
14
+ /**
15
+ * Plain text cell renderer. Wraps the value in a `<span>` and
16
+ * optionally truncates on overflow. Use this for the most common
17
+ * case: render a single string value.
18
+ *
19
+ * The Table component does NOT use this by default for plain
20
+ * string columns — it just renders `{value}` directly. Use
21
+ * `RenderText` when you need the truncate / muted styling.
22
+ */
23
+ export function RenderText(props: RenderTextProps) {
24
+ const { value, truncate, muted, className } = props;
25
+ return (
26
+ <span
27
+ className={cn(
28
+ truncate && 'block max-w-full truncate',
29
+ // Neutral palette auto-inverts via dashforgePreset() CSS-var swap.
30
+ muted && 'text-neutral-500',
31
+ className,
32
+ )}
33
+ >
34
+ {value}
35
+ </span>
36
+ );
37
+ }
@@ -0,0 +1,34 @@
1
+ import type { ReactNode } from 'react';
2
+ import { cn } from '../../../utils/cn.js';
3
+
4
+ export interface RenderTwoLineProps {
5
+ primary: ReactNode;
6
+ secondary: ReactNode;
7
+ className?: string;
8
+ }
9
+
10
+ /**
11
+ * Two-line cell renderer: a bold primary line stacked above a muted
12
+ * secondary line. Useful for `Name + email`, `Title + ID`, etc.
13
+ *
14
+ * Replaces the MUI-side `RenderTwoLineCell` without the magic-number
15
+ * vertical-align fudges (`mt: 0.8 / 0.2`) — flexbox handles the
16
+ * vertical layout cleanly.
17
+ *
18
+ * Dashforge identity rule: text colors auto-invert via the
19
+ * `dashforgePreset()` CSS-variable swap. No `dark:` variants needed
20
+ * on neutral palette.
21
+ */
22
+ export function RenderTwoLine(props: RenderTwoLineProps) {
23
+ const { primary, secondary, className } = props;
24
+ return (
25
+ <div className={cn('flex flex-col leading-tight', className)}>
26
+ <span className="font-medium text-neutral-900">
27
+ {primary}
28
+ </span>
29
+ <span className="text-xs text-neutral-500 mt-0.5">
30
+ {secondary}
31
+ </span>
32
+ </div>
33
+ );
34
+ }
@@ -0,0 +1,147 @@
1
+ import { useState, type ReactNode } from 'react';
2
+ import type { AccessRequirement } from '@dashforge/rbac';
3
+ import { Popover } from '../../Popover/Popover.js';
4
+ import { useAccessState } from '../../../hooks/useAccessState.js';
5
+ import { cn } from '../../../utils/cn.js';
6
+
7
+ /**
8
+ * Single action definition for `<RowActionsMenu>`.
9
+ *
10
+ * `access` (optional) gates the action via RBAC:
11
+ * - `onUnauthorized: 'hide'` → action not rendered
12
+ * - `onUnauthorized: 'disable'` → action visible, disabled
13
+ * - `onUnauthorized: 'readonly'` → action visible, disabled
14
+ */
15
+ export interface TableRowAction<T> {
16
+ label: ReactNode;
17
+ onClick: (row: T) => void;
18
+ icon?: ReactNode;
19
+ color?: 'default' | 'danger' | 'warning';
20
+ disabled?: boolean;
21
+ access?: AccessRequirement;
22
+ }
23
+
24
+ export interface RowActionsMenuProps<T> {
25
+ row: T;
26
+ actions: TableRowAction<T>[];
27
+ /** Custom trigger element. Defaults to a ⋮ icon button. */
28
+ trigger?: ReactNode;
29
+ /** Aria-label for the default trigger. */
30
+ ariaLabel?: string;
31
+ }
32
+
33
+ /**
34
+ * 3-dot row actions menu. Renders a trigger button that opens a
35
+ * Popover (Sprint 3 component) with a list of action menu items.
36
+ *
37
+ * Replaces the MUI-side `RowActions.tsx` (IconButton + Menu) with
38
+ * the TW equivalents. Per-action RBAC honored: hidden actions are
39
+ * filtered out, disabled actions render disabled.
40
+ */
41
+ export function RowActionsMenu<T>(props: RowActionsMenuProps<T>) {
42
+ const { row, actions, trigger, ariaLabel = 'Row actions' } = props;
43
+ const [open, setOpen] = useState(false);
44
+
45
+ // Filter actions by RBAC (hidden actions removed entirely)
46
+ const visibleActions = actions.filter((action) => {
47
+ if (!action.access) return true;
48
+ return action.access.onUnauthorized !== 'hide';
49
+ });
50
+
51
+ if (visibleActions.length === 0) return null;
52
+
53
+ return (
54
+ <Popover
55
+ open={open}
56
+ onOpenChange={setOpen}
57
+ side="bottom"
58
+ align="end"
59
+ content={
60
+ <ul className="flex flex-col gap-0.5 min-w-[140px]" role="menu">
61
+ {visibleActions.map((action, idx) => (
62
+ <RowActionMenuItem
63
+ key={idx}
64
+ action={action}
65
+ row={row}
66
+ onClose={() => setOpen(false)}
67
+ />
68
+ ))}
69
+ </ul>
70
+ }
71
+ >
72
+ {trigger ?? (
73
+ <button
74
+ type="button"
75
+ aria-label={ariaLabel}
76
+ aria-haspopup="menu"
77
+ aria-expanded={open}
78
+ className={cn(
79
+ 'inline-flex items-center justify-center h-8 w-8 rounded-md',
80
+ // Neutral palette auto-inverts via the dashforgePreset()
81
+ // CSS-var swap — no `dark:` variant needed.
82
+ 'text-neutral-500 hover:text-neutral-900',
83
+ 'hover:bg-neutral-100',
84
+ 'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500',
85
+ 'transition-colors motion-reduce:transition-none',
86
+ )}
87
+ >
88
+ <svg
89
+ width="1em"
90
+ height="1em"
91
+ viewBox="0 0 16 16"
92
+ fill="currentColor"
93
+ aria-hidden="true"
94
+ >
95
+ <circle cx="8" cy="3" r="1.4" />
96
+ <circle cx="8" cy="8" r="1.4" />
97
+ <circle cx="8" cy="13" r="1.4" />
98
+ </svg>
99
+ </button>
100
+ )}
101
+ </Popover>
102
+ );
103
+ }
104
+
105
+ function RowActionMenuItem<T>(props: {
106
+ action: TableRowAction<T>;
107
+ row: T;
108
+ onClose: () => void;
109
+ }) {
110
+ const { action, row, onClose } = props;
111
+ const accessState = useAccessState(action.access);
112
+ const isDisabled = action.disabled || accessState.disabled || accessState.readonly;
113
+
114
+ return (
115
+ <li role="none">
116
+ <button
117
+ type="button"
118
+ role="menuitem"
119
+ disabled={isDisabled}
120
+ onClick={() => {
121
+ onClose();
122
+ action.onClick(row);
123
+ }}
124
+ className={cn(
125
+ 'flex items-center gap-2 w-full text-left',
126
+ 'px-3 py-1.5 rounded-md text-sm',
127
+ // Neutral palette auto-inverts; danger/warning are color
128
+ // palettes (no auto-invert) so they keep their `dark:` shift
129
+ // intentionally — design choice, not a bug.
130
+ 'hover:bg-neutral-100',
131
+ 'focus:outline-none focus-visible:bg-neutral-100',
132
+ 'transition-colors motion-reduce:transition-none',
133
+ 'disabled:opacity-50 disabled:pointer-events-none',
134
+ action.color === 'danger' && 'text-danger-700 dark:text-danger-300',
135
+ action.color === 'warning' && 'text-warning-700 dark:text-warning-300',
136
+ )}
137
+ >
138
+ {action.icon && (
139
+ <span className="inline-flex shrink-0" aria-hidden="true">
140
+ {action.icon}
141
+ </span>
142
+ )}
143
+ <span>{action.label}</span>
144
+ </button>
145
+ </li>
146
+ );
147
+ }
@@ -0,0 +1,14 @@
1
+ export { RenderText } from './RenderText.js';
2
+ export type { RenderTextProps } from './RenderText.js';
3
+
4
+ export { RenderTwoLine } from './RenderTwoLine.js';
5
+ export type { RenderTwoLineProps } from './RenderTwoLine.js';
6
+
7
+ export { RenderChip } from './RenderChip.js';
8
+ export type { RenderChipProps } from './RenderChip.js';
9
+
10
+ export { RenderButton } from './RenderButton.js';
11
+ export type { RenderButtonProps } from './RenderButton.js';
12
+
13
+ export { RowActionsMenu } from './RowActionsMenu.js';
14
+ export type { RowActionsMenuProps, TableRowAction } from './RowActionsMenu.js';