@dashforge/tw 0.2.1-beta → 0.4.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 (86) hide show
  1. package/CHANGELOG.md +257 -0
  2. package/CONSUMER-VALIDATION.md +130 -0
  3. package/PARITY.md +483 -0
  4. package/PERFORMANCE.md +169 -0
  5. package/dist/index.esm.js +801 -42
  6. package/dist/src/components/Accordion/Accordion.d.ts +21 -0
  7. package/dist/src/components/Accordion/Accordion.d.ts.map +1 -0
  8. package/dist/src/components/Accordion/accordion.types.d.ts +74 -0
  9. package/dist/src/components/Accordion/accordion.types.d.ts.map +1 -0
  10. package/dist/src/components/Accordion/accordion.variants.d.ts +58 -0
  11. package/dist/src/components/Accordion/accordion.variants.d.ts.map +1 -0
  12. package/dist/src/components/AppShell/AppShell.d.ts +14 -0
  13. package/dist/src/components/AppShell/AppShell.d.ts.map +1 -1
  14. package/dist/src/components/AppShell/appShell.variants.d.ts.map +1 -1
  15. package/dist/src/components/Autocomplete/Autocomplete.d.ts.map +1 -1
  16. package/dist/src/components/Autocomplete/autocomplete.variants.d.ts.map +1 -1
  17. package/dist/src/components/Breadcrumbs/breadcrumbs.variants.d.ts.map +1 -1
  18. package/dist/src/components/Checkbox/Checkbox.d.ts.map +1 -1
  19. package/dist/src/components/Dialog/Dialog.d.ts +17 -0
  20. package/dist/src/components/Dialog/Dialog.d.ts.map +1 -0
  21. package/dist/src/components/Dialog/dialog.types.d.ts +75 -0
  22. package/dist/src/components/Dialog/dialog.types.d.ts.map +1 -0
  23. package/dist/src/components/Dialog/dialog.variants.d.ts +80 -0
  24. package/dist/src/components/Dialog/dialog.variants.d.ts.map +1 -0
  25. package/dist/src/components/LeftNav/leftNav.variants.d.ts.map +1 -1
  26. package/dist/src/components/Popover/Popover.d.ts +15 -0
  27. package/dist/src/components/Popover/Popover.d.ts.map +1 -0
  28. package/dist/src/components/Popover/popover.types.d.ts +45 -0
  29. package/dist/src/components/Popover/popover.types.d.ts.map +1 -0
  30. package/dist/src/components/Popover/popover.variants.d.ts +34 -0
  31. package/dist/src/components/Popover/popover.variants.d.ts.map +1 -0
  32. package/dist/src/components/Snackbar/snackbar.variants.d.ts.map +1 -1
  33. package/dist/src/components/Switch/switch.variants.d.ts.map +1 -1
  34. package/dist/src/components/Tabs/Tabs.d.ts +15 -0
  35. package/dist/src/components/Tabs/Tabs.d.ts.map +1 -0
  36. package/dist/src/components/Tabs/tabs.types.d.ts +55 -0
  37. package/dist/src/components/Tabs/tabs.types.d.ts.map +1 -0
  38. package/dist/src/components/Tabs/tabs.variants.d.ts +82 -0
  39. package/dist/src/components/Tabs/tabs.variants.d.ts.map +1 -0
  40. package/dist/src/components/TextField/TextField.d.ts.map +1 -1
  41. package/dist/src/components/TextField/textField.types.d.ts +29 -3
  42. package/dist/src/components/TextField/textField.types.d.ts.map +1 -1
  43. package/dist/src/components/TextField/textField.variants.d.ts +6 -0
  44. package/dist/src/components/TextField/textField.variants.d.ts.map +1 -1
  45. package/dist/src/components/Tooltip/Tooltip.d.ts +29 -0
  46. package/dist/src/components/Tooltip/Tooltip.d.ts.map +1 -0
  47. package/dist/src/components/Tooltip/tooltip.types.d.ts +47 -0
  48. package/dist/src/components/Tooltip/tooltip.types.d.ts.map +1 -0
  49. package/dist/src/components/Tooltip/tooltip.variants.d.ts +34 -0
  50. package/dist/src/components/Tooltip/tooltip.variants.d.ts.map +1 -0
  51. package/dist/src/index.d.ts +16 -1
  52. package/dist/src/index.d.ts.map +1 -1
  53. package/package.json +6 -1
  54. package/src/components/Accordion/Accordion.test.tsx +95 -0
  55. package/src/components/Accordion/Accordion.tsx +97 -0
  56. package/src/components/Accordion/accordion.types.ts +66 -0
  57. package/src/components/Accordion/accordion.variants.ts +30 -0
  58. package/src/components/AppShell/AppShell.tsx +126 -1
  59. package/src/components/AppShell/appShell.variants.ts +8 -2
  60. package/src/components/Autocomplete/Autocomplete.tsx +77 -4
  61. package/src/components/Autocomplete/autocomplete.variants.ts +6 -0
  62. package/src/components/Breadcrumbs/breadcrumbs.variants.ts +5 -0
  63. package/src/components/Checkbox/Checkbox.tsx +43 -9
  64. package/src/components/Dialog/Dialog.test.tsx +96 -0
  65. package/src/components/Dialog/Dialog.tsx +93 -0
  66. package/src/components/Dialog/dialog.types.ts +62 -0
  67. package/src/components/Dialog/dialog.variants.ts +62 -0
  68. package/src/components/LeftNav/leftNav.variants.ts +12 -1
  69. package/src/components/Popover/Popover.test.tsx +72 -0
  70. package/src/components/Popover/Popover.tsx +55 -0
  71. package/src/components/Popover/popover.types.ts +42 -0
  72. package/src/components/Popover/popover.variants.ts +18 -0
  73. package/src/components/Snackbar/snackbar.variants.ts +4 -2
  74. package/src/components/Switch/switch.variants.ts +6 -1
  75. package/src/components/Tabs/Tabs.test.tsx +74 -0
  76. package/src/components/Tabs/Tabs.tsx +66 -0
  77. package/src/components/Tabs/tabs.types.ts +49 -0
  78. package/src/components/Tabs/tabs.variants.ts +58 -0
  79. package/src/components/TextField/TextField.tsx +29 -0
  80. package/src/components/TextField/textField.types.ts +23 -3
  81. package/src/components/TextField/textField.variants.ts +18 -0
  82. package/src/components/Tooltip/Tooltip.test.tsx +73 -0
  83. package/src/components/Tooltip/Tooltip.tsx +70 -0
  84. package/src/components/Tooltip/tooltip.types.ts +44 -0
  85. package/src/components/Tooltip/tooltip.variants.ts +18 -0
  86. package/src/index.ts +40 -1
@@ -6,9 +6,27 @@ import type { TextFieldVariants } from './textField.variants.js';
6
6
  /**
7
7
  * Per-slot overrides for `<TextField>`.
8
8
  *
9
- * Each slot accepts `{ className: string }` so the override path is
10
- * extensible (style / aria-* / data-* can be added without a breaking
11
- * change). Mirrors the MUI-side `slotProps` shape.
9
+ * Each slot accepts `{ className: string }` (and, for `prefix` /
10
+ * `suffix`, a `children` node) so the override path is extensible —
11
+ * extra fields like `style`, `aria-*`, `data-*` can be added without
12
+ * a breaking change. Mirrors the MUI-side `slotProps` shape.
13
+ *
14
+ * **`prefix` / `suffix`** are inline adornments rendered INSIDE the
15
+ * `inputWrapper`, before / after the `<input>` element. Typical use:
16
+ * currency symbols, units, status icons. Passing only `className`
17
+ * (no `children`) is allowed but renders an empty slot — usually
18
+ * you'll always pair the two.
19
+ *
20
+ * ```tsx
21
+ * <TextField
22
+ * name="price"
23
+ * type="number"
24
+ * slotProps={{
25
+ * prefix: { children: '$' },
26
+ * suffix: { children: 'USD' },
27
+ * }}
28
+ * />
29
+ * ```
12
30
  */
13
31
  export interface TextFieldSlotProps {
14
32
  root?: { className?: string };
@@ -18,6 +36,8 @@ export interface TextFieldSlotProps {
18
36
  input?: { className?: string };
19
37
  helperText?: { className?: string };
20
38
  errorText?: { className?: string };
39
+ prefix?: { children?: ReactNode; className?: string };
40
+ suffix?: { children?: ReactNode; className?: string };
21
41
  }
22
42
 
23
43
  /**
@@ -34,6 +34,24 @@ export const textFieldVariants = tv({
34
34
  ],
35
35
  helperText: 'mt-1 text-sm text-neutral-600',
36
36
  errorText: 'mt-1 text-sm text-danger-600',
37
+ /*
38
+ * `prefix` / `suffix` slot — inline adornment rendered before /
39
+ * after the input INSIDE the inputWrapper. `shrink-0` keeps the
40
+ * adornment width fixed so it doesn't compete for space with the
41
+ * input; `select-none` + `pointer-events-none` avoids
42
+ * accidentally stealing focus from the input on click (the
43
+ * inputWrapper handles focus via `focus-within`).
44
+ */
45
+ prefix: [
46
+ 'shrink-0 inline-flex items-center select-none pointer-events-none',
47
+ 'text-neutral-500',
48
+ 'mr-2',
49
+ ],
50
+ suffix: [
51
+ 'shrink-0 inline-flex items-center select-none pointer-events-none',
52
+ 'text-neutral-500',
53
+ 'ml-2',
54
+ ],
37
55
  },
38
56
  variants: {
39
57
  size: {
@@ -0,0 +1,73 @@
1
+ // @vitest-environment jsdom
2
+ import * as React from 'react';
3
+ import { describe, it, expect, afterEach, beforeAll } from 'vitest';
4
+ import { render, screen, cleanup } from '@testing-library/react';
5
+ import { Tooltip } from './Tooltip.js';
6
+
7
+ void React;
8
+ afterEach(() => cleanup());
9
+
10
+ // jsdom doesn't implement ResizeObserver — Radix Floating UI requires it.
11
+ // Shim with a noop before any test mounts a Tooltip.
12
+ beforeAll(() => {
13
+ if (typeof globalThis.ResizeObserver === 'undefined') {
14
+ (globalThis as unknown as { ResizeObserver: unknown }).ResizeObserver = class {
15
+ observe() {}
16
+ unobserve() {}
17
+ disconnect() {}
18
+ };
19
+ }
20
+ if (typeof globalThis.DOMRect === 'undefined') {
21
+ (globalThis as unknown as { DOMRect: unknown }).DOMRect = class {
22
+ static fromRect(r?: DOMRectInit) {
23
+ return new (globalThis as unknown as { DOMRect: new (x: number, y: number, w: number, h: number) => unknown }).DOMRect(
24
+ r?.x ?? 0, r?.y ?? 0, r?.width ?? 0, r?.height ?? 0
25
+ );
26
+ }
27
+ constructor(public x = 0, public y = 0, public width = 0, public height = 0) {}
28
+ };
29
+ }
30
+ });
31
+
32
+ describe('Tooltip — rendering', () => {
33
+ it('renders the trigger (children) always', () => {
34
+ render(
35
+ <Tooltip content="Tip text">
36
+ <button>Trigger</button>
37
+ </Tooltip>
38
+ );
39
+ expect(screen.queryByRole('button', { name: 'Trigger' })).not.toBeNull();
40
+ });
41
+
42
+ it('renders content when controlled open=true', () => {
43
+ render(
44
+ <Tooltip content="Tip text" open onOpenChange={() => {}}>
45
+ <button>Trigger</button>
46
+ </Tooltip>
47
+ );
48
+ expect(screen.queryAllByText('Tip text').length).toBeGreaterThan(0);
49
+ });
50
+
51
+ it('does not render content when controlled open=false', () => {
52
+ render(
53
+ <Tooltip content="Tip text" open={false} onOpenChange={() => {}}>
54
+ <button>Trigger</button>
55
+ </Tooltip>
56
+ );
57
+ expect(screen.queryByText('Tip text')).toBeNull();
58
+ });
59
+ });
60
+
61
+ describe('Tooltip — placement props', () => {
62
+ it.each(['top', 'right', 'bottom', 'left'] as const)(
63
+ 'renders side=%s without crashing',
64
+ (side) => {
65
+ render(
66
+ <Tooltip content="Tip" open side={side} onOpenChange={() => {}}>
67
+ <button>Trigger</button>
68
+ </Tooltip>
69
+ );
70
+ expect(screen.queryByRole('button', { name: 'Trigger' })).not.toBeNull();
71
+ }
72
+ );
73
+ });
@@ -0,0 +1,70 @@
1
+ import * as RadixTooltip from '@radix-ui/react-tooltip';
2
+ import { cn } from '../../utils/cn.js';
3
+ import { tooltipVariants } from './tooltip.variants.js';
4
+ import type { TooltipProps } from './tooltip.types.js';
5
+
6
+ /**
7
+ * Dashforge TW `<Tooltip>` — hoverable / focusable tooltip.
8
+ *
9
+ * Built on `@radix-ui/react-tooltip`:
10
+ * - APG tooltip pattern: `role="tooltip"`, `aria-describedby` wired
11
+ * to the trigger automatically (Radix manages the id linkage).
12
+ * - Shows on hover OR keyboard focus.
13
+ * - Dismissed on Escape or pointer leave.
14
+ *
15
+ * Wrap any single React node — it becomes the trigger via Radix's
16
+ * `asChild` slot. The trigger inherits `aria-describedby` so screen
17
+ * readers announce the tooltip content along with the trigger.
18
+ *
19
+ * @example
20
+ * ```tsx
21
+ * <Tooltip content="Delete this item">
22
+ * <Button variant="ghost">
23
+ * <TrashIcon />
24
+ * </Button>
25
+ * </Tooltip>
26
+ * ```
27
+ *
28
+ * Each consumer can also wrap the entire app once in
29
+ * `<RadixTooltip.Provider>` to share delay/skip configuration; we
30
+ * include a per-component provider here for ease-of-use.
31
+ */
32
+ export function Tooltip(props: TooltipProps) {
33
+ const {
34
+ children,
35
+ content,
36
+ side = 'top',
37
+ align = 'center',
38
+ hideArrow = false,
39
+ delayDuration = 200,
40
+ open,
41
+ onOpenChange,
42
+ sideOffset = 4,
43
+ slotProps,
44
+ } = props;
45
+
46
+ const v = tooltipVariants();
47
+
48
+ return (
49
+ <RadixTooltip.Provider delayDuration={delayDuration}>
50
+ <RadixTooltip.Root open={open} onOpenChange={onOpenChange}>
51
+ <RadixTooltip.Trigger asChild>{children}</RadixTooltip.Trigger>
52
+ <RadixTooltip.Portal>
53
+ <RadixTooltip.Content
54
+ side={side}
55
+ align={align}
56
+ sideOffset={sideOffset}
57
+ className={cn(v.content(), slotProps?.content?.className)}
58
+ >
59
+ {content}
60
+ {!hideArrow && (
61
+ <RadixTooltip.Arrow
62
+ className={cn(v.arrow(), slotProps?.arrow?.className)}
63
+ />
64
+ )}
65
+ </RadixTooltip.Content>
66
+ </RadixTooltip.Portal>
67
+ </RadixTooltip.Root>
68
+ </RadixTooltip.Provider>
69
+ );
70
+ }
@@ -0,0 +1,44 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ export interface TooltipSlotProps {
4
+ content?: { className?: string };
5
+ arrow?: { className?: string };
6
+ }
7
+
8
+ /**
9
+ * Props for `<Tooltip>`.
10
+ *
11
+ * Built on `@radix-ui/react-tooltip`. Wrap any element with a tooltip
12
+ * that follows APG semantics (`role="tooltip"`, `aria-describedby`
13
+ * wired to the trigger).
14
+ *
15
+ * Renders an arrow by default. Three side options
16
+ * (top / right / bottom / left) with 'top' as default. Delay
17
+ * configurable via `delayDuration` (forwarded to Radix Provider).
18
+ */
19
+ export interface TooltipProps {
20
+ /** The element that, when hovered/focused, shows the tooltip. */
21
+ children: ReactNode;
22
+ /** Tooltip body content. */
23
+ content: ReactNode;
24
+ /** Side of the trigger to show the tooltip on. @default 'top' */
25
+ side?: 'top' | 'right' | 'bottom' | 'left';
26
+ /** Alignment along the perpendicular axis. @default 'center' */
27
+ align?: 'start' | 'center' | 'end';
28
+ /** Hide the arrow indicator. @default false (arrow shown) */
29
+ hideArrow?: boolean;
30
+ /** Delay before the tooltip shows on hover (ms). @default 200 */
31
+ delayDuration?: number;
32
+ /**
33
+ * Controlled open state. Use when the tooltip should be programmatic
34
+ * (e.g. revealed on a manual button press). Omit for default
35
+ * hover / focus behavior.
36
+ */
37
+ open?: boolean;
38
+ /** Called when the tooltip's open state changes. */
39
+ onOpenChange?: (open: boolean) => void;
40
+ /** Side offset in pixels from the trigger. @default 4 */
41
+ sideOffset?: number;
42
+ /** Per-slot overrides. */
43
+ slotProps?: TooltipSlotProps;
44
+ }
@@ -0,0 +1,18 @@
1
+ import { tv, type VariantProps } from 'tailwind-variants';
2
+
3
+ export const tooltipVariants = tv({
4
+ slots: {
5
+ content: [
6
+ 'z-50 max-w-xs rounded-md px-3 py-1.5 text-xs font-medium',
7
+ 'bg-neutral-900 text-neutral-50',
8
+ 'dark:bg-neutral-50 dark:text-neutral-900',
9
+ 'shadow-md',
10
+ 'data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95',
11
+ 'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
12
+ 'motion-reduce:transition-none motion-reduce:duration-0',
13
+ ],
14
+ arrow: 'fill-neutral-900 dark:fill-neutral-50',
15
+ },
16
+ });
17
+
18
+ export type TooltipVariants = VariantProps<typeof tooltipVariants>;
package/src/index.ts CHANGED
@@ -223,6 +223,45 @@ export type { AspectRatioProps } from './components/AspectRatio/aspectRatio.type
223
223
  export { VisuallyHidden } from './components/VisuallyHidden/VisuallyHidden.js';
224
224
  export type { VisuallyHiddenProps } from './components/VisuallyHidden/visuallyHidden.types.js';
225
225
 
226
+ // F11 tier-4: overlay + disclosure primitives.
227
+
228
+ export { Dialog } from './components/Dialog/Dialog.js';
229
+ export type {
230
+ DialogProps,
231
+ DialogSlotProps,
232
+ } from './components/Dialog/dialog.types.js';
233
+ export { dialogVariants } from './components/Dialog/dialog.variants.js';
234
+
235
+ export { Tabs } from './components/Tabs/Tabs.js';
236
+ export type {
237
+ TabsProps,
238
+ TabsSlotProps,
239
+ TabItem,
240
+ } from './components/Tabs/tabs.types.js';
241
+ export { tabsVariants } from './components/Tabs/tabs.variants.js';
242
+
243
+ export { Tooltip } from './components/Tooltip/Tooltip.js';
244
+ export type {
245
+ TooltipProps,
246
+ TooltipSlotProps,
247
+ } from './components/Tooltip/tooltip.types.js';
248
+ export { tooltipVariants } from './components/Tooltip/tooltip.variants.js';
249
+
250
+ export { Popover } from './components/Popover/Popover.js';
251
+ export type {
252
+ PopoverProps,
253
+ PopoverSlotProps,
254
+ } from './components/Popover/popover.types.js';
255
+ export { popoverVariants } from './components/Popover/popover.variants.js';
256
+
257
+ export { Accordion } from './components/Accordion/Accordion.js';
258
+ export type {
259
+ AccordionProps,
260
+ AccordionSlotProps,
261
+ AccordionItem,
262
+ } from './components/Accordion/accordion.types.js';
263
+ export { accordionVariants } from './components/Accordion/accordion.variants.js';
264
+
226
265
  // ───── Hooks ─────
227
266
  export { useAccessState } from './hooks/useAccessState.js';
228
267
 
@@ -237,4 +276,4 @@ export type { VariantProps } from 'tailwind-variants';
237
276
  /**
238
277
  * Package version (synced with `package.json` at publish time).
239
278
  */
240
- export const VERSION = '0.2.1-beta';
279
+ export const VERSION = '0.4.0-beta';