@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
package/dist/index.esm.js CHANGED
@@ -11,6 +11,11 @@ import { useDashFieldMeta } from '@dashforge/forms';
11
11
  import * as RadixCheckbox from '@radix-ui/react-checkbox';
12
12
  import * as RadixSwitch from '@radix-ui/react-switch';
13
13
  import * as RadixRadioGroup from '@radix-ui/react-radio-group';
14
+ import * as RadixDialog from '@radix-ui/react-dialog';
15
+ import * as RadixTabs from '@radix-ui/react-tabs';
16
+ import * as RadixTooltip from '@radix-ui/react-tooltip';
17
+ import * as RadixPopover from '@radix-ui/react-popover';
18
+ import * as RadixAccordion from '@radix-ui/react-accordion';
14
19
 
15
20
  function _extends() {
16
21
  _extends = Object.assign || function assign(target) {
@@ -510,7 +515,24 @@ function _object_without_properties_loose(source, excluded) {
510
515
  'read-only:cursor-default'
511
516
  ],
512
517
  helperText: 'mt-1 text-sm text-neutral-600',
513
- errorText: 'mt-1 text-sm text-danger-600'
518
+ errorText: 'mt-1 text-sm text-danger-600',
519
+ /*
520
+ * `prefix` / `suffix` slot — inline adornment rendered before /
521
+ * after the input INSIDE the inputWrapper. `shrink-0` keeps the
522
+ * adornment width fixed so it doesn't compete for space with the
523
+ * input; `select-none` + `pointer-events-none` avoids
524
+ * accidentally stealing focus from the input on click (the
525
+ * inputWrapper handles focus via `focus-within`).
526
+ */ prefix: [
527
+ 'shrink-0 inline-flex items-center select-none pointer-events-none',
528
+ 'text-neutral-500',
529
+ 'mr-2'
530
+ ],
531
+ suffix: [
532
+ 'shrink-0 inline-flex items-center select-none pointer-events-none',
533
+ 'text-neutral-500',
534
+ 'ml-2'
535
+ ]
514
536
  },
515
537
  variants: {
516
538
  size: {
@@ -587,7 +609,7 @@ function _object_without_properties_loose(source, excluded) {
587
609
  * - `aria-describedby` links the input to the helper/error text node.
588
610
  * - Required fields get the native `required` attribute + a visual `*`.
589
611
  */ function TextField(props) {
590
- var _slotProps_root, _slotProps_label, _slotProps_requiredMark, _slotProps_inputWrapper, _slotProps_input, _slotProps_errorText, _slotProps_helperText;
612
+ var _slotProps_root, _slotProps_label, _slotProps_requiredMark, _slotProps_inputWrapper, _slotProps_prefix, _slotProps_input, _slotProps_suffix, _slotProps_errorText, _slotProps_helperText;
591
613
  const { name, rules, visibleWhen, layout = 'stacked', size, label, helperText, required, error, disabled, fullWidth, access, sx, slotProps, placeholder, type, onChange: userOnChange, onBlur: userOnBlur, value: userValue, defaultValue } = props, rest = _object_without_properties_loose(props, [
592
614
  "name",
593
615
  "rules",
@@ -717,25 +739,37 @@ function _object_without_properties_loose(source, excluded) {
717
739
  })
718
740
  ]
719
741
  }),
720
- jsx("div", {
742
+ jsxs("div", {
721
743
  className: cn(v.inputWrapper(), slotProps == null ? void 0 : (_slotProps_inputWrapper = slotProps.inputWrapper) == null ? void 0 : _slotProps_inputWrapper.className),
722
- children: jsx("input", _extends({}, rest, {
723
- id: inputId,
724
- name: name,
725
- type: type != null ? type : 'text',
726
- placeholder: placeholder,
727
- value: isFormMode ? resolvedValue != null ? resolvedValue : '' : userValue,
728
- defaultValue: !isFormMode && userValue === undefined ? defaultValue : undefined,
729
- disabled: effectiveDisabled,
730
- readOnly: effectiveReadOnly,
731
- required: required,
732
- "aria-invalid": resolvedError || undefined,
733
- "aria-describedby": resolvedHelperText ? helperId : undefined,
734
- onChange: handleChange,
735
- onBlur: handleBlur,
736
- ref: inputRef,
737
- className: cn(v.input(), slotProps == null ? void 0 : (_slotProps_input = slotProps.input) == null ? void 0 : _slotProps_input.className)
738
- }))
744
+ children: [
745
+ (slotProps == null ? void 0 : (_slotProps_prefix = slotProps.prefix) == null ? void 0 : _slotProps_prefix.children) !== undefined && jsx("span", {
746
+ "aria-hidden": "true",
747
+ className: cn(v.prefix(), slotProps.prefix.className),
748
+ children: slotProps.prefix.children
749
+ }),
750
+ jsx("input", _extends({}, rest, {
751
+ id: inputId,
752
+ name: name,
753
+ type: type != null ? type : 'text',
754
+ placeholder: placeholder,
755
+ value: isFormMode ? resolvedValue != null ? resolvedValue : '' : userValue,
756
+ defaultValue: !isFormMode && userValue === undefined ? defaultValue : undefined,
757
+ disabled: effectiveDisabled,
758
+ readOnly: effectiveReadOnly,
759
+ required: required,
760
+ "aria-invalid": resolvedError || undefined,
761
+ "aria-describedby": resolvedHelperText ? helperId : undefined,
762
+ onChange: handleChange,
763
+ onBlur: handleBlur,
764
+ ref: inputRef,
765
+ className: cn(v.input(), slotProps == null ? void 0 : (_slotProps_input = slotProps.input) == null ? void 0 : _slotProps_input.className)
766
+ })),
767
+ (slotProps == null ? void 0 : (_slotProps_suffix = slotProps.suffix) == null ? void 0 : _slotProps_suffix.children) !== undefined && jsx("span", {
768
+ "aria-hidden": "true",
769
+ className: cn(v.suffix(), slotProps.suffix.className),
770
+ children: slotProps.suffix.children
771
+ })
772
+ ]
739
773
  }),
740
774
  resolvedHelperText && jsx("p", {
741
775
  id: helperId,
@@ -811,8 +845,8 @@ function _object_without_properties_loose(source, excluded) {
811
845
  });
812
846
 
813
847
  /**
814
- * Inline checkmark SVG used by `Radix.Indicator`. Stroke uses
815
- * `currentColor` so the parent's `text-white` propagates correctly.
848
+ * Inline checkmark SVG used by `Radix.Indicator` when fully checked.
849
+ * Stroke uses `currentColor` so the parent's `text-white` propagates.
816
850
  *
817
851
  * @internal
818
852
  */ function CheckIcon({ className }) {
@@ -830,6 +864,32 @@ function _object_without_properties_loose(source, excluded) {
830
864
  })
831
865
  });
832
866
  }
867
+ /**
868
+ * Inline dash SVG used by `Radix.Indicator` when the checkbox is in
869
+ * the `indeterminate` tri-state ("some, but not all, children
870
+ * selected" — the canonical "select all" partial state).
871
+ *
872
+ * Pre-0.2.2-beta the indicator rendered the CheckIcon for BOTH
873
+ * checked and indeterminate (Radix mounts the Indicator for either
874
+ * state). We now discriminate via the `data-state` attribute Radix
875
+ * sets on the Indicator element itself — see the parent `Indicator`
876
+ * rendering below for the CSS toggle.
877
+ *
878
+ * @internal
879
+ */ function DashIcon({ className }) {
880
+ return jsx("svg", {
881
+ "aria-hidden": "true",
882
+ viewBox: "0 0 16 16",
883
+ fill: "none",
884
+ className: className,
885
+ children: jsx("path", {
886
+ d: "M3 8h10",
887
+ stroke: "currentColor",
888
+ strokeWidth: "2.5",
889
+ strokeLinecap: "round"
890
+ })
891
+ });
892
+ }
833
893
  /**
834
894
  * Dashforge TW Checkbox — bridge-integrated form control.
835
895
  *
@@ -975,11 +1035,16 @@ function _object_without_properties_loose(source, excluded) {
975
1035
  onBlur: handleBlur,
976
1036
  ref: registration == null ? void 0 : registration.ref,
977
1037
  className: cn(v.control(), slotProps == null ? void 0 : (_slotProps_control = slotProps.control) == null ? void 0 : _slotProps_control.className),
978
- children: jsx(RadixCheckbox.Indicator, {
979
- className: cn(v.indicator(), slotProps == null ? void 0 : (_slotProps_indicator = slotProps.indicator) == null ? void 0 : _slotProps_indicator.className),
980
- children: jsx(CheckIcon, {
981
- className: "h-full w-full"
982
- })
1038
+ children: jsxs(RadixCheckbox.Indicator, {
1039
+ className: cn(v.indicator(), 'group', slotProps == null ? void 0 : (_slotProps_indicator = slotProps.indicator) == null ? void 0 : _slotProps_indicator.className),
1040
+ children: [
1041
+ jsx(CheckIcon, {
1042
+ className: "h-full w-full group-data-[state=indeterminate]:hidden"
1043
+ }),
1044
+ jsx(DashIcon, {
1045
+ className: "h-full w-full group-data-[state=checked]:hidden"
1046
+ })
1047
+ ]
983
1048
  })
984
1049
  })),
985
1050
  jsxs("div", {
@@ -1024,7 +1089,12 @@ function _object_without_properties_loose(source, excluded) {
1024
1089
  ],
1025
1090
  thumb: [
1026
1091
  'pointer-events-none inline-block rounded-full bg-white shadow ring-0',
1027
- 'transition-transform',
1092
+ // Slide animation gated on `prefers-reduced-motion: no-preference`
1093
+ // (WCAG 2.3.3). The thumb still moves between positions instantly
1094
+ // for users who request reduced motion — the data-state change
1095
+ // applies the translate-x rule unconditionally, only the smooth
1096
+ // transition between the two is suppressed.
1097
+ 'transition-transform motion-reduce:transition-none',
1028
1098
  'data-[state=unchecked]:translate-x-0'
1029
1099
  ],
1030
1100
  label: 'select-none cursor-pointer text-neutral-900',
@@ -2414,7 +2484,13 @@ function _object_without_properties_loose(source, excluded) {
2414
2484
  'flex items-center justify-center shrink-0 px-2',
2415
2485
  'text-neutral-600 hover:text-neutral-900',
2416
2486
  'disabled:cursor-not-allowed disabled:opacity-40',
2417
- 'transition-colors'
2487
+ 'transition-colors',
2488
+ // Chevron flip on open — targets the SVG child via the aria-
2489
+ // expanded state on the button itself (set by React). Smooth
2490
+ // rotate gated on prefers-reduced-motion (WCAG 2.3.3); the
2491
+ // 180° state still applies, just without the animated tween.
2492
+ '[&[aria-expanded=true]>svg]:rotate-180',
2493
+ '[&>svg]:transition-transform [&>svg]:duration-150 motion-reduce:[&>svg]:transition-none'
2418
2494
  ],
2419
2495
  clearButton: [
2420
2496
  'flex items-center justify-center shrink-0 px-2',
@@ -2507,6 +2583,52 @@ function _object_without_properties_loose(source, excluded) {
2507
2583
  }
2508
2584
  });
2509
2585
 
2586
+ /**
2587
+ * Inline SVG icons used by the Autocomplete chrome (chip remove, clear,
2588
+ * dropdown caret). Stroke uses `currentColor` so the parent's `text-*`
2589
+ * propagates — same pattern as Checkbox's CheckIcon.
2590
+ *
2591
+ * Why inline SVG (not lucide / heroicons / unicode glyphs):
2592
+ * - Zero icon-library dependency: keeps `@dashforge/tw` self-contained.
2593
+ * - Crisp at every size (the unicode `×` / `▾` glyphs we shipped pre-
2594
+ * 0.2.2 rendered as chunky font characters that looked unpolished
2595
+ * next to the rest of the design system).
2596
+ * - SVG scales with parent font-size cleanly via `width="1em" height="1em"`.
2597
+ *
2598
+ * @internal
2599
+ */ function CloseIcon({ className }) {
2600
+ return jsx("svg", {
2601
+ "aria-hidden": "true",
2602
+ viewBox: "0 0 16 16",
2603
+ width: "1em",
2604
+ height: "1em",
2605
+ fill: "none",
2606
+ className: className,
2607
+ children: jsx("path", {
2608
+ d: "M4 4l8 8M12 4l-8 8",
2609
+ stroke: "currentColor",
2610
+ strokeWidth: "1.75",
2611
+ strokeLinecap: "round"
2612
+ })
2613
+ });
2614
+ }
2615
+ function ChevronDownIcon({ className }) {
2616
+ return jsx("svg", {
2617
+ "aria-hidden": "true",
2618
+ viewBox: "0 0 16 16",
2619
+ width: "1em",
2620
+ height: "1em",
2621
+ fill: "none",
2622
+ className: className,
2623
+ children: jsx("path", {
2624
+ d: "M4 6l4 4 4-4",
2625
+ stroke: "currentColor",
2626
+ strokeWidth: "1.75",
2627
+ strokeLinecap: "round",
2628
+ strokeLinejoin: "round"
2629
+ })
2630
+ });
2631
+ }
2510
2632
  /**
2511
2633
  * Coerce a raw value (from bridge / props) into a canonical shape:
2512
2634
  *
@@ -2813,7 +2935,15 @@ const defaultGetOptionDisabled = (option)=>Boolean(option.disabled);
2813
2935
  if (next == null) {
2814
2936
  setInputValue('');
2815
2937
  } else {
2816
- const found = options.find((opt)=>getOptionValue(opt) === next);
2938
+ // Look up the label in the EFFECTIVE option pool (static
2939
+ // `options` + async `asyncOptions` when `loadOptions` is
2940
+ // configured). Looking only at the static `options` here
2941
+ // means async-loaded picks never update the input — the
2942
+ // user sees their search query stick instead of the
2943
+ // selected label after click. Same lookup logic mirrored in
2944
+ // `displayInputValue` so the two paths can't drift.
2945
+ const pool = loadOptions && asyncOptions !== null ? asyncOptions : options;
2946
+ const found = pool.find((opt)=>getOptionValue(opt) === next);
2817
2947
  const found_label = found ? labelAsString(found) : undefined;
2818
2948
  if (found_label !== undefined) {
2819
2949
  setInputValue(found_label);
@@ -2843,6 +2973,8 @@ const defaultGetOptionDisabled = (option)=>Boolean(option.disabled);
2843
2973
  // eslint-disable-next-line react-hooks/exhaustive-deps
2844
2974
  [
2845
2975
  options,
2976
+ asyncOptions,
2977
+ loadOptions,
2846
2978
  isFormMode,
2847
2979
  bridge,
2848
2980
  name,
@@ -3189,7 +3321,7 @@ const defaultGetOptionDisabled = (option)=>Boolean(option.disabled);
3189
3321
  "aria-label": `Remove ${labelText}`,
3190
3322
  tabIndex: -1,
3191
3323
  className: cn(v.chipRemove(), slotProps == null ? void 0 : (_slotProps_chipRemove = slotProps.chipRemove) == null ? void 0 : _slotProps_chipRemove.className),
3192
- children: "\u00D7"
3324
+ children: jsx(CloseIcon, {})
3193
3325
  })
3194
3326
  ]
3195
3327
  }, key);
@@ -3233,7 +3365,7 @@ const defaultGetOptionDisabled = (option)=>Boolean(option.disabled);
3233
3365
  "aria-label": "Clear selection",
3234
3366
  tabIndex: -1,
3235
3367
  className: cn(v.clearButton(), slotProps == null ? void 0 : (_slotProps_clearButton = slotProps.clearButton) == null ? void 0 : _slotProps_clearButton.className),
3236
- children: "\u00D7"
3368
+ children: jsx(CloseIcon, {})
3237
3369
  }),
3238
3370
  jsx("button", {
3239
3371
  type: "button",
@@ -3245,7 +3377,7 @@ const defaultGetOptionDisabled = (option)=>Boolean(option.disabled);
3245
3377
  tabIndex: -1,
3246
3378
  disabled: effectiveDisabled,
3247
3379
  className: cn(v.trigger(), slotProps == null ? void 0 : (_slotProps_trigger = slotProps.trigger) == null ? void 0 : _slotProps_trigger.className),
3248
- children: "\u25BE"
3380
+ children: jsx(ChevronDownIcon, {})
3249
3381
  }),
3250
3382
  isOpen && jsx("ul", {
3251
3383
  ref: listboxRef,
@@ -3643,7 +3775,12 @@ const defaultGetOptionDisabled = (option)=>Boolean(option.disabled);
3643
3775
  'text-neutral-600 hover:text-primary-700',
3644
3776
  'rounded-sm outline-none',
3645
3777
  'focus-visible:ring-2 focus-visible:ring-primary-500/50',
3646
- 'transition-colors'
3778
+ 'transition-colors',
3779
+ // Defensive `no-underline` — see the matching comment in
3780
+ // LeftNav itemLink slot. The Breadcrumbs root cause covers
3781
+ // TopBar too because Breadcrumbs is typically rendered in
3782
+ // TopBar's center slot.
3783
+ 'no-underline hover:no-underline'
3647
3784
  ],
3648
3785
  current: [
3649
3786
  'inline-flex items-center gap-1 truncate',
@@ -3837,7 +3974,10 @@ const defaultGetOptionDisabled = (option)=>Boolean(option.disabled);
3837
3974
  root: [
3838
3975
  'flex flex-col h-full',
3839
3976
  'bg-neutral-50 border-r border-neutral-200',
3840
- 'transition-[width] duration-200'
3977
+ // Width transition (rail-mode toggle) — gated on
3978
+ // prefers-reduced-motion (WCAG 2.3.3). The new width still
3979
+ // applies, just without the animated tween.
3980
+ 'transition-[width] duration-200 motion-reduce:transition-none motion-reduce:duration-0'
3841
3981
  ],
3842
3982
  brand: [
3843
3983
  'flex items-center gap-2 px-3 h-14 shrink-0',
@@ -3851,7 +3991,15 @@ const defaultGetOptionDisabled = (option)=>Boolean(option.disabled);
3851
3991
  'outline-none focus-visible:ring-2 focus-visible:ring-primary-500/50',
3852
3992
  'transition-colors w-full',
3853
3993
  'aria-disabled:opacity-50 aria-disabled:cursor-not-allowed',
3854
- 'aria-disabled:hover:bg-transparent'
3994
+ 'aria-disabled:hover:bg-transparent',
3995
+ // Defensive `no-underline` — Tailwind's preflight removes the
3996
+ // default browser anchor underline globally, but environments
3997
+ // that DISABLE preflight (e.g. our docs-lab, where the tw
3998
+ // section coexists with MUI's chrome) get raw browser defaults
3999
+ // back. Without this, `<a>` items in the nav render underlined
4000
+ // in those contexts. Explicit `no-underline` + `hover:no-underline`
4001
+ // keeps the appearance consistent regardless of preflight state.
4002
+ 'no-underline hover:no-underline'
3855
4003
  ],
3856
4004
  itemActive: 'bg-primary-100 text-primary-900 font-medium',
3857
4005
  itemIcon: 'shrink-0 w-5 h-5 flex items-center justify-center',
@@ -4259,14 +4407,20 @@ const defaultGetOptionDisabled = (option)=>Boolean(option.disabled);
4259
4407
  nav: 'hidden md:flex shrink-0',
4260
4408
  navMobile: [
4261
4409
  'flex md:hidden fixed inset-y-0 left-0 z-40',
4262
- 'transition-transform duration-200 ease-out',
4410
+ // Drawer slide-in is the most prominent motion in AppShell and
4411
+ // a clear WCAG 2.3.3 candidate. Gate the transition on
4412
+ // `prefers-reduced-motion: no-preference`; the data-driven
4413
+ // `-translate-x-full` / `translate-x-0` still applies, just
4414
+ // without the smooth slide.
4415
+ 'transition-transform duration-200 ease-out motion-reduce:transition-none motion-reduce:duration-0',
4263
4416
  '-translate-x-full'
4264
4417
  ],
4265
4418
  main: 'flex-1 min-w-0 overflow-y-auto',
4266
4419
  footer: 'shrink-0',
4267
4420
  backdrop: [
4268
4421
  'md:hidden fixed inset-0 z-30 bg-black/40',
4269
- 'transition-opacity duration-200',
4422
+ // Backdrop opacity fade — micro motion, gated for consistency.
4423
+ 'transition-opacity duration-200 motion-reduce:transition-none motion-reduce:duration-0',
4270
4424
  'opacity-0 pointer-events-none'
4271
4425
  ]
4272
4426
  },
@@ -4283,6 +4437,28 @@ const defaultGetOptionDisabled = (option)=>Boolean(option.disabled);
4283
4437
  }
4284
4438
  });
4285
4439
 
4440
+ /**
4441
+ * Query selector for natively-focusable elements + anything with an
4442
+ * explicit positive (or implicit `0`) tabindex. Used by the focus
4443
+ * trap to enumerate the drawer's interactive content.
4444
+ *
4445
+ * Notes:
4446
+ * - Excludes `[tabindex="-1"]` (intentionally non-tabbable).
4447
+ * - Excludes `:disabled` — the browser already skips them in tab
4448
+ * order; including them here would route focus to dead elements.
4449
+ * - `details > summary` is the standard way to get a focusable
4450
+ * `<details>` toggle.
4451
+ *
4452
+ * @internal
4453
+ */ const FOCUSABLE_SELECTOR = [
4454
+ 'a[href]',
4455
+ 'button:not(:disabled)',
4456
+ 'input:not(:disabled):not([type="hidden"])',
4457
+ 'select:not(:disabled)',
4458
+ 'textarea:not(:disabled)',
4459
+ '[tabindex]:not([tabindex="-1"]):not(:disabled)',
4460
+ 'details > summary:first-of-type'
4461
+ ].join(',');
4286
4462
  /**
4287
4463
  * Dashforge TW AppShell — top-level layout orchestrator.
4288
4464
  *
@@ -4311,6 +4487,20 @@ const defaultGetOptionDisabled = (option)=>Boolean(option.disabled);
4311
4487
  * - The mobile drawer + backdrop participate in the standard
4312
4488
  * "click outside to close" pattern — `Escape` closes the drawer
4313
4489
  * too (added via global keydown).
4490
+ * - **Focus trap** (WCAG 2.4.3) — when the mobile drawer opens:
4491
+ * 1. The previously-focused element is captured.
4492
+ * 2. Focus moves to the first focusable element inside the drawer.
4493
+ * 3. Tab / Shift+Tab wrap inside the drawer (cycle from
4494
+ * last → first and first → last) — Tab can never escape
4495
+ * to the page underneath while the drawer is open.
4496
+ * 4. On close, focus returns to the captured element (typically
4497
+ * the hamburger toggle the user pressed to open the drawer).
4498
+ * This pattern matches WAI-ARIA APG's modal dialog guidance even
4499
+ * though the drawer isn't strictly a dialog — same focus
4500
+ * management makes keyboard users' experience predictable.
4501
+ * - **`role="dialog"` + `aria-modal="true"`** are applied to the
4502
+ * drawer when open so screen readers announce it as a modal
4503
+ * overlay rather than just an aside.
4314
4504
  */ function AppShell(props) {
4315
4505
  var _slotProps_root, _slotProps_header, _slotProps_nav, _slotProps_navMobile, _slotProps_backdrop, _slotProps_main, _slotProps_footer;
4316
4506
  const { header, nav, footer, children, navOpen = false, onNavOpenChange, sx, slotProps } = props;
@@ -4340,6 +4530,80 @@ const defaultGetOptionDisabled = (option)=>Boolean(option.disabled);
4340
4530
  navOpen,
4341
4531
  onNavOpenChange
4342
4532
  ]);
4533
+ /*
4534
+ * Focus trap for the mobile drawer.
4535
+ *
4536
+ * Implementation:
4537
+ * - On open: capture the currently-focused element (`document.activeElement`)
4538
+ * so we can restore it on close. Then move focus to the first
4539
+ * focusable element inside the drawer.
4540
+ * - While open: a Tab/Shift+Tab keydown listener wraps focus inside
4541
+ * the drawer subtree (last → first on Tab from end, first → last
4542
+ * on Shift+Tab from start).
4543
+ * - On close: restore focus to the captured element.
4544
+ *
4545
+ * Hand-rolled (no `focus-trap-react` dep) — the logic is ~20 LOC and
4546
+ * scoped to a single drawer; adding a runtime dep felt like overkill.
4547
+ * If we ever need more sophisticated trap semantics (nested traps,
4548
+ * sentinel nodes, etc.) the dep is the right call.
4549
+ */ const drawerRef = useRef(null);
4550
+ const restoreFocusRef = useRef(null);
4551
+ useEffect(()=>{
4552
+ if (!navOpen) return;
4553
+ // 1) Capture the element that had focus before the drawer opened.
4554
+ restoreFocusRef.current = document.activeElement;
4555
+ // 2) Move focus to the first focusable element inside the drawer.
4556
+ // Defer via rAF so the drawer DOM (slide-in animation start) has
4557
+ // settled and the elements are actually visible/focusable.
4558
+ const raf = requestAnimationFrame(()=>{
4559
+ const drawer = drawerRef.current;
4560
+ if (!drawer) return;
4561
+ const first = drawer.querySelector(FOCUSABLE_SELECTOR);
4562
+ first == null ? void 0 : first.focus();
4563
+ });
4564
+ // 3) Trap Tab / Shift+Tab inside the drawer.
4565
+ const onKeyDown = (event)=>{
4566
+ if (event.key !== 'Tab') return;
4567
+ const drawer = drawerRef.current;
4568
+ if (!drawer) return;
4569
+ const focusables = Array.from(drawer.querySelectorAll(FOCUSABLE_SELECTOR));
4570
+ if (focusables.length === 0) {
4571
+ event.preventDefault();
4572
+ return;
4573
+ }
4574
+ const first = focusables[0];
4575
+ const last = focusables[focusables.length - 1];
4576
+ const active = document.activeElement;
4577
+ if (event.shiftKey) {
4578
+ // Shift+Tab on first → wrap to last.
4579
+ if (active === first || !drawer.contains(active)) {
4580
+ event.preventDefault();
4581
+ last.focus();
4582
+ }
4583
+ } else {
4584
+ // Tab on last → wrap to first.
4585
+ if (active === last || !drawer.contains(active)) {
4586
+ event.preventDefault();
4587
+ first.focus();
4588
+ }
4589
+ }
4590
+ };
4591
+ document.addEventListener('keydown', onKeyDown);
4592
+ return ()=>{
4593
+ cancelAnimationFrame(raf);
4594
+ document.removeEventListener('keydown', onKeyDown);
4595
+ // 4) Restore focus to the previously-focused element on close.
4596
+ // Guard against the element being removed from the DOM (rare
4597
+ // but possible if the consumer re-renders the page while the
4598
+ // drawer is open).
4599
+ const restore = restoreFocusRef.current;
4600
+ if (restore && document.body.contains(restore)) {
4601
+ restore.focus();
4602
+ }
4603
+ };
4604
+ }, [
4605
+ navOpen
4606
+ ]);
4343
4607
  return jsxs("div", {
4344
4608
  className: cn(v.root(), sx, slotProps == null ? void 0 : (_slotProps_root = slotProps.root) == null ? void 0 : _slotProps_root.className),
4345
4609
  children: [
@@ -4357,8 +4621,15 @@ const defaultGetOptionDisabled = (option)=>Boolean(option.disabled);
4357
4621
  children: nav
4358
4622
  }),
4359
4623
  jsx("aside", {
4624
+ ref: drawerRef,
4360
4625
  className: cn(v.navMobile(), slotProps == null ? void 0 : (_slotProps_navMobile = slotProps.navMobile) == null ? void 0 : _slotProps_navMobile.className),
4361
4626
  "aria-hidden": !navOpen,
4627
+ // When open, the drawer is a modal overlay — `dialog` +
4628
+ // `aria-modal="true"` so screen readers announce it as
4629
+ // such. When closed, drop both attributes (the aria-
4630
+ // hidden=true above already removes it from the AT tree).
4631
+ role: navOpen ? 'dialog' : undefined,
4632
+ "aria-modal": navOpen ? true : undefined,
4362
4633
  children: nav
4363
4634
  }),
4364
4635
  jsx("div", {
@@ -4663,8 +4934,10 @@ const ConfirmContext = /*#__PURE__*/ createContext(null);
4663
4934
  'rounded-lg border shadow-lg',
4664
4935
  'text-sm bg-neutral-50 text-neutral-900',
4665
4936
  // Subtle enter transition — opacity + translate, kept short so a
4666
- // burst of snackbars feels snappy.
4667
- 'transition-all duration-200',
4937
+ // burst of snackbars feels snappy. Gated on motion-reduce
4938
+ // (WCAG 2.3.3) — users who request reduced motion see snackbars
4939
+ // pop in instantly without the slide animation.
4940
+ 'transition-all duration-200 motion-reduce:transition-none motion-reduce:duration-0',
4668
4941
  'data-[state=entered]:opacity-100 data-[state=exited]:opacity-0'
4669
4942
  ],
4670
4943
  icon: 'shrink-0 mt-0.5 w-5 h-5 inline-flex items-center justify-center',
@@ -6523,6 +6796,492 @@ AspectRatio.displayName = 'AspectRatio';
6523
6796
  });
6524
6797
  VisuallyHidden.displayName = 'VisuallyHidden';
6525
6798
 
6799
+ /**
6800
+ * `<Dialog>` variant recipe.
6801
+ *
6802
+ * Slots:
6803
+ * - `overlay` — backdrop (Radix's `DialogOverlay`)
6804
+ * - `content` — the dialog box itself
6805
+ * - `title` — heading at the top
6806
+ * - `description` — secondary text under the title
6807
+ * - `closeButton` — top-right `×` button
6808
+ * - `body` — main scrollable area
6809
+ * - `actions` — footer action bar
6810
+ *
6811
+ * Three sizes (`sm` / `md` / `lg`) drive the content max-width.
6812
+ * Motion is gated by `prefers-reduced-motion` so the slide-in
6813
+ * animation collapses to an instant appear on users that opt out.
6814
+ */ const dialogVariants = tv({
6815
+ slots: {
6816
+ overlay: [
6817
+ 'fixed inset-0 z-50 bg-neutral-950/60 backdrop-blur-sm',
6818
+ 'data-[state=open]:animate-in data-[state=open]:fade-in-0',
6819
+ 'data-[state=closed]:animate-out data-[state=closed]:fade-out-0',
6820
+ 'motion-reduce:transition-none motion-reduce:duration-0'
6821
+ ],
6822
+ content: [
6823
+ 'fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2',
6824
+ 'w-full bg-white dark:bg-neutral-900 rounded-lg shadow-xl',
6825
+ 'border border-neutral-200 dark:border-neutral-800',
6826
+ 'p-6 flex flex-col gap-4 max-h-[90vh]',
6827
+ 'focus:outline-none',
6828
+ 'data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
6829
+ 'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
6830
+ 'motion-reduce:transition-none motion-reduce:duration-0'
6831
+ ],
6832
+ title: 'text-lg font-semibold leading-tight text-neutral-900 dark:text-neutral-50',
6833
+ description: 'text-sm text-neutral-600 dark:text-neutral-400',
6834
+ closeButton: [
6835
+ 'absolute top-4 right-4 inline-flex items-center justify-center',
6836
+ 'h-8 w-8 rounded-md text-neutral-500 hover:text-neutral-900',
6837
+ 'dark:text-neutral-400 dark:hover:text-neutral-50',
6838
+ 'hover:bg-neutral-100 dark:hover:bg-neutral-800',
6839
+ 'focus:outline-none focus:ring-2 focus:ring-primary-500',
6840
+ 'transition-colors'
6841
+ ],
6842
+ body: 'flex-1 overflow-y-auto',
6843
+ actions: 'flex justify-end gap-2 pt-2'
6844
+ },
6845
+ variants: {
6846
+ size: {
6847
+ sm: {
6848
+ content: 'max-w-sm'
6849
+ },
6850
+ md: {
6851
+ content: 'max-w-md'
6852
+ },
6853
+ lg: {
6854
+ content: 'max-w-2xl'
6855
+ }
6856
+ }
6857
+ },
6858
+ defaultVariants: {
6859
+ size: 'md'
6860
+ }
6861
+ });
6862
+
6863
+ /**
6864
+ * Dashforge TW `<Dialog>` — declarative modal dialog.
6865
+ *
6866
+ * Built on `@radix-ui/react-dialog`:
6867
+ * - APG dialog pattern out of the box (focus trap, restore focus on
6868
+ * close, Esc-to-close, `aria-modal`, `aria-labelledby`).
6869
+ * - Renders into a portal (no z-index hell).
6870
+ * - Scroll lock on `<body>` while open.
6871
+ *
6872
+ * Use the imperative `useDialog()` (export from this same module) if
6873
+ * you want a `confirm()`-style return value pattern (similar to
6874
+ * `useConfirm()` from `ConfirmDialog`). Use `<Dialog>` when you want
6875
+ * the control flow living in your component tree.
6876
+ */ function Dialog(props) {
6877
+ var _slotProps_overlay, _slotProps_content, _slotProps_title, _slotProps_description, _slotProps_body, _slotProps_closeButton;
6878
+ const { open, onOpenChange, children, title, description, showCloseButton = true, disableBackdropClose = false, disableEscapeClose = false, size = 'md', sx, slotProps } = props;
6879
+ const v = dialogVariants({
6880
+ size
6881
+ });
6882
+ return jsx(RadixDialog.Root, {
6883
+ open: open,
6884
+ onOpenChange: onOpenChange,
6885
+ children: jsxs(RadixDialog.Portal, {
6886
+ children: [
6887
+ jsx(RadixDialog.Overlay, {
6888
+ className: cn(v.overlay(), slotProps == null ? void 0 : (_slotProps_overlay = slotProps.overlay) == null ? void 0 : _slotProps_overlay.className)
6889
+ }),
6890
+ jsxs(RadixDialog.Content, {
6891
+ className: cn(v.content(), sx, slotProps == null ? void 0 : (_slotProps_content = slotProps.content) == null ? void 0 : _slotProps_content.className),
6892
+ onPointerDownOutside: (event)=>{
6893
+ if (disableBackdropClose) event.preventDefault();
6894
+ },
6895
+ onEscapeKeyDown: (event)=>{
6896
+ if (disableEscapeClose) event.preventDefault();
6897
+ },
6898
+ children: [
6899
+ title != null && jsx(RadixDialog.Title, {
6900
+ className: cn(v.title(), slotProps == null ? void 0 : (_slotProps_title = slotProps.title) == null ? void 0 : _slotProps_title.className),
6901
+ children: title
6902
+ }),
6903
+ description != null && jsx(RadixDialog.Description, {
6904
+ className: cn(v.description(), slotProps == null ? void 0 : (_slotProps_description = slotProps.description) == null ? void 0 : _slotProps_description.className),
6905
+ children: description
6906
+ }),
6907
+ jsx("div", {
6908
+ className: cn(v.body(), slotProps == null ? void 0 : (_slotProps_body = slotProps.body) == null ? void 0 : _slotProps_body.className),
6909
+ children: children
6910
+ }),
6911
+ showCloseButton && jsx(RadixDialog.Close, {
6912
+ "aria-label": "Close",
6913
+ className: cn(v.closeButton(), slotProps == null ? void 0 : (_slotProps_closeButton = slotProps.closeButton) == null ? void 0 : _slotProps_closeButton.className),
6914
+ children: jsx("svg", {
6915
+ width: "1em",
6916
+ height: "1em",
6917
+ viewBox: "0 0 16 16",
6918
+ fill: "none",
6919
+ stroke: "currentColor",
6920
+ strokeWidth: "1.5",
6921
+ strokeLinecap: "round",
6922
+ strokeLinejoin: "round",
6923
+ "aria-hidden": "true",
6924
+ children: jsx("path", {
6925
+ d: "M4 4l8 8M12 4l-8 8"
6926
+ })
6927
+ })
6928
+ })
6929
+ ]
6930
+ })
6931
+ ]
6932
+ })
6933
+ });
6934
+ }
6935
+
6936
+ const tabsVariants = tv({
6937
+ slots: {
6938
+ root: 'flex',
6939
+ list: 'inline-flex items-center gap-1',
6940
+ trigger: [
6941
+ 'inline-flex items-center justify-center whitespace-nowrap',
6942
+ 'text-sm font-medium transition-colors',
6943
+ 'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500',
6944
+ 'disabled:opacity-50 disabled:pointer-events-none',
6945
+ 'motion-reduce:transition-none'
6946
+ ],
6947
+ content: [
6948
+ 'mt-3 focus:outline-none',
6949
+ 'data-[state=inactive]:hidden'
6950
+ ]
6951
+ },
6952
+ variants: {
6953
+ variant: {
6954
+ underline: {
6955
+ list: 'border-b border-neutral-200 dark:border-neutral-800',
6956
+ trigger: [
6957
+ 'px-3 py-2 border-b-2 border-transparent text-neutral-600 dark:text-neutral-400',
6958
+ 'hover:text-neutral-900 dark:hover:text-neutral-50',
6959
+ 'data-[state=active]:border-primary-500 data-[state=active]:text-primary-700',
6960
+ 'dark:data-[state=active]:text-primary-400'
6961
+ ]
6962
+ },
6963
+ pill: {
6964
+ list: 'rounded-lg bg-neutral-100 dark:bg-neutral-800 p-1',
6965
+ trigger: [
6966
+ 'px-3 py-1.5 rounded-md text-neutral-600 dark:text-neutral-400',
6967
+ 'hover:text-neutral-900 dark:hover:text-neutral-50',
6968
+ 'data-[state=active]:bg-white dark:data-[state=active]:bg-neutral-900',
6969
+ 'data-[state=active]:text-neutral-900 dark:data-[state=active]:text-neutral-50',
6970
+ 'data-[state=active]:shadow-sm'
6971
+ ]
6972
+ }
6973
+ },
6974
+ orientation: {
6975
+ horizontal: {
6976
+ root: 'flex-col',
6977
+ list: 'flex-row'
6978
+ },
6979
+ vertical: {
6980
+ root: 'flex-row gap-4',
6981
+ list: 'flex-col items-stretch'
6982
+ }
6983
+ }
6984
+ },
6985
+ defaultVariants: {
6986
+ variant: 'underline',
6987
+ orientation: 'horizontal'
6988
+ }
6989
+ });
6990
+
6991
+ /**
6992
+ * Dashforge TW `<Tabs>` — declarative tab navigation.
6993
+ *
6994
+ * Built on `@radix-ui/react-tabs`:
6995
+ * - APG tabs pattern (arrow-key navigation, automatic activation,
6996
+ * `role="tablist"` + `role="tab"` + `role="tabpanel"`).
6997
+ * - Controlled / uncontrolled (`value` / `defaultValue`).
6998
+ *
6999
+ * Two variant axes:
7000
+ * - `variant` — `underline` (default) | `pill`
7001
+ * - `orientation` — `horizontal` (default) | `vertical`
7002
+ */ function Tabs(props) {
7003
+ var _items_, _slotProps_root, _slotProps_list;
7004
+ const { items, value, defaultValue, onValueChange, variant = 'underline', orientation = 'horizontal', sx, slotProps } = props;
7005
+ const v = tabsVariants({
7006
+ variant,
7007
+ orientation
7008
+ });
7009
+ const resolvedDefault = defaultValue != null ? defaultValue : (_items_ = items[0]) == null ? void 0 : _items_.value;
7010
+ return jsxs(RadixTabs.Root, {
7011
+ value: value,
7012
+ defaultValue: resolvedDefault,
7013
+ onValueChange: onValueChange,
7014
+ orientation: orientation,
7015
+ className: cn(v.root(), sx, slotProps == null ? void 0 : (_slotProps_root = slotProps.root) == null ? void 0 : _slotProps_root.className),
7016
+ children: [
7017
+ jsx(RadixTabs.List, {
7018
+ className: cn(v.list(), slotProps == null ? void 0 : (_slotProps_list = slotProps.list) == null ? void 0 : _slotProps_list.className),
7019
+ children: items.map((item)=>{
7020
+ var _slotProps_trigger;
7021
+ return jsx(RadixTabs.Trigger, {
7022
+ value: item.value,
7023
+ disabled: item.disabled,
7024
+ className: cn(v.trigger(), slotProps == null ? void 0 : (_slotProps_trigger = slotProps.trigger) == null ? void 0 : _slotProps_trigger.className),
7025
+ children: item.label
7026
+ }, item.value);
7027
+ })
7028
+ }),
7029
+ items.map((item)=>{
7030
+ var _slotProps_content;
7031
+ return jsx(RadixTabs.Content, {
7032
+ value: item.value,
7033
+ className: cn(v.content(), slotProps == null ? void 0 : (_slotProps_content = slotProps.content) == null ? void 0 : _slotProps_content.className),
7034
+ children: item.content
7035
+ }, item.value);
7036
+ })
7037
+ ]
7038
+ });
7039
+ }
7040
+
7041
+ const tooltipVariants = tv({
7042
+ slots: {
7043
+ content: [
7044
+ 'z-50 max-w-xs rounded-md px-3 py-1.5 text-xs font-medium',
7045
+ 'bg-neutral-900 text-neutral-50',
7046
+ 'dark:bg-neutral-50 dark:text-neutral-900',
7047
+ 'shadow-md',
7048
+ 'data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95',
7049
+ 'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
7050
+ 'motion-reduce:transition-none motion-reduce:duration-0'
7051
+ ],
7052
+ arrow: 'fill-neutral-900 dark:fill-neutral-50'
7053
+ }
7054
+ });
7055
+
7056
+ /**
7057
+ * Dashforge TW `<Tooltip>` — hoverable / focusable tooltip.
7058
+ *
7059
+ * Built on `@radix-ui/react-tooltip`:
7060
+ * - APG tooltip pattern: `role="tooltip"`, `aria-describedby` wired
7061
+ * to the trigger automatically (Radix manages the id linkage).
7062
+ * - Shows on hover OR keyboard focus.
7063
+ * - Dismissed on Escape or pointer leave.
7064
+ *
7065
+ * Wrap any single React node — it becomes the trigger via Radix's
7066
+ * `asChild` slot. The trigger inherits `aria-describedby` so screen
7067
+ * readers announce the tooltip content along with the trigger.
7068
+ *
7069
+ * @example
7070
+ * ```tsx
7071
+ * <Tooltip content="Delete this item">
7072
+ * <Button variant="ghost">
7073
+ * <TrashIcon />
7074
+ * </Button>
7075
+ * </Tooltip>
7076
+ * ```
7077
+ *
7078
+ * Each consumer can also wrap the entire app once in
7079
+ * `<RadixTooltip.Provider>` to share delay/skip configuration; we
7080
+ * include a per-component provider here for ease-of-use.
7081
+ */ function Tooltip(props) {
7082
+ var _slotProps_content, _slotProps_arrow;
7083
+ const { children, content, side = 'top', align = 'center', hideArrow = false, delayDuration = 200, open, onOpenChange, sideOffset = 4, slotProps } = props;
7084
+ const v = tooltipVariants();
7085
+ return jsx(RadixTooltip.Provider, {
7086
+ delayDuration: delayDuration,
7087
+ children: jsxs(RadixTooltip.Root, {
7088
+ open: open,
7089
+ onOpenChange: onOpenChange,
7090
+ children: [
7091
+ jsx(RadixTooltip.Trigger, {
7092
+ asChild: true,
7093
+ children: children
7094
+ }),
7095
+ jsx(RadixTooltip.Portal, {
7096
+ children: jsxs(RadixTooltip.Content, {
7097
+ side: side,
7098
+ align: align,
7099
+ sideOffset: sideOffset,
7100
+ className: cn(v.content(), slotProps == null ? void 0 : (_slotProps_content = slotProps.content) == null ? void 0 : _slotProps_content.className),
7101
+ children: [
7102
+ content,
7103
+ !hideArrow && jsx(RadixTooltip.Arrow, {
7104
+ className: cn(v.arrow(), slotProps == null ? void 0 : (_slotProps_arrow = slotProps.arrow) == null ? void 0 : _slotProps_arrow.className)
7105
+ })
7106
+ ]
7107
+ })
7108
+ })
7109
+ ]
7110
+ })
7111
+ });
7112
+ }
7113
+
7114
+ const popoverVariants = tv({
7115
+ slots: {
7116
+ content: [
7117
+ 'z-50 rounded-md border border-neutral-200 dark:border-neutral-800',
7118
+ 'bg-white dark:bg-neutral-900 p-4 shadow-lg',
7119
+ 'text-sm text-neutral-900 dark:text-neutral-50',
7120
+ 'focus:outline-none',
7121
+ 'data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
7122
+ 'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
7123
+ 'motion-reduce:transition-none motion-reduce:duration-0'
7124
+ ],
7125
+ arrow: 'fill-white dark:fill-neutral-900'
7126
+ }
7127
+ });
7128
+
7129
+ /**
7130
+ * Dashforge TW `<Popover>` — clickable floating content panel.
7131
+ *
7132
+ * Built on `@radix-ui/react-popover`:
7133
+ * - Focus trap inside the popover (Tab cycles within).
7134
+ * - Outside-click + Escape dismiss.
7135
+ * - APG dialog-like semantics (`role="dialog"` on content).
7136
+ * - Portal-rendered.
7137
+ *
7138
+ * Use `<Popover>` for menus / pickers / settings panels with
7139
+ * interactive children. Use `<Tooltip>` for read-only hints.
7140
+ */ function Popover(props) {
7141
+ var _slotProps_content, _slotProps_arrow;
7142
+ const { children, content, side = 'bottom', align = 'center', showArrow = false, open, onOpenChange, sideOffset = 8, width, slotProps } = props;
7143
+ const v = popoverVariants();
7144
+ return jsxs(RadixPopover.Root, {
7145
+ open: open,
7146
+ onOpenChange: onOpenChange,
7147
+ children: [
7148
+ jsx(RadixPopover.Trigger, {
7149
+ asChild: true,
7150
+ children: children
7151
+ }),
7152
+ jsx(RadixPopover.Portal, {
7153
+ children: jsxs(RadixPopover.Content, {
7154
+ side: side,
7155
+ align: align,
7156
+ sideOffset: sideOffset,
7157
+ style: width != null ? {
7158
+ width
7159
+ } : undefined,
7160
+ className: cn(v.content(), slotProps == null ? void 0 : (_slotProps_content = slotProps.content) == null ? void 0 : _slotProps_content.className),
7161
+ children: [
7162
+ content,
7163
+ showArrow && jsx(RadixPopover.Arrow, {
7164
+ className: cn(v.arrow(), slotProps == null ? void 0 : (_slotProps_arrow = slotProps.arrow) == null ? void 0 : _slotProps_arrow.className)
7165
+ })
7166
+ ]
7167
+ })
7168
+ })
7169
+ ]
7170
+ });
7171
+ }
7172
+
7173
+ const accordionVariants = tv({
7174
+ slots: {
7175
+ root: 'w-full',
7176
+ item: 'border-b border-neutral-200 dark:border-neutral-800',
7177
+ header: 'flex',
7178
+ trigger: [
7179
+ 'flex flex-1 items-center justify-between py-3 text-left',
7180
+ 'text-sm font-medium text-neutral-900 dark:text-neutral-50',
7181
+ 'transition-all hover:underline',
7182
+ 'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:rounded',
7183
+ '[&[data-state=open]>svg]:rotate-180',
7184
+ 'disabled:opacity-50 disabled:pointer-events-none',
7185
+ 'motion-reduce:transition-none'
7186
+ ],
7187
+ content: [
7188
+ 'overflow-hidden text-sm text-neutral-700 dark:text-neutral-300',
7189
+ 'data-[state=open]:animate-accordion-down',
7190
+ 'data-[state=closed]:animate-accordion-up',
7191
+ 'motion-reduce:animate-none'
7192
+ ],
7193
+ chevron: [
7194
+ 'h-4 w-4 shrink-0 text-neutral-500 transition-transform duration-200',
7195
+ 'motion-reduce:transition-none motion-reduce:duration-0'
7196
+ ]
7197
+ }
7198
+ });
7199
+
7200
+ /**
7201
+ * Dashforge TW `<Accordion>` — collapsible section list.
7202
+ *
7203
+ * Built on `@radix-ui/react-accordion`:
7204
+ * - APG accordion pattern: `role="region"` on content, header
7205
+ * button with `aria-expanded` and `aria-controls` linked to
7206
+ * the panel.
7207
+ * - Keyboard support: Space / Enter toggle, ArrowDown / ArrowUp
7208
+ * move focus between triggers, Home / End to extremes.
7209
+ * - Two modes:
7210
+ * - `type="single"` — at most one section open at a time;
7211
+ * `collapsible` allows clicking the open section to close it
7212
+ * (default true).
7213
+ * - `type="multiple"` — multiple sections open simultaneously.
7214
+ *
7215
+ * The trigger renders a chevron that flips 180° via CSS
7216
+ * (`data-state=open` selector) — no React state, no JS animation.
7217
+ */ function Accordion(props) {
7218
+ var _props_collapsible;
7219
+ var _slotProps_root;
7220
+ const v = accordionVariants();
7221
+ const { items, sx, slotProps } = props;
7222
+ const renderItems = (items, slotProps)=>items.map((item)=>{
7223
+ var _slotProps_item, _slotProps_header, _slotProps_trigger, _slotProps_chevron, _slotProps_content;
7224
+ return jsxs(RadixAccordion.Item, {
7225
+ value: item.value,
7226
+ disabled: item.disabled,
7227
+ className: cn(v.item(), slotProps == null ? void 0 : (_slotProps_item = slotProps.item) == null ? void 0 : _slotProps_item.className),
7228
+ children: [
7229
+ jsx(RadixAccordion.Header, {
7230
+ className: cn(v.header(), slotProps == null ? void 0 : (_slotProps_header = slotProps.header) == null ? void 0 : _slotProps_header.className),
7231
+ children: jsxs(RadixAccordion.Trigger, {
7232
+ className: cn(v.trigger(), slotProps == null ? void 0 : (_slotProps_trigger = slotProps.trigger) == null ? void 0 : _slotProps_trigger.className),
7233
+ children: [
7234
+ item.header,
7235
+ jsx("svg", {
7236
+ className: cn(v.chevron(), slotProps == null ? void 0 : (_slotProps_chevron = slotProps.chevron) == null ? void 0 : _slotProps_chevron.className),
7237
+ width: "1em",
7238
+ height: "1em",
7239
+ viewBox: "0 0 16 16",
7240
+ fill: "none",
7241
+ stroke: "currentColor",
7242
+ strokeWidth: "1.5",
7243
+ strokeLinecap: "round",
7244
+ strokeLinejoin: "round",
7245
+ "aria-hidden": "true",
7246
+ children: jsx("path", {
7247
+ d: "M4 6l4 4 4-4"
7248
+ })
7249
+ })
7250
+ ]
7251
+ })
7252
+ }),
7253
+ jsx(RadixAccordion.Content, {
7254
+ className: cn(v.content(), slotProps == null ? void 0 : (_slotProps_content = slotProps.content) == null ? void 0 : _slotProps_content.className),
7255
+ children: jsx("div", {
7256
+ className: "pb-3 pt-0",
7257
+ children: item.content
7258
+ })
7259
+ })
7260
+ ]
7261
+ }, item.value);
7262
+ });
7263
+ if (props.type === 'multiple') {
7264
+ var _slotProps_root1;
7265
+ return jsx(RadixAccordion.Root, {
7266
+ type: "multiple",
7267
+ value: props.value,
7268
+ defaultValue: props.defaultValue,
7269
+ onValueChange: props.onValueChange,
7270
+ className: cn(v.root(), sx, slotProps == null ? void 0 : (_slotProps_root1 = slotProps.root) == null ? void 0 : _slotProps_root1.className),
7271
+ children: renderItems(items, slotProps)
7272
+ });
7273
+ }
7274
+ return jsx(RadixAccordion.Root, {
7275
+ type: "single",
7276
+ collapsible: (_props_collapsible = props.collapsible) != null ? _props_collapsible : true,
7277
+ value: props.value,
7278
+ defaultValue: props.defaultValue,
7279
+ onValueChange: props.onValueChange,
7280
+ className: cn(v.root(), sx, slotProps == null ? void 0 : (_slotProps_root = slotProps.root) == null ? void 0 : _slotProps_root.className),
7281
+ children: renderItems(items, slotProps)
7282
+ });
7283
+ }
7284
+
6526
7285
  /**
6527
7286
  * @dashforge/tw
6528
7287
  *
@@ -6550,6 +7309,6 @@ VisuallyHidden.displayName = 'VisuallyHidden';
6550
7309
  */ // ───── Components ─────
6551
7310
  /**
6552
7311
  * Package version (synced with `package.json` at publish time).
6553
- */ const VERSION = '0.2.1-beta';
7312
+ */ const VERSION = '0.4.0-beta';
6554
7313
 
6555
- export { AppShell, AspectRatio, Autocomplete, Box, Breadcrumbs, Button, Checkbox, ConfirmDialogProvider, Container, DateTimePicker, Divider, Grid, LeftNav, NumberField, OTPField, RadioGroup, SnackbarProvider, Stack, Switch, TextField, Textarea, TopBar, Typography, VERSION, VisuallyHidden, appShellVariants, autocompleteVariants, boxVariants, breadcrumbsVariants, buttonVariants, checkboxVariants, cn, confirmDialogVariants, containerVariants, dateTimePickerVariants, dividerLineVariants, dividerVariants, gridVariants, isoToInputValue, leftNavVariants, numberFieldVariants, otpFieldVariants, radioGroupVariants, snackbarVariants, stackVariants, switchVariants, textFieldVariants, textareaVariants, topBarVariants, typographyVariants, useAccessState, useConfirm, useSnackbar };
7314
+ export { Accordion, AppShell, AspectRatio, Autocomplete, Box, Breadcrumbs, Button, Checkbox, ConfirmDialogProvider, Container, DateTimePicker, Dialog, Divider, Grid, LeftNav, NumberField, OTPField, Popover, RadioGroup, 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, popoverVariants, radioGroupVariants, snackbarVariants, stackVariants, switchVariants, tabsVariants, textFieldVariants, textareaVariants, tooltipVariants, topBarVariants, typographyVariants, useAccessState, useConfirm, useSnackbar };