@dashforge/tw 1.5.2 → 1.7.0

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 (37) hide show
  1. package/CHANGELOG.md +202 -0
  2. package/LICENSE +21 -0
  3. package/dist/index.esm.js +535 -140
  4. package/dist/src/components/Autocomplete/Autocomplete.d.ts +22 -0
  5. package/dist/src/components/Autocomplete/Autocomplete.d.ts.map +1 -1
  6. package/dist/src/components/Autocomplete/autocomplete.props.type-test.d.ts +2 -0
  7. package/dist/src/components/Autocomplete/autocomplete.props.type-test.d.ts.map +1 -0
  8. package/dist/src/components/Autocomplete/autocomplete.types.d.ts +103 -22
  9. package/dist/src/components/Autocomplete/autocomplete.types.d.ts.map +1 -1
  10. package/dist/src/components/Button/button.variants.d.ts.map +1 -1
  11. package/dist/src/components/Checkbox/Checkbox.d.ts.map +1 -1
  12. package/dist/src/components/Checkbox/checkbox.types.d.ts +16 -0
  13. package/dist/src/components/Checkbox/checkbox.types.d.ts.map +1 -1
  14. package/dist/src/components/Checkbox/checkbox.variants.d.ts +3 -0
  15. package/dist/src/components/Checkbox/checkbox.variants.d.ts.map +1 -1
  16. package/dist/src/components/DatePicker/DatePicker.d.ts.map +1 -1
  17. package/dist/src/components/DateRangePicker/DateRangePicker.d.ts.map +1 -1
  18. package/dist/src/components/DateTimePicker/DateTimePicker.d.ts.map +1 -1
  19. package/dist/src/components/Dialog/Dialog.d.ts.map +1 -1
  20. package/dist/src/components/Dialog/dialog.types.d.ts +28 -0
  21. package/dist/src/components/Dialog/dialog.types.d.ts.map +1 -1
  22. package/dist/src/components/NumberField/NumberField.d.ts.map +1 -1
  23. package/dist/src/components/OTPField/OTPField.d.ts.map +1 -1
  24. package/dist/src/components/RadioGroup/RadioGroup.d.ts.map +1 -1
  25. package/dist/src/components/RadioGroup/radioGroup.variants.d.ts.map +1 -1
  26. package/dist/src/components/Select/Select.d.ts.map +1 -1
  27. package/dist/src/components/Slider/Slider.d.ts.map +1 -1
  28. package/dist/src/components/Switch/Switch.d.ts.map +1 -1
  29. package/dist/src/components/Switch/switch.types.d.ts +15 -0
  30. package/dist/src/components/Switch/switch.types.d.ts.map +1 -1
  31. package/dist/src/components/Switch/switch.variants.d.ts +3 -0
  32. package/dist/src/components/Switch/switch.variants.d.ts.map +1 -1
  33. package/dist/src/components/Tabs/tabs.variants.d.ts.map +1 -1
  34. package/dist/src/components/TextField/TextField.d.ts.map +1 -1
  35. package/dist/src/components/Textarea/Textarea.d.ts.map +1 -1
  36. package/dist/src/components/TimePicker/TimePicker.d.ts.map +1 -1
  37. package/package.json +6 -4
package/dist/index.esm.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
2
2
  import { useMemo, forwardRef, useContext, useState, useEffect, useRef, useId, useCallback, Fragment as Fragment$1, createContext, Children, isValidElement, memo, cloneElement, useLayoutEffect } from 'react';
3
3
  import { Slot } from '@radix-ui/react-slot';
4
- import { DashFormContext, useEngineVisibility } from '@dashforge/ui-core';
4
+ import { DashFormContext, useEngineVisibility, useWarnIfControlledInFormMode } from '@dashforge/ui-core';
5
5
  import { useComponentDefaults } from '@dashforge/tw-theme';
6
6
  import clsx from 'clsx';
7
7
  import { twMerge } from 'tailwind-merge';
@@ -13,8 +13,8 @@ import * as RadixTooltip from '@radix-ui/react-tooltip';
13
13
  import * as RadixCheckbox from '@radix-ui/react-checkbox';
14
14
  import * as RadixSwitch from '@radix-ui/react-switch';
15
15
  import * as RadixRadioGroup from '@radix-ui/react-radio-group';
16
- import { useCalendar, resolveCalendarKey, formatLongDate, parseISODate, formatTime, generateTimeOptions, timeStringToMinutes, getTodayISODate, useDateRange } from '@dashforge/calendar-core';
17
16
  import * as RadixPopover from '@radix-ui/react-popover';
17
+ import { useCalendar, resolveCalendarKey, formatLongDate, parseISODate, formatTime, generateTimeOptions, timeStringToMinutes, getTodayISODate, useDateRange } from '@dashforge/calendar-core';
18
18
  import * as RadixDialog from '@radix-ui/react-dialog';
19
19
  import * as RadixSlider from '@radix-ui/react-slider';
20
20
  import * as RadixMenu from '@radix-ui/react-dropdown-menu';
@@ -350,6 +350,12 @@ Spinner.displayName = 'Spinner';
350
350
  'inline-flex items-center justify-center gap-2',
351
351
  'font-medium',
352
352
  'rounded-md',
353
+ // Pointer affordance — the browser UA cursor for `<button>` is
354
+ // `default`, not `pointer`. Applies to every non-disabled state
355
+ // (disabled + loading override below); `variant='link'` inherits
356
+ // this cursor too, matching what browsers show for anchors. See
357
+ // README-BUG § BUG 13.
358
+ 'cursor-pointer',
353
359
  'select-none whitespace-nowrap',
354
360
  'transition-colors',
355
361
  'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2',
@@ -1033,7 +1039,14 @@ const tooltipVariants = tv({
1033
1039
  const themeSlotProps = themeDefaults == null ? void 0 : themeDefaults.slotProps;
1034
1040
  // Option C deep-merge: instance `tooltip` over `theme…defaults.tooltip`.
1035
1041
  const tooltipConfig = resolveFieldTooltip(props.tooltip, themeDefaults == null ? void 0 : (_themeDefaults_defaults = themeDefaults.defaults) == null ? void 0 : _themeDefaults_defaults.tooltip);
1036
- 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 } = merged, rest = _object_without_properties_loose(merged, [
1042
+ 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, // Consume `tooltip` here so it does not leak into `...rest` and
1043
+ // end up as an unknown HTML attribute on the rendered <input>.
1044
+ // React 19 passes lowercase unknown attrs through silently, so
1045
+ // the value would ship as `tooltip="…"` on the DOM without any
1046
+ // console signal. Also strips the object form (`{ content, side, … }`)
1047
+ // whose default stringification is `[object Object]`.
1048
+ // See README-BUG § BUG 9.
1049
+ tooltip: _tooltip } = merged, rest = _object_without_properties_loose(merged, [
1037
1050
  "name",
1038
1051
  "rules",
1039
1052
  "visibleWhen",
@@ -1053,7 +1066,8 @@ const tooltipVariants = tv({
1053
1066
  "onChange",
1054
1067
  "onBlur",
1055
1068
  "value",
1056
- "defaultValue"
1069
+ "defaultValue",
1070
+ "tooltip"
1057
1071
  ]);
1058
1072
  // ───── Hooks (unconditional) ─────
1059
1073
  const bridge = useContext(DashFormContext);
@@ -1081,6 +1095,11 @@ const tooltipVariants = tv({
1081
1095
  return ()=>{
1082
1096
  isMountedRef.current = false;
1083
1097
  const { bridge: cap, name: capName } = unregisterRef.current;
1098
+ // BUG 22: only release bridge state on unmount if the form
1099
+ // is configured to forget unmounted fields. Default `false`
1100
+ // keeps values in RHF so <Stepper> / tab-swap patterns can
1101
+ // read earlier answers back on later steps. See README-BUG.md § BUG 22.
1102
+ if (!(cap == null ? void 0 : cap.shouldUnregister)) return;
1084
1103
  queueMicrotask(()=>{
1085
1104
  var _cap_unregister;
1086
1105
  if (!isMountedRef.current) cap == null ? void 0 : (_cap_unregister = cap.unregister) == null ? void 0 : _cap_unregister.call(cap, capName);
@@ -1230,6 +1249,11 @@ const tooltipVariants = tv({
1230
1249
  'rounded border bg-neutral-50',
1231
1250
  'border-neutral-300',
1232
1251
  'transition-colors',
1252
+ // Radix.Checkbox.Root renders a `<button>`, whose UA cursor is
1253
+ // `default`. The label already reads `cursor-pointer`, but the
1254
+ // control itself needs the same affordance when the pointer
1255
+ // sits directly on the box. See README-BUG § BUG 13.
1256
+ 'cursor-pointer',
1233
1257
  'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-primary-500',
1234
1258
  // Radix data-state hooks
1235
1259
  'data-[state=checked]:bg-primary-500 data-[state=checked]:border-primary-500',
@@ -1238,6 +1262,10 @@ const tooltipVariants = tv({
1238
1262
  ],
1239
1263
  indicator: 'flex items-center justify-center text-white',
1240
1264
  label: 'select-none cursor-pointer text-neutral-900',
1265
+ // BUG 21: `*` marker after the label, matching the `textField.variants.ts`
1266
+ // `requiredMark` slot so a consent box aligns visually with sibling
1267
+ // required fields in a form.
1268
+ requiredMark: 'text-danger-500 ml-0.5',
1241
1269
  helperText: 'text-sm text-neutral-600 mt-1',
1242
1270
  errorText: 'text-sm text-danger-600 mt-1'
1243
1271
  },
@@ -1346,13 +1374,13 @@ const tooltipVariants = tv({
1346
1374
  * rather than MUI slot components.
1347
1375
  */ function Checkbox(props) {
1348
1376
  var _ref;
1349
- var _themeDefaults_defaults, _themeSlotProps_root, _slotProps_root, _themeSlotProps_control, _slotProps_control, _themeSlotProps_indicator, _slotProps_indicator, _themeSlotProps_label, _slotProps_label, _themeSlotProps_errorText, _slotProps_errorText, _themeSlotProps_helperText, _slotProps_helperText;
1377
+ var _themeDefaults_defaults, _themeSlotProps_root, _slotProps_root, _themeSlotProps_control, _slotProps_control, _themeSlotProps_indicator, _slotProps_indicator, _themeSlotProps_label, _slotProps_label, _themeSlotProps_requiredMark, _slotProps_requiredMark, _themeSlotProps_errorText, _slotProps_errorText, _themeSlotProps_helperText, _slotProps_helperText;
1350
1378
  const themeDefaults = useComponentDefaults('Checkbox');
1351
1379
  const merged = _extends({}, themeDefaults == null ? void 0 : themeDefaults.defaults, props);
1352
1380
  const themeSlotProps = themeDefaults == null ? void 0 : themeDefaults.slotProps;
1353
1381
  // Option C deep-merge: instance `tooltip` over `theme…defaults.tooltip`.
1354
1382
  const tooltipConfig = resolveFieldTooltip(props.tooltip, themeDefaults == null ? void 0 : (_themeDefaults_defaults = themeDefaults.defaults) == null ? void 0 : _themeDefaults_defaults.tooltip);
1355
- const { name, rules, visibleWhen, label, helperText, error, access, size, disabled, checked, defaultChecked, onCheckedChange, sx, slotProps } = merged;
1383
+ const { name, rules, visibleWhen, label, helperText, error, access, size, disabled, required, checked, defaultChecked, onCheckedChange, sx, slotProps } = merged;
1356
1384
  // ───── Hooks (always called, unconditional) ─────
1357
1385
  const bridge = useContext(DashFormContext);
1358
1386
  const isVisible = useEngineVisibility(bridge == null ? void 0 : bridge.engine, visibleWhen);
@@ -1362,6 +1390,32 @@ const tooltipVariants = tv({
1362
1390
  const fieldMeta = useDashFieldMeta(name);
1363
1391
  const accessState = useAccessState(access);
1364
1392
  const controlId = useId();
1393
+ // BUG 21 v2: local ref for the Radix Root DOM button. We use this to
1394
+ // set `aria-required` post-mount because setting the attribute via a
1395
+ // JSX prop on `RadixCheckbox.Root` does not reach the browser DOM in
1396
+ // production builds (verified empirically in urbango on Radix v1.3.3;
1397
+ // jsdom does not surface the same gap).
1398
+ const buttonRef = useRef(null);
1399
+ const setControlRef = (node)=>{
1400
+ buttonRef.current = node;
1401
+ const regRef = registration == null ? void 0 : registration.ref;
1402
+ if (typeof regRef === 'function') {
1403
+ regRef(node);
1404
+ } else if (regRef && typeof regRef === 'object') {
1405
+ regRef.current = node;
1406
+ }
1407
+ };
1408
+ useEffect(()=>{
1409
+ const node = buttonRef.current;
1410
+ if (!node) return;
1411
+ if (required) {
1412
+ node.setAttribute('aria-required', 'true');
1413
+ } else {
1414
+ node.removeAttribute('aria-required');
1415
+ }
1416
+ }, [
1417
+ required
1418
+ ]);
1365
1419
  // StrictMode-safe unregister-on-unmount — mirrors MUI Checkbox.tsx
1366
1420
  // (the bridge identity changes on every keystroke, so we MUST NOT
1367
1421
  // re-run cleanup on deps changes — empty deps + ref pattern).
@@ -1379,6 +1433,11 @@ const tooltipVariants = tv({
1379
1433
  return ()=>{
1380
1434
  isMountedRef.current = false;
1381
1435
  const { bridge: cap, name: capName } = unregisterRef.current;
1436
+ // BUG 22: only release bridge state on unmount if the form
1437
+ // is configured to forget unmounted fields. Default `false`
1438
+ // keeps values in RHF so <Stepper> / tab-swap patterns can
1439
+ // read earlier answers back on later steps. See README-BUG.md § BUG 22.
1440
+ if (!(cap == null ? void 0 : cap.shouldUnregister)) return;
1382
1441
  queueMicrotask(()=>{
1383
1442
  var _cap_unregister;
1384
1443
  if (!isMountedRef.current) cap == null ? void 0 : (_cap_unregister = cap.unregister) == null ? void 0 : _cap_unregister.call(cap, capName);
@@ -1467,9 +1526,19 @@ const tooltipVariants = tv({
1467
1526
  name: name
1468
1527
  }, radixStateProps, {
1469
1528
  disabled: effectiveDisabled,
1529
+ required: required,
1470
1530
  onCheckedChange: handleCheckedChange,
1471
1531
  onBlur: handleBlur,
1472
- ref: registration == null ? void 0 : registration.ref,
1532
+ // BUG 21 v2: compose our own ref with `registration.ref` so we can
1533
+ // ALSO set `aria-required` post-mount via a `useEffect` below. The
1534
+ // JSX-level `aria-required` prop was verified empirically not to
1535
+ // reach the DOM in a real browser (Radix v1.3.3 reads its own
1536
+ // `required` prop into context and re-emits `aria-required` from
1537
+ // the internal trigger, but the attribute is stripped somewhere
1538
+ // in the production path — jsdom does not catch it). Post-mount
1539
+ // ref-based setter is Radix-version-agnostic and lands on the
1540
+ // element the browser actually shows.
1541
+ ref: setControlRef,
1473
1542
  className: cn(v.control(), themeSlotProps == null ? void 0 : (_themeSlotProps_control = themeSlotProps.control) == null ? void 0 : _themeSlotProps_control.className, slotProps == null ? void 0 : (_slotProps_control = slotProps.control) == null ? void 0 : _slotProps_control.className),
1474
1543
  children: jsxs(RadixCheckbox.Indicator, {
1475
1544
  className: cn(v.indicator(), 'group', themeSlotProps == null ? void 0 : (_themeSlotProps_indicator = themeSlotProps.indicator) == null ? void 0 : _themeSlotProps_indicator.className, slotProps == null ? void 0 : (_slotProps_indicator = slotProps.indicator) == null ? void 0 : _slotProps_indicator.className),
@@ -1486,10 +1555,17 @@ const tooltipVariants = tv({
1486
1555
  jsxs("div", {
1487
1556
  className: "flex flex-col",
1488
1557
  children: [
1489
- label && jsx("label", {
1558
+ label && jsxs("label", {
1490
1559
  htmlFor: controlId,
1491
1560
  className: cn(v.label(), themeSlotProps == null ? void 0 : (_themeSlotProps_label = themeSlotProps.label) == null ? void 0 : _themeSlotProps_label.className, slotProps == null ? void 0 : (_slotProps_label = slotProps.label) == null ? void 0 : _slotProps_label.className),
1492
- children: renderLabelWithTooltip(label, tooltipConfig)
1561
+ children: [
1562
+ renderLabelWithTooltip(label, tooltipConfig),
1563
+ required && jsx("span", {
1564
+ "aria-hidden": "true",
1565
+ className: cn(v.requiredMark(), themeSlotProps == null ? void 0 : (_themeSlotProps_requiredMark = themeSlotProps.requiredMark) == null ? void 0 : _themeSlotProps_requiredMark.className, slotProps == null ? void 0 : (_slotProps_requiredMark = slotProps.requiredMark) == null ? void 0 : _slotProps_requiredMark.className),
1566
+ children: "*"
1567
+ })
1568
+ ]
1493
1569
  }),
1494
1570
  resolvedHelperText && jsx("p", {
1495
1571
  className: cn(resolvedError ? v.errorText() : v.helperText(), resolvedError ? [
@@ -1540,6 +1616,9 @@ const tooltipVariants = tv({
1540
1616
  'data-[state=unchecked]:translate-x-0'
1541
1617
  ],
1542
1618
  label: 'select-none cursor-pointer text-neutral-900',
1619
+ // BUG 21: `*` marker after the label, same tokens as
1620
+ // `checkboxVariants.requiredMark` for visual parity.
1621
+ requiredMark: 'text-danger-500 ml-0.5',
1543
1622
  helperText: 'text-sm text-neutral-600 mt-1',
1544
1623
  errorText: 'text-sm text-danger-600 mt-1'
1545
1624
  },
@@ -1586,19 +1665,45 @@ const tooltipVariants = tv({
1586
1665
  * translate-x distance so the knob lands flush at each end.
1587
1666
  */ function Switch(props) {
1588
1667
  var _ref;
1589
- var _themeDefaults_defaults, _themeSlotProps_root, _slotProps_root, _themeSlotProps_control, _slotProps_control, _themeSlotProps_thumb, _slotProps_thumb, _themeSlotProps_label, _slotProps_label, _themeSlotProps_errorText, _slotProps_errorText, _themeSlotProps_helperText, _slotProps_helperText;
1668
+ var _themeDefaults_defaults, _themeSlotProps_root, _slotProps_root, _themeSlotProps_control, _slotProps_control, _themeSlotProps_thumb, _slotProps_thumb, _themeSlotProps_label, _slotProps_label, _themeSlotProps_requiredMark, _slotProps_requiredMark, _themeSlotProps_errorText, _slotProps_errorText, _themeSlotProps_helperText, _slotProps_helperText;
1590
1669
  const themeDefaults = useComponentDefaults('Switch');
1591
1670
  const merged = _extends({}, themeDefaults == null ? void 0 : themeDefaults.defaults, props);
1592
1671
  const themeSlotProps = themeDefaults == null ? void 0 : themeDefaults.slotProps;
1593
1672
  // Option C deep-merge: instance `tooltip` over `theme…defaults.tooltip`.
1594
1673
  const tooltipConfig = resolveFieldTooltip(props.tooltip, themeDefaults == null ? void 0 : (_themeDefaults_defaults = themeDefaults.defaults) == null ? void 0 : _themeDefaults_defaults.tooltip);
1595
- const { name, rules, visibleWhen, label, helperText, error, access, size, disabled, checked, defaultChecked, onCheckedChange, sx, slotProps } = merged;
1674
+ const { name, rules, visibleWhen, label, helperText, error, access, size, disabled, required, checked, defaultChecked, onCheckedChange, sx, slotProps } = merged;
1596
1675
  const bridge = useContext(DashFormContext);
1597
1676
  const isVisible = useEngineVisibility(bridge == null ? void 0 : bridge.engine, visibleWhen);
1598
1677
  // Reactive snapshot — see Checkbox.tsx for the same rationale.
1599
1678
  const fieldMeta = useDashFieldMeta(name);
1600
1679
  const accessState = useAccessState(access);
1601
1680
  const controlId = useId();
1681
+ // BUG 21 v2: local ref + post-mount `aria-required` setter — same
1682
+ // pattern as Checkbox.tsx. Radix.Switch.Root reads `required` prop
1683
+ // into context but the JSX `aria-required` attribute does not reach
1684
+ // the browser DOM in production builds. Setting via ref is
1685
+ // Radix-version-agnostic. See README-BUG.md § BUG 21.
1686
+ const buttonRef = useRef(null);
1687
+ const setControlRef = (node)=>{
1688
+ buttonRef.current = node;
1689
+ const regRef = registration == null ? void 0 : registration.ref;
1690
+ if (typeof regRef === 'function') {
1691
+ regRef(node);
1692
+ } else if (regRef && typeof regRef === 'object') {
1693
+ regRef.current = node;
1694
+ }
1695
+ };
1696
+ useEffect(()=>{
1697
+ const node = buttonRef.current;
1698
+ if (!node) return;
1699
+ if (required) {
1700
+ node.setAttribute('aria-required', 'true');
1701
+ } else {
1702
+ node.removeAttribute('aria-required');
1703
+ }
1704
+ }, [
1705
+ required
1706
+ ]);
1602
1707
  // StrictMode-safe unregister-on-unmount
1603
1708
  const unregisterRef = useRef({
1604
1709
  bridge,
@@ -1614,6 +1719,11 @@ const tooltipVariants = tv({
1614
1719
  return ()=>{
1615
1720
  isMountedRef.current = false;
1616
1721
  const { bridge: cap, name: capName } = unregisterRef.current;
1722
+ // BUG 22: only release bridge state on unmount if the form
1723
+ // is configured to forget unmounted fields. Default `false`
1724
+ // keeps values in RHF so <Stepper> / tab-swap patterns can
1725
+ // read earlier answers back on later steps. See README-BUG.md § BUG 22.
1726
+ if (!(cap == null ? void 0 : cap.shouldUnregister)) return;
1617
1727
  queueMicrotask(()=>{
1618
1728
  var _cap_unregister;
1619
1729
  if (!isMountedRef.current) cap == null ? void 0 : (_cap_unregister = cap.unregister) == null ? void 0 : _cap_unregister.call(cap, capName);
@@ -1689,9 +1799,13 @@ const tooltipVariants = tv({
1689
1799
  name: name
1690
1800
  }, radixStateProps, {
1691
1801
  disabled: effectiveDisabled,
1802
+ required: required,
1692
1803
  onCheckedChange: handleCheckedChange,
1693
1804
  onBlur: handleBlur,
1694
- ref: registration == null ? void 0 : registration.ref,
1805
+ // BUG 21 v2: `aria-required` set via `setControlRef` + `useEffect`
1806
+ // above, not via JSX prop. See Checkbox.tsx for the same pattern
1807
+ // and the reasoning.
1808
+ ref: setControlRef,
1695
1809
  className: cn(v.control(), themeSlotProps == null ? void 0 : (_themeSlotProps_control = themeSlotProps.control) == null ? void 0 : _themeSlotProps_control.className, slotProps == null ? void 0 : (_slotProps_control = slotProps.control) == null ? void 0 : _slotProps_control.className),
1696
1810
  children: jsx(RadixSwitch.Thumb, {
1697
1811
  className: cn(v.thumb(), themeSlotProps == null ? void 0 : (_themeSlotProps_thumb = themeSlotProps.thumb) == null ? void 0 : _themeSlotProps_thumb.className, slotProps == null ? void 0 : (_slotProps_thumb = slotProps.thumb) == null ? void 0 : _slotProps_thumb.className)
@@ -1700,10 +1814,17 @@ const tooltipVariants = tv({
1700
1814
  jsxs("div", {
1701
1815
  className: "flex flex-col",
1702
1816
  children: [
1703
- label && jsx("label", {
1817
+ label && jsxs("label", {
1704
1818
  htmlFor: controlId,
1705
1819
  className: cn(v.label(), themeSlotProps == null ? void 0 : (_themeSlotProps_label = themeSlotProps.label) == null ? void 0 : _themeSlotProps_label.className, slotProps == null ? void 0 : (_slotProps_label = slotProps.label) == null ? void 0 : _slotProps_label.className),
1706
- children: renderLabelWithTooltip(label, tooltipConfig)
1820
+ children: [
1821
+ renderLabelWithTooltip(label, tooltipConfig),
1822
+ required && jsx("span", {
1823
+ "aria-hidden": "true",
1824
+ className: cn(v.requiredMark(), themeSlotProps == null ? void 0 : (_themeSlotProps_requiredMark = themeSlotProps.requiredMark) == null ? void 0 : _themeSlotProps_requiredMark.className, slotProps == null ? void 0 : (_slotProps_requiredMark = slotProps.requiredMark) == null ? void 0 : _slotProps_requiredMark.className),
1825
+ children: "*"
1826
+ })
1827
+ ]
1707
1828
  }),
1708
1829
  resolvedHelperText && jsx("p", {
1709
1830
  className: cn(resolvedError ? v.errorText() : v.helperText(), resolvedError ? [
@@ -1754,6 +1875,11 @@ const tooltipVariants = tv({
1754
1875
  'rounded-full border bg-neutral-50',
1755
1876
  'border-neutral-300',
1756
1877
  'transition-colors',
1878
+ // Radix.RadioGroup.Item renders a `<button>`, whose UA cursor
1879
+ // is `default`. optionLabel already reads `cursor-pointer`, but
1880
+ // the control itself needs the same affordance when the pointer
1881
+ // sits directly on the circle. See README-BUG § BUG 13.
1882
+ 'cursor-pointer',
1757
1883
  'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-primary-500',
1758
1884
  'data-[state=checked]:border-primary-500',
1759
1885
  'data-[disabled]:opacity-50 data-[disabled]:cursor-not-allowed'
@@ -1879,6 +2005,11 @@ const tooltipVariants = tv({
1879
2005
  return ()=>{
1880
2006
  isMountedRef.current = false;
1881
2007
  const { bridge: cap, name: capName } = unregisterRef.current;
2008
+ // BUG 22: only release bridge state on unmount if the form
2009
+ // is configured to forget unmounted fields. Default `false`
2010
+ // keeps values in RHF so <Stepper> / tab-swap patterns can
2011
+ // read earlier answers back on later steps. See README-BUG.md § BUG 22.
2012
+ if (!(cap == null ? void 0 : cap.shouldUnregister)) return;
1882
2013
  queueMicrotask(()=>{
1883
2014
  var _cap_unregister;
1884
2015
  if (!isMountedRef.current) cap == null ? void 0 : (_cap_unregister = cap.unregister) == null ? void 0 : _cap_unregister.call(cap, capName);
@@ -1889,6 +2020,15 @@ const tooltipVariants = tv({
1889
2020
  // must run before the visibility early-returns so hook ordering is
1890
2021
  // consistent across renders. See #113 / useStandaloneFieldWarning.
1891
2022
  useStandaloneFieldWarning('RadioGroup', name, Boolean(bridge == null ? void 0 : bridge.register), explicitValue, onValueChange);
2023
+ // Mirror of the above: warn if under a <DashFormProvider> AND the
2024
+ // consumer passed controlled-mode props (silently ignored in form
2025
+ // mode). See BUG 2 in libs/dashforge/README-BUG.md. Zero cost in
2026
+ // production.
2027
+ useWarnIfControlledInFormMode('RadioGroup', name, {
2028
+ value: explicitValue,
2029
+ defaultValue,
2030
+ onValueChange
2031
+ });
1892
2032
  // ───── Early returns ─────
1893
2033
  if (!isVisible) return null;
1894
2034
  if (!groupAccessState.visible) return null;
@@ -2136,7 +2276,11 @@ const tooltipVariants = tv({
2136
2276
  const themeSlotProps = themeDefaults == null ? void 0 : themeDefaults.slotProps;
2137
2277
  // Option C deep-merge: instance `tooltip` over `theme…defaults.tooltip`.
2138
2278
  const tooltipConfig = resolveFieldTooltip(props.tooltip, themeDefaults == null ? void 0 : (_themeDefaults_defaults = themeDefaults.defaults) == null ? void 0 : _themeDefaults_defaults.tooltip);
2139
- const { name, rules, visibleWhen, layout = 'stacked', size, resize, label, helperText, required, error, disabled, fullWidth, access, sx, slotProps, placeholder, rows = 3, onChange: userOnChange, onBlur: userOnBlur, value: userValue, defaultValue } = merged, rest = _object_without_properties_loose(merged, [
2279
+ const { name, rules, visibleWhen, layout = 'stacked', size, resize, label, helperText, required, error, disabled, fullWidth, access, sx, slotProps, placeholder, rows = 3, onChange: userOnChange, onBlur: userOnBlur, value: userValue, defaultValue, // Consume `tooltip` here so it does not leak into `...rest` and
2280
+ // end up as an unknown HTML attribute on the rendered <textarea>
2281
+ // (React 19 forwards lowercase unknown attrs silently). See
2282
+ // README-BUG § BUG 9.
2283
+ tooltip: _tooltip } = merged, rest = _object_without_properties_loose(merged, [
2140
2284
  "name",
2141
2285
  "rules",
2142
2286
  "visibleWhen",
@@ -2157,7 +2301,8 @@ const tooltipVariants = tv({
2157
2301
  "onChange",
2158
2302
  "onBlur",
2159
2303
  "value",
2160
- "defaultValue"
2304
+ "defaultValue",
2305
+ "tooltip"
2161
2306
  ]);
2162
2307
  // ───── Hooks (unconditional) ─────
2163
2308
  const bridge = useContext(DashFormContext);
@@ -2181,6 +2326,11 @@ const tooltipVariants = tv({
2181
2326
  return ()=>{
2182
2327
  isMountedRef.current = false;
2183
2328
  const { bridge: cap, name: capName } = unregisterRef.current;
2329
+ // BUG 22: only release bridge state on unmount if the form
2330
+ // is configured to forget unmounted fields. Default `false`
2331
+ // keeps values in RHF so <Stepper> / tab-swap patterns can
2332
+ // read earlier answers back on later steps. See README-BUG.md § BUG 22.
2333
+ if (!(cap == null ? void 0 : cap.shouldUnregister)) return;
2184
2334
  queueMicrotask(()=>{
2185
2335
  var _cap_unregister;
2186
2336
  if (!isMountedRef.current) cap == null ? void 0 : (_cap_unregister = cap.unregister) == null ? void 0 : _cap_unregister.call(cap, capName);
@@ -2436,7 +2586,11 @@ const tooltipVariants = tv({
2436
2586
  const themeSlotProps = themeDefaults == null ? void 0 : themeDefaults.slotProps;
2437
2587
  // Option C deep-merge: instance `tooltip` over `theme…defaults.tooltip`.
2438
2588
  const tooltipConfig = resolveFieldTooltip(props.tooltip, themeDefaults == null ? void 0 : (_themeDefaults_defaults = themeDefaults.defaults) == null ? void 0 : _themeDefaults_defaults.tooltip);
2439
- const { name, rules, visibleWhen, layout = 'stacked', size, label, helperText, required, error, disabled, fullWidth, access, sx, slotProps, placeholder, min, max, step = 1, showStepper = false, onChange: userOnChange, onBlur: userOnBlur, value: userValue, defaultValue } = merged, rest = _object_without_properties_loose(merged, [
2589
+ const { name, rules, visibleWhen, layout = 'stacked', size, label, helperText, required, error, disabled, fullWidth, access, sx, slotProps, placeholder, min, max, step = 1, showStepper = false, onChange: userOnChange, onBlur: userOnBlur, value: userValue, defaultValue, // Consume `tooltip` here so it does not leak into `...rest` and
2590
+ // end up as an unknown HTML attribute on the rendered <input>
2591
+ // (React 19 forwards lowercase unknown attrs silently). See
2592
+ // README-BUG § BUG 9.
2593
+ tooltip: _tooltip } = merged, rest = _object_without_properties_loose(merged, [
2440
2594
  "name",
2441
2595
  "rules",
2442
2596
  "visibleWhen",
@@ -2459,7 +2613,8 @@ const tooltipVariants = tv({
2459
2613
  "onChange",
2460
2614
  "onBlur",
2461
2615
  "value",
2462
- "defaultValue"
2616
+ "defaultValue",
2617
+ "tooltip"
2463
2618
  ]);
2464
2619
  // ───── Hooks (unconditional) ─────
2465
2620
  const bridge = useContext(DashFormContext);
@@ -2498,6 +2653,11 @@ const tooltipVariants = tv({
2498
2653
  return ()=>{
2499
2654
  isMountedRef.current = false;
2500
2655
  const { bridge: cap, name: capName } = unregisterRef.current;
2656
+ // BUG 22: only release bridge state on unmount if the form
2657
+ // is configured to forget unmounted fields. Default `false`
2658
+ // keeps values in RHF so <Stepper> / tab-swap patterns can
2659
+ // read earlier answers back on later steps. See README-BUG.md § BUG 22.
2660
+ if (!(cap == null ? void 0 : cap.shouldUnregister)) return;
2501
2661
  queueMicrotask(()=>{
2502
2662
  var _cap_unregister;
2503
2663
  if (!isMountedRef.current) cap == null ? void 0 : (_cap_unregister = cap.unregister) == null ? void 0 : _cap_unregister.call(cap, capName);
@@ -2792,6 +2952,11 @@ const tooltipVariants = tv({
2792
2952
  return ()=>{
2793
2953
  isMountedRef.current = false;
2794
2954
  const { bridge: cap, name: capName } = unregisterRef.current;
2955
+ // BUG 22: only release bridge state on unmount if the form
2956
+ // is configured to forget unmounted fields. Default `false`
2957
+ // keeps values in RHF so <Stepper> / tab-swap patterns can
2958
+ // read earlier answers back on later steps. See README-BUG.md § BUG 22.
2959
+ if (!(cap == null ? void 0 : cap.shouldUnregister)) return;
2795
2960
  queueMicrotask(()=>{
2796
2961
  var _cap_unregister;
2797
2962
  if (!isMountedRef.current) cap == null ? void 0 : (_cap_unregister = cap.unregister) == null ? void 0 : _cap_unregister.call(cap, capName);
@@ -3185,14 +3350,51 @@ const defaultGetOptionDisabled = (option)=>Boolean(option.disabled);
3185
3350
  * (case-insensitive). Multi-select, free-solo text, generic option
3186
3351
  * shapes, and async runtime options (`optionsFromFieldData`) are
3187
3352
  * deferred to F5-A-bis.
3188
- */ function Autocomplete(props) {
3189
- var _themeDefaults_defaults, _themeSlotProps_root, _slotProps_root, _themeSlotProps_label, _slotProps_label, _themeSlotProps_requiredMark, _slotProps_requiredMark, _themeSlotProps_inputWrapper, _slotProps_inputWrapper, _themeSlotProps_chipsList, _slotProps_chipsList, _themeSlotProps_input, _slotProps_input, _themeSlotProps_clearButton, _slotProps_clearButton, _themeSlotProps_trigger, _slotProps_trigger, _themeSlotProps_popover, _slotProps_popover, _themeSlotProps_listBox, _slotProps_listBox, _themeSlotProps_emptyState, _slotProps_emptyState, _themeSlotProps_emptyState1, _slotProps_emptyState1, _themeSlotProps_errorText, _slotProps_errorText, _themeSlotProps_helperText, _slotProps_helperText;
3353
+ */ /**
3354
+ * Is this event ours?
3355
+ *
3356
+ * "Ours" is the root PLUS the listbox, and the second half is the whole
3357
+ * point. Since the listbox was portaled to `document.body` it is no
3358
+ * longer inside the component's DOM subtree, so `root.contains(target)`
3359
+ * answers FALSE for the options themselves — and two separate pieces of
3360
+ * code used that question to mean "is this mine?":
3361
+ *
3362
+ * the click-outside handler, which closed the popover and reset the
3363
+ * input on a mousedown over an option, before the option's own
3364
+ * `onClick` could run (mouse selection broken, keyboard fine);
3365
+ *
3366
+ * Radix's `DismissableLayer`, which dismissed on the focus that the
3367
+ * combobox input itself receives.
3368
+ *
3369
+ * Both are BUG 7 and BUG 8 in `libs/dashforge/README-BUG.md`. The
3370
+ * predicate exists so the rule has ONE definition and a test can pin
3371
+ * it — `rootRef` does not span the whole widget any more, and anything
3372
+ * here that reasons about containment has to say so out loud.
3373
+ */ function isWithinCombobox(target, root, listbox) {
3374
+ if (!target) return false;
3375
+ return Boolean(root == null ? void 0 : root.contains(target)) || Boolean(listbox == null ? void 0 : listbox.contains(target));
3376
+ }
3377
+ function Autocomplete(props) {
3378
+ var _themeDefaults_defaults, _themeSlotProps_root, _slotProps_root, _themeSlotProps_label, _slotProps_label, _themeSlotProps_requiredMark, _slotProps_requiredMark, _themeSlotProps_inputWrapper, _slotProps_inputWrapper, _themeSlotProps_chipsList, _slotProps_chipsList, _themeSlotProps_input, _slotProps_input, _themeSlotProps_clearButton, _slotProps_clearButton, _themeSlotProps_trigger, _slotProps_trigger, _themeSlotProps_input1, _slotProps_input1, _themeSlotProps_clearButton1, _slotProps_clearButton1, _themeSlotProps_trigger1, _slotProps_trigger1, _themeSlotProps_popover, _slotProps_popover, _themeSlotProps_listBox, _slotProps_listBox, _themeSlotProps_emptyState, _slotProps_emptyState, _themeSlotProps_emptyState1, _slotProps_emptyState1, _themeSlotProps_errorText, _slotProps_errorText, _themeSlotProps_helperText, _slotProps_helperText;
3190
3379
  const themeDefaults = useComponentDefaults('Autocomplete');
3191
3380
  const merged = _extends({}, themeDefaults == null ? void 0 : themeDefaults.defaults, props);
3192
3381
  const themeSlotProps = themeDefaults == null ? void 0 : themeDefaults.slotProps;
3193
3382
  // Option C deep-merge: instance `tooltip` over `theme…defaults.tooltip`.
3194
3383
  const tooltipConfig = resolveFieldTooltip(props.tooltip, themeDefaults == null ? void 0 : (_themeDefaults_defaults = themeDefaults.defaults) == null ? void 0 : _themeDefaults_defaults.tooltip);
3195
3384
  const { name, rules, options, visibleWhen, layout = 'stacked', size, label, helperText, required, error, disabled, fullWidth, access, sx, slotProps, placeholder, value: explicitValue, defaultValue, onValueChange, emptyMessage = 'No matching options', getOptionValue = defaultGetOptionValue, getOptionLabel = defaultGetOptionLabel, getOptionDisabled = defaultGetOptionDisabled, getOptionKey, multiple, freeSolo, loadOptions, loadDebounceMs = 250, loadingMessage = 'Loading…' } = merged;
3385
+ // Dev-mode warning: if this Autocomplete is running under a
3386
+ // <DashFormProvider> AND the consumer has passed any of the
3387
+ // controlled / uncontrolled value props, warn once per
3388
+ // (component, name, prop) that those props are silently ignored
3389
+ // in form mode. See BUG 2 in libs/dashforge/README-BUG.md — the
3390
+ // type-side split can only catch the `rules + defaultValue` combo
3391
+ // at compile time; this runtime rete covers the common case where
3392
+ // consumers pass no `rules`. Zero cost in production.
3393
+ useWarnIfControlledInFormMode('Autocomplete', name, {
3394
+ value: explicitValue,
3395
+ defaultValue,
3396
+ onValueChange
3397
+ });
3196
3398
  const isMulti = Boolean(multiple);
3197
3399
  // Default the React-key accessor to `getOptionValue` (typical case:
3198
3400
  // value is unique). Consumers override only when distinct option
@@ -3231,6 +3433,11 @@ const defaultGetOptionDisabled = (option)=>Boolean(option.disabled);
3231
3433
  return ()=>{
3232
3434
  isMountedRef.current = false;
3233
3435
  const { bridge: cap, name: capName } = unregisterRef.current;
3436
+ // BUG 22: only release bridge state on unmount if the form
3437
+ // is configured to forget unmounted fields. Default `false`
3438
+ // keeps values in RHF so <Stepper> / tab-swap patterns can
3439
+ // read earlier answers back on later steps. See README-BUG.md § BUG 22.
3440
+ if (!(cap == null ? void 0 : cap.shouldUnregister)) return;
3234
3441
  queueMicrotask(()=>{
3235
3442
  var _cap_unregister;
3236
3443
  if (!isMountedRef.current) cap == null ? void 0 : (_cap_unregister = cap.unregister) == null ? void 0 : _cap_unregister.call(cap, capName);
@@ -3510,7 +3717,9 @@ const defaultGetOptionDisabled = (option)=>Boolean(option.disabled);
3510
3717
  const handlePointerDown = (event)=>{
3511
3718
  const root = rootRef.current;
3512
3719
  if (!root) return;
3513
- if (root.contains(event.target)) return;
3720
+ // The listbox counts as ours even though it is portaled out of
3721
+ // the root — see `isWithinCombobox`.
3722
+ if (isWithinCombobox(event.target, root, listboxRef.current)) return;
3514
3723
  closePopover();
3515
3724
  // Multi mode: just clear the filter query (chips remain).
3516
3725
  // Single + free-solo: commit the typed text as the value (the
@@ -3789,127 +3998,254 @@ const defaultGetOptionDisabled = (option)=>Boolean(option.disabled);
3789
3998
  children: "*"
3790
3999
  }))
3791
4000
  }),
3792
- jsxs("div", {
3793
- className: cn(v.inputWrapper(), // Multi mode: let chips wrap and replace the fixed `h-*`
3794
- // with `min-h-*` so the wrapper grows with selections.
3795
- isMulti && 'flex-wrap min-h-[var(--ac-min-h,2.5rem)] !h-auto py-1', themeSlotProps == null ? void 0 : (_themeSlotProps_inputWrapper = themeSlotProps.inputWrapper) == null ? void 0 : _themeSlotProps_inputWrapper.className, slotProps == null ? void 0 : (_slotProps_inputWrapper = slotProps.inputWrapper) == null ? void 0 : _slotProps_inputWrapper.className),
4001
+ jsxs(RadixPopover.Root, {
4002
+ open: isOpen,
4003
+ onOpenChange: (next)=>{
4004
+ if (!next) setIsOpen(false);
4005
+ },
4006
+ modal: false,
3796
4007
  children: [
3797
- isMulti && selectedKeys.size > 0 && jsx("div", {
3798
- className: cn(v.chipsList(), themeSlotProps == null ? void 0 : (_themeSlotProps_chipsList = themeSlotProps.chipsList) == null ? void 0 : _themeSlotProps_chipsList.className, slotProps == null ? void 0 : (_slotProps_chipsList = slotProps.chipsList) == null ? void 0 : _slotProps_chipsList.className),
3799
- children: Array.from(selectedKeys).map((key)=>{
3800
- var _labelAsString;
3801
- var _themeSlotProps_chip, _slotProps_chip, _themeSlotProps_chipRemove, _slotProps_chipRemove;
3802
- const opt = options.find((o)=>getOptionValue(o) === key);
3803
- const labelNode = opt ? getOptionLabel(opt) : key;
3804
- const labelText = opt ? (_labelAsString = labelAsString(opt)) != null ? _labelAsString : key : key;
3805
- return jsxs("span", {
3806
- className: cn(v.chip(), themeSlotProps == null ? void 0 : (_themeSlotProps_chip = themeSlotProps.chip) == null ? void 0 : _themeSlotProps_chip.className, slotProps == null ? void 0 : (_slotProps_chip = slotProps.chip) == null ? void 0 : _slotProps_chip.className),
3807
- "data-chip-key": key,
3808
- children: [
3809
- labelNode,
3810
- !effectiveDisabled && !effectiveReadOnly && jsx("button", {
3811
- type: "button",
3812
- onMouseDown: preventBlur,
3813
- onClick: ()=>{
3814
- var _inputRef_current;
3815
- commitSelection(key); // toggle removes
3816
- (_inputRef_current = inputRef.current) == null ? void 0 : _inputRef_current.focus();
3817
- },
3818
- "aria-label": `Remove ${labelText}`,
3819
- tabIndex: -1,
3820
- className: cn(v.chipRemove(), themeSlotProps == null ? void 0 : (_themeSlotProps_chipRemove = themeSlotProps.chipRemove) == null ? void 0 : _themeSlotProps_chipRemove.className, slotProps == null ? void 0 : (_slotProps_chipRemove = slotProps.chipRemove) == null ? void 0 : _slotProps_chipRemove.className),
3821
- children: jsx(CloseIcon, {})
4008
+ jsx(RadixPopover.Anchor, {
4009
+ asChild: true,
4010
+ children: jsxs("div", {
4011
+ className: cn(v.inputWrapper(), // Multi mode: let chips wrap and replace the fixed `h-*`
4012
+ // with `min-h-*` so the wrapper grows with selections.
4013
+ isMulti && 'flex-wrap min-h-[var(--ac-min-h,2.5rem)] !h-auto py-1', themeSlotProps == null ? void 0 : (_themeSlotProps_inputWrapper = themeSlotProps.inputWrapper) == null ? void 0 : _themeSlotProps_inputWrapper.className, slotProps == null ? void 0 : (_slotProps_inputWrapper = slotProps.inputWrapper) == null ? void 0 : _slotProps_inputWrapper.className),
4014
+ children: [
4015
+ isMulti && selectedKeys.size > 0 && jsx("div", {
4016
+ className: cn(v.chipsList(), themeSlotProps == null ? void 0 : (_themeSlotProps_chipsList = themeSlotProps.chipsList) == null ? void 0 : _themeSlotProps_chipsList.className, slotProps == null ? void 0 : (_slotProps_chipsList = slotProps.chipsList) == null ? void 0 : _slotProps_chipsList.className),
4017
+ children: Array.from(selectedKeys).map((key)=>{
4018
+ var _labelAsString;
4019
+ var _themeSlotProps_chip, _slotProps_chip, _themeSlotProps_chipRemove, _slotProps_chipRemove;
4020
+ const opt = options.find((o)=>getOptionValue(o) === key);
4021
+ const labelNode = opt ? getOptionLabel(opt) : key;
4022
+ const labelText = opt ? (_labelAsString = labelAsString(opt)) != null ? _labelAsString : key : key;
4023
+ return jsxs("span", {
4024
+ className: cn(v.chip(), themeSlotProps == null ? void 0 : (_themeSlotProps_chip = themeSlotProps.chip) == null ? void 0 : _themeSlotProps_chip.className, slotProps == null ? void 0 : (_slotProps_chip = slotProps.chip) == null ? void 0 : _slotProps_chip.className),
4025
+ "data-chip-key": key,
4026
+ children: [
4027
+ labelNode,
4028
+ !effectiveDisabled && !effectiveReadOnly && jsx("button", {
4029
+ type: "button",
4030
+ onMouseDown: preventBlur,
4031
+ onClick: ()=>{
4032
+ var _inputRef_current;
4033
+ commitSelection(key); // toggle removes
4034
+ (_inputRef_current = inputRef.current) == null ? void 0 : _inputRef_current.focus();
4035
+ },
4036
+ "aria-label": `Remove ${labelText}`,
4037
+ tabIndex: -1,
4038
+ className: cn(v.chipRemove(), themeSlotProps == null ? void 0 : (_themeSlotProps_chipRemove = themeSlotProps.chipRemove) == null ? void 0 : _themeSlotProps_chipRemove.className, slotProps == null ? void 0 : (_slotProps_chipRemove = slotProps.chipRemove) == null ? void 0 : _slotProps_chipRemove.className),
4039
+ children: jsx(CloseIcon, {})
4040
+ })
4041
+ ]
4042
+ }, key);
3822
4043
  })
3823
- ]
3824
- }, key);
4044
+ }),
4045
+ isMulti ? jsxs("div", {
4046
+ className: cn(// `min-w-0` on the controls group (and on the input
4047
+ // below) so the input can shrink when chips have taken
4048
+ // most of the outer wrapper's width. Without it, the
4049
+ // group's intrinsic min-content-size (input floor +
4050
+ // shrink-0 buttons) pushed the chevron past the right
4051
+ // edge by ~6px, which turned into a horizontal scrollbar
4052
+ // inside a `<Dialog>` (whose body is `overflow-y-auto`
4053
+ // and so computes `overflow-x: auto`). See BUG 5 in
4054
+ // README-BUG.md — the regression from the BUG 3 fix.
4055
+ 'flex items-center flex-nowrap flex-1 min-w-0', // The gap keeps the clear × visually separated from the
4056
+ // chevron when both are present.
4057
+ hasAnySelection && !effectiveDisabled && !effectiveReadOnly && 'gap-0.5'),
4058
+ children: [
4059
+ jsx("input", {
4060
+ ref: inputRef,
4061
+ id: `${baseId}-input`,
4062
+ name: name,
4063
+ type: "text",
4064
+ role: "combobox",
4065
+ "aria-autocomplete": "list",
4066
+ "aria-expanded": isOpen,
4067
+ "aria-controls": isOpen ? listboxId : undefined,
4068
+ "aria-activedescendant": activeOptionId,
4069
+ "aria-labelledby": label ? labelId : undefined,
4070
+ "aria-describedby": resolvedHelperText ? helperId : undefined,
4071
+ "aria-invalid": resolvedError ? true : undefined,
4072
+ "aria-required": required ? true : undefined,
4073
+ placeholder: // In multi mode hide the placeholder once the user has
4074
+ // at least one chip — chips already convey "something
4075
+ // is selected".
4076
+ selectedKeys.size > 0 ? undefined : placeholder,
4077
+ value: displayInputValue,
4078
+ onChange: handleInputChange,
4079
+ onKeyDown: handleKeyDown,
4080
+ onFocus: openPopover,
4081
+ onBlur: handleBlur,
4082
+ disabled: effectiveDisabled,
4083
+ readOnly: effectiveReadOnly,
4084
+ autoComplete: "off",
4085
+ // `flex-1` inside the non-wrap controls row: the input
4086
+ // expands to consume whatever the controls-row has left
4087
+ // after the two buttons. `basis-24` is the *preferred*
4088
+ // starting size; `min-w-0` lets the input shrink below
4089
+ // that when chips have consumed most of the outer
4090
+ // wrapper — same BUG 5 rationale as the group's
4091
+ // `min-w-0`. Buttons stay `shrink-0` and always
4092
+ // reachable; the input becomes narrow but still usable.
4093
+ className: cn(v.input(), 'min-w-0 flex-1 basis-24', themeSlotProps == null ? void 0 : (_themeSlotProps_input = themeSlotProps.input) == null ? void 0 : _themeSlotProps_input.className, slotProps == null ? void 0 : (_slotProps_input = slotProps.input) == null ? void 0 : _slotProps_input.className)
4094
+ }),
4095
+ hasAnySelection && !effectiveDisabled && !effectiveReadOnly && jsx("button", {
4096
+ type: "button",
4097
+ onMouseDown: preventBlur,
4098
+ onClick: handleClearClick,
4099
+ "aria-label": "Clear selection",
4100
+ tabIndex: -1,
4101
+ className: cn(v.clearButton(), themeSlotProps == null ? void 0 : (_themeSlotProps_clearButton = themeSlotProps.clearButton) == null ? void 0 : _themeSlotProps_clearButton.className, slotProps == null ? void 0 : (_slotProps_clearButton = slotProps.clearButton) == null ? void 0 : _slotProps_clearButton.className),
4102
+ children: jsx(CloseIcon, {})
4103
+ }),
4104
+ jsx("button", {
4105
+ type: "button",
4106
+ onMouseDown: preventBlur,
4107
+ onClick: handleTriggerClick,
4108
+ "aria-label": "Open",
4109
+ "aria-haspopup": "listbox",
4110
+ "aria-expanded": isOpen,
4111
+ tabIndex: -1,
4112
+ disabled: effectiveDisabled,
4113
+ className: cn(v.trigger(), themeSlotProps == null ? void 0 : (_themeSlotProps_trigger = themeSlotProps.trigger) == null ? void 0 : _themeSlotProps_trigger.className, slotProps == null ? void 0 : (_slotProps_trigger = slotProps.trigger) == null ? void 0 : _slotProps_trigger.className),
4114
+ children: jsx(ChevronDownIcon$1, {})
4115
+ })
4116
+ ]
4117
+ }) : jsxs(Fragment, {
4118
+ children: [
4119
+ jsx("input", {
4120
+ ref: inputRef,
4121
+ id: `${baseId}-input`,
4122
+ name: name,
4123
+ type: "text",
4124
+ role: "combobox",
4125
+ "aria-autocomplete": "list",
4126
+ "aria-expanded": isOpen,
4127
+ "aria-controls": isOpen ? listboxId : undefined,
4128
+ "aria-activedescendant": activeOptionId,
4129
+ "aria-labelledby": label ? labelId : undefined,
4130
+ "aria-describedby": resolvedHelperText ? helperId : undefined,
4131
+ "aria-invalid": resolvedError ? true : undefined,
4132
+ "aria-required": required ? true : undefined,
4133
+ placeholder: placeholder,
4134
+ value: displayInputValue,
4135
+ onChange: handleInputChange,
4136
+ onKeyDown: handleKeyDown,
4137
+ onFocus: openPopover,
4138
+ onBlur: handleBlur,
4139
+ disabled: effectiveDisabled,
4140
+ readOnly: effectiveReadOnly,
4141
+ autoComplete: "off",
4142
+ className: cn(v.input(), themeSlotProps == null ? void 0 : (_themeSlotProps_input1 = themeSlotProps.input) == null ? void 0 : _themeSlotProps_input1.className, slotProps == null ? void 0 : (_slotProps_input1 = slotProps.input) == null ? void 0 : _slotProps_input1.className)
4143
+ }),
4144
+ hasAnySelection && !effectiveDisabled && !effectiveReadOnly && jsx("button", {
4145
+ type: "button",
4146
+ onMouseDown: preventBlur,
4147
+ onClick: handleClearClick,
4148
+ "aria-label": "Clear selection",
4149
+ tabIndex: -1,
4150
+ className: cn(v.clearButton(), themeSlotProps == null ? void 0 : (_themeSlotProps_clearButton1 = themeSlotProps.clearButton) == null ? void 0 : _themeSlotProps_clearButton1.className, slotProps == null ? void 0 : (_slotProps_clearButton1 = slotProps.clearButton) == null ? void 0 : _slotProps_clearButton1.className),
4151
+ children: jsx(CloseIcon, {})
4152
+ }),
4153
+ jsx("button", {
4154
+ type: "button",
4155
+ onMouseDown: preventBlur,
4156
+ onClick: handleTriggerClick,
4157
+ "aria-label": "Open",
4158
+ "aria-haspopup": "listbox",
4159
+ "aria-expanded": isOpen,
4160
+ tabIndex: -1,
4161
+ disabled: effectiveDisabled,
4162
+ className: cn(v.trigger(), themeSlotProps == null ? void 0 : (_themeSlotProps_trigger1 = themeSlotProps.trigger) == null ? void 0 : _themeSlotProps_trigger1.className, slotProps == null ? void 0 : (_slotProps_trigger1 = slotProps.trigger) == null ? void 0 : _slotProps_trigger1.className),
4163
+ children: jsx(ChevronDownIcon$1, {})
4164
+ })
4165
+ ]
4166
+ })
4167
+ ]
3825
4168
  })
3826
4169
  }),
3827
- jsx("input", {
3828
- ref: inputRef,
3829
- id: `${baseId}-input`,
3830
- name: name,
3831
- type: "text",
3832
- role: "combobox",
3833
- "aria-autocomplete": "list",
3834
- "aria-expanded": isOpen,
3835
- "aria-controls": isOpen ? listboxId : undefined,
3836
- "aria-activedescendant": activeOptionId,
3837
- "aria-labelledby": label ? labelId : undefined,
3838
- "aria-describedby": resolvedHelperText ? helperId : undefined,
3839
- "aria-invalid": resolvedError ? true : undefined,
3840
- "aria-required": required ? true : undefined,
3841
- placeholder: // In multi mode hide the placeholder once the user has at
3842
- // least one chip — the chips already convey "something is
3843
- // selected" and the placeholder would clutter the UI.
3844
- isMulti && selectedKeys.size > 0 ? undefined : placeholder,
3845
- value: displayInputValue,
3846
- onChange: handleInputChange,
3847
- onKeyDown: handleKeyDown,
3848
- onFocus: openPopover,
3849
- onBlur: handleBlur,
3850
- disabled: effectiveDisabled,
3851
- readOnly: effectiveReadOnly,
3852
- autoComplete: "off",
3853
- // Multi mode: give the input some min-width so it stays
3854
- // clickable even with many chips, and let it shrink/grow with
3855
- // the available row space.
3856
- className: cn(v.input(), isMulti && 'min-w-[6rem] flex-1 basis-24', themeSlotProps == null ? void 0 : (_themeSlotProps_input = themeSlotProps.input) == null ? void 0 : _themeSlotProps_input.className, slotProps == null ? void 0 : (_slotProps_input = slotProps.input) == null ? void 0 : _slotProps_input.className)
3857
- }),
3858
- hasAnySelection && !effectiveDisabled && !effectiveReadOnly && jsx("button", {
3859
- type: "button",
3860
- onMouseDown: preventBlur,
3861
- onClick: handleClearClick,
3862
- "aria-label": "Clear selection",
3863
- tabIndex: -1,
3864
- className: cn(v.clearButton(), themeSlotProps == null ? void 0 : (_themeSlotProps_clearButton = themeSlotProps.clearButton) == null ? void 0 : _themeSlotProps_clearButton.className, slotProps == null ? void 0 : (_slotProps_clearButton = slotProps.clearButton) == null ? void 0 : _slotProps_clearButton.className),
3865
- children: jsx(CloseIcon, {})
3866
- }),
3867
- jsx("button", {
3868
- type: "button",
3869
- onMouseDown: preventBlur,
3870
- onClick: handleTriggerClick,
3871
- "aria-label": "Open",
3872
- "aria-haspopup": "listbox",
3873
- "aria-expanded": isOpen,
3874
- tabIndex: -1,
3875
- disabled: effectiveDisabled,
3876
- className: cn(v.trigger(), themeSlotProps == null ? void 0 : (_themeSlotProps_trigger = themeSlotProps.trigger) == null ? void 0 : _themeSlotProps_trigger.className, slotProps == null ? void 0 : (_slotProps_trigger = slotProps.trigger) == null ? void 0 : _slotProps_trigger.className),
3877
- children: jsx(ChevronDownIcon$1, {})
3878
- }),
3879
- isOpen && jsx("ul", {
3880
- ref: listboxRef,
3881
- id: listboxId,
3882
- role: "listbox",
3883
- "aria-labelledby": label ? labelId : undefined,
3884
- className: cn(v.popover(), 'absolute left-0 right-0 top-full', v.listBox(), themeSlotProps == null ? void 0 : (_themeSlotProps_popover = themeSlotProps.popover) == null ? void 0 : _themeSlotProps_popover.className, slotProps == null ? void 0 : (_slotProps_popover = slotProps.popover) == null ? void 0 : _slotProps_popover.className, themeSlotProps == null ? void 0 : (_themeSlotProps_listBox = themeSlotProps.listBox) == null ? void 0 : _themeSlotProps_listBox.className, slotProps == null ? void 0 : (_slotProps_listBox = slotProps.listBox) == null ? void 0 : _slotProps_listBox.className),
3885
- children: isLoading ? jsx("li", {
3886
- className: cn(v.emptyState(), themeSlotProps == null ? void 0 : (_themeSlotProps_emptyState = themeSlotProps.emptyState) == null ? void 0 : _themeSlotProps_emptyState.className, slotProps == null ? void 0 : (_slotProps_emptyState = slotProps.emptyState) == null ? void 0 : _slotProps_emptyState.className),
3887
- "aria-live": "polite",
3888
- "aria-busy": "true",
3889
- children: loadingMessage
3890
- }) : filteredOptions.length === 0 ? jsx("li", {
3891
- className: cn(v.emptyState(), themeSlotProps == null ? void 0 : (_themeSlotProps_emptyState1 = themeSlotProps.emptyState) == null ? void 0 : _themeSlotProps_emptyState1.className, slotProps == null ? void 0 : (_slotProps_emptyState1 = slotProps.emptyState) == null ? void 0 : _slotProps_emptyState1.className),
3892
- children: emptyMessage
3893
- }) : filteredOptions.map((opt, idx)=>{
3894
- var _themeSlotProps_listItem, _slotProps_listItem;
3895
- const optValue = getOptionValue(opt);
3896
- const optDisabled = getOptionDisabled(opt);
3897
- const isHighlighted = idx === highlightedIndex;
3898
- const isSelected = selectedKeys.has(optValue);
3899
- return jsx("li", {
3900
- id: `${optionIdPrefix}${optValue}`,
3901
- role: "option",
3902
- "aria-selected": isSelected,
3903
- "aria-disabled": optDisabled || undefined,
3904
- "data-focused": isHighlighted || undefined,
3905
- "data-selected": isSelected || undefined,
3906
- "data-disabled": optDisabled || undefined,
3907
- onMouseDown: preventBlur,
3908
- onClick: ()=>handleOptionClick(opt),
3909
- onMouseEnter: ()=>setHighlightedIndex(idx),
3910
- className: cn(v.listItem(), themeSlotProps == null ? void 0 : (_themeSlotProps_listItem = themeSlotProps.listItem) == null ? void 0 : _themeSlotProps_listItem.className, slotProps == null ? void 0 : (_slotProps_listItem = slotProps.listItem) == null ? void 0 : _slotProps_listItem.className),
3911
- children: getOptionLabel(opt)
3912
- }, resolveOptionKey(opt));
4170
+ isOpen && jsx(RadixPopover.Portal, {
4171
+ children: jsx(RadixPopover.Content, {
4172
+ asChild: true,
4173
+ side: "bottom",
4174
+ align: "start",
4175
+ sideOffset: 4,
4176
+ collisionPadding: 8,
4177
+ // Anchor width → listbox width (Radix exposes this CSS var).
4178
+ style: {
4179
+ width: 'var(--radix-popover-trigger-width)'
4180
+ },
4181
+ // Focus stays on the combobox input; Radix must not steal it.
4182
+ onOpenAutoFocus: (e)=>e.preventDefault(),
4183
+ onCloseAutoFocus: (e)=>e.preventDefault(),
4184
+ /*
4185
+ THE COMBOBOX INPUT IS NOT "OUTSIDE".
4186
+
4187
+ In a combobox the focused element is the `<input>`, and the
4188
+ input is the ANCHOR — it lives outside `Popover.Content`.
4189
+ Radix's `DismissableLayer` does not know that: it sees a
4190
+ `focusin` on an element outside the layer and dismisses.
4191
+
4192
+ The symptom is that the listbox opens on focus and closes
4193
+ about 10ms later, so the FIRST keystroke after clicking the
4194
+ field never shows suggestions and the field looks broken.
4195
+ Later keystrokes work, because they raise no new focus
4196
+ event — which makes it look intermittent rather than
4197
+ systematic.
4198
+
4199
+ Both handlers guard on the component root, which contains
4200
+ the input, the chevron and the clear button. Anything
4201
+ genuinely outside still dismisses, so click-away is
4202
+ unaffected.
4203
+ */ onFocusOutside: (event)=>{
4204
+ if (isWithinCombobox(event.target, rootRef.current, listboxRef.current)) {
4205
+ event.preventDefault();
4206
+ }
4207
+ },
4208
+ onInteractOutside: (event)=>{
4209
+ if (isWithinCombobox(event.target, rootRef.current, listboxRef.current)) {
4210
+ event.preventDefault();
4211
+ }
4212
+ },
4213
+ children: jsx("ul", {
4214
+ ref: listboxRef,
4215
+ id: listboxId,
4216
+ role: "listbox",
4217
+ "aria-labelledby": label ? labelId : undefined,
4218
+ className: cn(v.popover(), v.listBox(), themeSlotProps == null ? void 0 : (_themeSlotProps_popover = themeSlotProps.popover) == null ? void 0 : _themeSlotProps_popover.className, slotProps == null ? void 0 : (_slotProps_popover = slotProps.popover) == null ? void 0 : _slotProps_popover.className, themeSlotProps == null ? void 0 : (_themeSlotProps_listBox = themeSlotProps.listBox) == null ? void 0 : _themeSlotProps_listBox.className, slotProps == null ? void 0 : (_slotProps_listBox = slotProps.listBox) == null ? void 0 : _slotProps_listBox.className),
4219
+ children: isLoading ? jsx("li", {
4220
+ className: cn(v.emptyState(), themeSlotProps == null ? void 0 : (_themeSlotProps_emptyState = themeSlotProps.emptyState) == null ? void 0 : _themeSlotProps_emptyState.className, slotProps == null ? void 0 : (_slotProps_emptyState = slotProps.emptyState) == null ? void 0 : _slotProps_emptyState.className),
4221
+ "aria-live": "polite",
4222
+ "aria-busy": "true",
4223
+ children: loadingMessage
4224
+ }) : filteredOptions.length === 0 ? jsx("li", {
4225
+ className: cn(v.emptyState(), themeSlotProps == null ? void 0 : (_themeSlotProps_emptyState1 = themeSlotProps.emptyState) == null ? void 0 : _themeSlotProps_emptyState1.className, slotProps == null ? void 0 : (_slotProps_emptyState1 = slotProps.emptyState) == null ? void 0 : _slotProps_emptyState1.className),
4226
+ children: emptyMessage
4227
+ }) : filteredOptions.map((opt, idx)=>{
4228
+ var _themeSlotProps_listItem, _slotProps_listItem;
4229
+ const optValue = getOptionValue(opt);
4230
+ const optDisabled = getOptionDisabled(opt);
4231
+ const isHighlighted = idx === highlightedIndex;
4232
+ const isSelected = selectedKeys.has(optValue);
4233
+ return jsx("li", {
4234
+ id: `${optionIdPrefix}${optValue}`,
4235
+ role: "option",
4236
+ "aria-selected": isSelected,
4237
+ "aria-disabled": optDisabled || undefined,
4238
+ "data-focused": isHighlighted || undefined,
4239
+ "data-selected": isSelected || undefined,
4240
+ "data-disabled": optDisabled || undefined,
4241
+ onMouseDown: preventBlur,
4242
+ onClick: ()=>handleOptionClick(opt),
4243
+ onMouseEnter: ()=>setHighlightedIndex(idx),
4244
+ className: cn(v.listItem(), themeSlotProps == null ? void 0 : (_themeSlotProps_listItem = themeSlotProps.listItem) == null ? void 0 : _themeSlotProps_listItem.className, slotProps == null ? void 0 : (_slotProps_listItem = slotProps.listItem) == null ? void 0 : _slotProps_listItem.className),
4245
+ children: getOptionLabel(opt)
4246
+ }, resolveOptionKey(opt));
4247
+ })
4248
+ })
3913
4249
  })
3914
4250
  })
3915
4251
  ]
@@ -4409,12 +4745,26 @@ function CalendarIcon$2() {
4409
4745
  return ()=>{
4410
4746
  isMountedRef.current = false;
4411
4747
  const { bridge: cap, name: capName } = unregisterRef.current;
4748
+ // BUG 22: only release bridge state on unmount if the form
4749
+ // is configured to forget unmounted fields. Default `false`
4750
+ // keeps values in RHF so <Stepper> / tab-swap patterns can
4751
+ // read earlier answers back on later steps. See README-BUG.md § BUG 22.
4752
+ if (!(cap == null ? void 0 : cap.shouldUnregister)) return;
4412
4753
  queueMicrotask(()=>{
4413
4754
  var _cap_unregister;
4414
4755
  if (!isMountedRef.current) cap == null ? void 0 : (_cap_unregister = cap.unregister) == null ? void 0 : _cap_unregister.call(cap, capName);
4415
4756
  });
4416
4757
  };
4417
4758
  }, []);
4759
+ // Dev-mode warning: if under <DashFormProvider> AND the consumer
4760
+ // passed controlled-mode props (silently ignored in form mode),
4761
+ // fire once per (component, name, prop). Zero cost in prod. See
4762
+ // BUG 2 in libs/dashforge/README-BUG.md.
4763
+ useWarnIfControlledInFormMode('DatePicker', name, {
4764
+ value: explicitValue,
4765
+ defaultValue,
4766
+ onChange
4767
+ });
4418
4768
  // ───── Derived ─────
4419
4769
  const effectiveDisabled = Boolean(disabled) || accessState.disabled;
4420
4770
  const effectiveReadOnly = accessState.readonly;
@@ -4683,6 +5033,11 @@ function ClockIcon() {
4683
5033
  return ()=>{
4684
5034
  isMountedRef.current = false;
4685
5035
  const { bridge: cap, name: capName } = unregisterRef.current;
5036
+ // BUG 22: only release bridge state on unmount if the form
5037
+ // is configured to forget unmounted fields. Default `false`
5038
+ // keeps values in RHF so <Stepper> / tab-swap patterns can
5039
+ // read earlier answers back on later steps. See README-BUG.md § BUG 22.
5040
+ if (!(cap == null ? void 0 : cap.shouldUnregister)) return;
4686
5041
  queueMicrotask(()=>{
4687
5042
  var _cap_unregister;
4688
5043
  if (!isMountedRef.current) cap == null ? void 0 : (_cap_unregister = cap.unregister) == null ? void 0 : _cap_unregister.call(cap, capName);
@@ -5020,6 +5375,11 @@ const DATETIME_PATTERN = /^(\d{4}-\d{2}-\d{2})T(\d{2}):(\d{2})/;
5020
5375
  return ()=>{
5021
5376
  isMountedRef.current = false;
5022
5377
  const { bridge: cap, name: capName } = unregisterRef.current;
5378
+ // BUG 22: only release bridge state on unmount if the form
5379
+ // is configured to forget unmounted fields. Default `false`
5380
+ // keeps values in RHF so <Stepper> / tab-swap patterns can
5381
+ // read earlier answers back on later steps. See README-BUG.md § BUG 22.
5382
+ if (!(cap == null ? void 0 : cap.shouldUnregister)) return;
5023
5383
  queueMicrotask(()=>{
5024
5384
  var _cap_unregister;
5025
5385
  if (!isMountedRef.current) cap == null ? void 0 : (_cap_unregister = cap.unregister) == null ? void 0 : _cap_unregister.call(cap, capName);
@@ -5584,6 +5944,11 @@ function formatRange(range, locale) {
5584
5944
  return ()=>{
5585
5945
  isMountedRef.current = false;
5586
5946
  const { bridge: cap, name: capName } = unregisterRef.current;
5947
+ // BUG 22: only release bridge state on unmount if the form
5948
+ // is configured to forget unmounted fields. Default `false`
5949
+ // keeps values in RHF so <Stepper> / tab-swap patterns can
5950
+ // read earlier answers back on later steps. See README-BUG.md § BUG 22.
5951
+ if (!(cap == null ? void 0 : cap.shouldUnregister)) return;
5587
5952
  queueMicrotask(()=>{
5588
5953
  var _cap_unregister;
5589
5954
  if (!isMountedRef.current) cap == null ? void 0 : (_cap_unregister = cap.unregister) == null ? void 0 : _cap_unregister.call(cap, capName);
@@ -9728,6 +10093,11 @@ const DEFAULT_STEP = 1;
9728
10093
  return ()=>{
9729
10094
  isMountedRef.current = false;
9730
10095
  const { bridge: cap, name: capName } = unregisterRef.current;
10096
+ // BUG 22: only release bridge state on unmount if the form
10097
+ // is configured to forget unmounted fields. Default `false`
10098
+ // keeps values in RHF so <Stepper> / tab-swap patterns can
10099
+ // read earlier answers back on later steps. See README-BUG.md § BUG 22.
10100
+ if (!(cap == null ? void 0 : cap.shouldUnregister)) return;
9731
10101
  queueMicrotask(()=>{
9732
10102
  var _cap_unregister;
9733
10103
  if (!isMountedRef.current) cap == null ? void 0 : (_cap_unregister = cap.unregister) == null ? void 0 : _cap_unregister.call(cap, capName);
@@ -10249,6 +10619,17 @@ Slider.displayName = 'Slider';
10249
10619
  const helperId = `${controlId}-help`;
10250
10620
  const isFormMode = Boolean(bridge == null ? void 0 : bridge.register);
10251
10621
  useStandaloneFieldWarning('Select', name, isFormMode, userValue, onChange);
10622
+ // Mirror of useStandaloneFieldWarning: warn when running under a
10623
+ // <DashFormProvider> AND the consumer passed controlled-mode props
10624
+ // that are silently ignored in form mode. See BUG 2 in
10625
+ // libs/dashforge/README-BUG.md — closes the case the type-side
10626
+ // discriminant cannot see (React context is invisible at compile
10627
+ // time). Zero cost in production.
10628
+ useWarnIfControlledInFormMode('Select', name, {
10629
+ value: userValue,
10630
+ defaultValue,
10631
+ onChange
10632
+ });
10252
10633
  // StrictMode-safe unregister
10253
10634
  const unregisterRef = useRef({
10254
10635
  bridge,
@@ -10264,6 +10645,11 @@ Slider.displayName = 'Slider';
10264
10645
  return ()=>{
10265
10646
  isMountedRef.current = false;
10266
10647
  const { bridge: cap, name: capName } = unregisterRef.current;
10648
+ // BUG 22: only release bridge state on unmount if the form
10649
+ // is configured to forget unmounted fields. Default `false`
10650
+ // keeps values in RHF so <Stepper> / tab-swap patterns can
10651
+ // read earlier answers back on later steps. See README-BUG.md § BUG 22.
10652
+ if (!(cap == null ? void 0 : cap.shouldUnregister)) return;
10267
10653
  queueMicrotask(()=>{
10268
10654
  var _cap_unregister;
10269
10655
  if (!isMountedRef.current) cap == null ? void 0 : (_cap_unregister = cap.unregister) == null ? void 0 : _cap_unregister.call(cap, capName);
@@ -15208,10 +15594,10 @@ VisuallyHidden.displayName = 'VisuallyHidden';
15208
15594
  * `useConfirm()` from `ConfirmDialog`). Use `<Dialog>` when you want
15209
15595
  * the control flow living in your component tree.
15210
15596
  */ function Dialog(props) {
15211
- var _themeSlotProps_overlay, _slotProps_overlay, _themeSlotProps_content, _slotProps_content, _themeSlotProps_title, _slotProps_title, _themeSlotProps_description, _slotProps_description, _themeSlotProps_body, _slotProps_body, _themeSlotProps_closeButton, _slotProps_closeButton;
15597
+ var _themeSlotProps_overlay, _slotProps_overlay, _themeSlotProps_content, _slotProps_content, _themeSlotProps_title, _slotProps_title, _themeSlotProps_description, _slotProps_description, _themeSlotProps_body, _slotProps_body, _themeSlotProps_actions, _slotProps_actions, _themeSlotProps_closeButton, _slotProps_closeButton;
15212
15598
  const themeDefaults = useComponentDefaults('Dialog');
15213
15599
  const merged = _extends({}, themeDefaults == null ? void 0 : themeDefaults.defaults, props);
15214
- const { open, onOpenChange, children, title, description, showCloseButton = true, disableBackdropClose = false, disableEscapeClose = false, size = 'md', sx, slotProps } = merged;
15600
+ const { open, onOpenChange, children, title, description, actions, showCloseButton = true, disableBackdropClose = false, disableEscapeClose = false, size = 'md', sx, slotProps } = merged;
15215
15601
  // Theme-level slotProps (Option C Track B). Theme classes go first,
15216
15602
  // instance classes second, so `tailwind-merge` picks the instance
15217
15603
  // where they collide.
@@ -15251,6 +15637,10 @@ VisuallyHidden.displayName = 'VisuallyHidden';
15251
15637
  className: cn(v.body(), themeSlotProps == null ? void 0 : (_themeSlotProps_body = themeSlotProps.body) == null ? void 0 : _themeSlotProps_body.className, slotProps == null ? void 0 : (_slotProps_body = slotProps.body) == null ? void 0 : _slotProps_body.className),
15252
15638
  children: children
15253
15639
  }),
15640
+ actions != null && jsx("div", {
15641
+ className: cn(v.actions(), themeSlotProps == null ? void 0 : (_themeSlotProps_actions = themeSlotProps.actions) == null ? void 0 : _themeSlotProps_actions.className, slotProps == null ? void 0 : (_slotProps_actions = slotProps.actions) == null ? void 0 : _slotProps_actions.className),
15642
+ children: actions
15643
+ }),
15254
15644
  showCloseButton && jsx(RadixDialog.Close, {
15255
15645
  "aria-label": "Close",
15256
15646
  className: cn(v.closeButton(), themeSlotProps == null ? void 0 : (_themeSlotProps_closeButton = themeSlotProps.closeButton) == null ? void 0 : _themeSlotProps_closeButton.className, slotProps == null ? void 0 : (_slotProps_closeButton = slotProps.closeButton) == null ? void 0 : _slotProps_closeButton.className),
@@ -15283,6 +15673,11 @@ const tabsVariants = tv({
15283
15673
  trigger: [
15284
15674
  'inline-flex items-center justify-center whitespace-nowrap',
15285
15675
  'text-sm font-medium transition-colors',
15676
+ // Radix Tabs.Trigger renders a `<button>`, whose UA cursor is
15677
+ // `default`. `disabled:pointer-events-none` neutralises it on
15678
+ // the disabled state (no pointer events → no cursor swap). See
15679
+ // README-BUG § BUG 13.
15680
+ 'cursor-pointer',
15286
15681
  'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500',
15287
15682
  'disabled:opacity-50 disabled:pointer-events-none',
15288
15683
  'motion-reduce:transition-none'