@lerianstudio/sindarian-ui 2.0.0-beta.1 → 2.0.0-beta.11

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 (140) hide show
  1. package/dist/components/breadcrumb/index.d.ts.map +1 -1
  2. package/dist/components/breadcrumb/index.js +25 -1
  3. package/dist/components/card/account-card/index.js +5 -5
  4. package/dist/components/card/basic-card/index.d.ts.map +1 -1
  5. package/dist/components/card/basic-card/index.js +2 -2
  6. package/dist/components/card/entity-card/index.js +1 -1
  7. package/dist/components/dialog/confirmation-dialog/index.d.ts.map +1 -1
  8. package/dist/components/dialog/confirmation-dialog/index.js +4 -1
  9. package/dist/components/entity-box/index.d.ts +18 -2
  10. package/dist/components/entity-box/index.d.ts.map +1 -1
  11. package/dist/components/entity-box/index.js +32 -4
  12. package/dist/components/form/file-upload-field/index.d.ts +11 -2
  13. package/dist/components/form/file-upload-field/index.d.ts.map +1 -1
  14. package/dist/components/form/file-upload-field/index.js +8 -3
  15. package/dist/components/form/input-field/index.d.ts +41 -2
  16. package/dist/components/form/input-field/index.d.ts.map +1 -1
  17. package/dist/components/form/input-field/index.js +73 -15
  18. package/dist/components/form/password-field/index.d.ts +5 -1
  19. package/dist/components/form/password-field/index.d.ts.map +1 -1
  20. package/dist/components/form/password-field/index.js +2 -2
  21. package/dist/components/form/select-field/index.d.ts +9 -1
  22. package/dist/components/form/select-field/index.d.ts.map +1 -1
  23. package/dist/components/form/select-field/index.js +2 -2
  24. package/dist/components/page-header/index.js +1 -1
  25. package/dist/components/table/id-table-cell.d.ts +5 -1
  26. package/dist/components/table/id-table-cell.d.ts.map +1 -1
  27. package/dist/components/table/id-table-cell.js +48 -6
  28. package/dist/components/ui/autocomplete/index.d.ts +24 -0
  29. package/dist/components/ui/autocomplete/index.d.ts.map +1 -1
  30. package/dist/components/ui/autocomplete/index.js +24 -0
  31. package/dist/components/ui/badge/index.d.ts +11 -7
  32. package/dist/components/ui/badge/index.d.ts.map +1 -1
  33. package/dist/components/ui/badge/index.js +20 -8
  34. package/dist/components/ui/button/styles.css +31 -1
  35. package/dist/components/ui/calendar/index.d.ts.map +1 -1
  36. package/dist/components/ui/calendar/index.js +7 -1
  37. package/dist/components/ui/copy-field/index.d.ts.map +1 -1
  38. package/dist/components/ui/copy-field/index.js +1 -1
  39. package/dist/components/ui/file-upload/index.d.ts +235 -9
  40. package/dist/components/ui/file-upload/index.d.ts.map +1 -1
  41. package/dist/components/ui/file-upload/index.js +354 -17
  42. package/dist/components/ui/form.d.ts +4 -0
  43. package/dist/components/ui/form.d.ts.map +1 -1
  44. package/dist/components/ui/form.js +48 -6
  45. package/dist/components/ui/input/styles.css +75 -3
  46. package/dist/components/ui/sheet/index.d.ts.map +1 -1
  47. package/dist/components/ui/sheet/index.js +2 -2
  48. package/dist/components/ui/sidebar/index.d.ts +1 -0
  49. package/dist/components/ui/sidebar/index.d.ts.map +1 -1
  50. package/dist/components/ui/sidebar/index.js +1 -0
  51. package/dist/components/ui/sidebar/sidebar-expand-button.d.ts +1 -1
  52. package/dist/components/ui/sidebar/sidebar-expand-button.d.ts.map +1 -1
  53. package/dist/components/ui/sidebar/sidebar-expand-button.js +15 -1
  54. package/dist/components/ui/sidebar/sidebar-item.d.ts +1 -1
  55. package/dist/components/ui/sidebar/sidebar-item.d.ts.map +1 -1
  56. package/dist/components/ui/sidebar/sidebar-item.js +28 -4
  57. package/dist/components/ui/sidebar/sidebar-provider.d.ts +37 -0
  58. package/dist/components/ui/sidebar/sidebar-provider.d.ts.map +1 -1
  59. package/dist/components/ui/sidebar/sidebar-provider.js +125 -4
  60. package/dist/components/ui/sidebar/sidebar-root.d.ts +38 -1
  61. package/dist/components/ui/sidebar/sidebar-root.d.ts.map +1 -1
  62. package/dist/components/ui/sidebar/sidebar-root.js +100 -5
  63. package/dist/components/ui/sidebar/sidebar-trigger.d.ts +18 -0
  64. package/dist/components/ui/sidebar/sidebar-trigger.d.ts.map +1 -0
  65. package/dist/components/ui/sidebar/sidebar-trigger.js +30 -0
  66. package/dist/components/ui/toggle-group/index.d.ts +1 -1
  67. package/dist/components/ui/toggle-group/index.d.ts.map +1 -1
  68. package/dist/components/ui/toggle-group/index.js +73 -4
  69. package/dist/enterprise/number-input/index.d.ts.map +1 -1
  70. package/dist/enterprise/number-input/index.js +1 -1
  71. package/dist/esm/components/breadcrumb/index.d.ts.map +1 -1
  72. package/dist/esm/components/breadcrumb/index.js +25 -1
  73. package/dist/esm/components/card/account-card/index.js +5 -5
  74. package/dist/esm/components/card/basic-card/index.d.ts.map +1 -1
  75. package/dist/esm/components/card/basic-card/index.js +2 -2
  76. package/dist/esm/components/card/entity-card/index.js +1 -1
  77. package/dist/esm/components/dialog/confirmation-dialog/index.d.ts.map +1 -1
  78. package/dist/esm/components/dialog/confirmation-dialog/index.js +4 -1
  79. package/dist/esm/components/entity-box/index.d.ts +18 -2
  80. package/dist/esm/components/entity-box/index.d.ts.map +1 -1
  81. package/dist/esm/components/entity-box/index.js +29 -4
  82. package/dist/esm/components/form/file-upload-field/index.d.ts +11 -2
  83. package/dist/esm/components/form/file-upload-field/index.d.ts.map +1 -1
  84. package/dist/esm/components/form/file-upload-field/index.js +8 -3
  85. package/dist/esm/components/form/input-field/index.d.ts +41 -2
  86. package/dist/esm/components/form/input-field/index.d.ts.map +1 -1
  87. package/dist/esm/components/form/input-field/index.js +73 -15
  88. package/dist/esm/components/form/password-field/index.d.ts +5 -1
  89. package/dist/esm/components/form/password-field/index.d.ts.map +1 -1
  90. package/dist/esm/components/form/password-field/index.js +2 -2
  91. package/dist/esm/components/form/select-field/index.d.ts +9 -1
  92. package/dist/esm/components/form/select-field/index.d.ts.map +1 -1
  93. package/dist/esm/components/form/select-field/index.js +2 -2
  94. package/dist/esm/components/page-header/index.js +1 -1
  95. package/dist/esm/components/table/id-table-cell.d.ts +5 -1
  96. package/dist/esm/components/table/id-table-cell.d.ts.map +1 -1
  97. package/dist/esm/components/table/id-table-cell.js +48 -3
  98. package/dist/esm/components/ui/autocomplete/index.d.ts +24 -0
  99. package/dist/esm/components/ui/autocomplete/index.d.ts.map +1 -1
  100. package/dist/esm/components/ui/autocomplete/index.js +24 -0
  101. package/dist/esm/components/ui/badge/index.d.ts +11 -7
  102. package/dist/esm/components/ui/badge/index.d.ts.map +1 -1
  103. package/dist/esm/components/ui/badge/index.js +20 -8
  104. package/dist/esm/components/ui/calendar/index.d.ts.map +1 -1
  105. package/dist/esm/components/ui/calendar/index.js +8 -2
  106. package/dist/esm/components/ui/copy-field/index.d.ts.map +1 -1
  107. package/dist/esm/components/ui/copy-field/index.js +1 -1
  108. package/dist/esm/components/ui/file-upload/index.d.ts +235 -9
  109. package/dist/esm/components/ui/file-upload/index.d.ts.map +1 -1
  110. package/dist/esm/components/ui/file-upload/index.js +353 -17
  111. package/dist/esm/components/ui/form.d.ts +4 -0
  112. package/dist/esm/components/ui/form.d.ts.map +1 -1
  113. package/dist/esm/components/ui/form.js +48 -6
  114. package/dist/esm/components/ui/sheet/index.d.ts.map +1 -1
  115. package/dist/esm/components/ui/sheet/index.js +2 -2
  116. package/dist/esm/components/ui/sidebar/index.d.ts +1 -0
  117. package/dist/esm/components/ui/sidebar/index.d.ts.map +1 -1
  118. package/dist/esm/components/ui/sidebar/index.js +1 -0
  119. package/dist/esm/components/ui/sidebar/sidebar-expand-button.d.ts +1 -1
  120. package/dist/esm/components/ui/sidebar/sidebar-expand-button.d.ts.map +1 -1
  121. package/dist/esm/components/ui/sidebar/sidebar-expand-button.js +15 -1
  122. package/dist/esm/components/ui/sidebar/sidebar-item.d.ts +1 -1
  123. package/dist/esm/components/ui/sidebar/sidebar-item.d.ts.map +1 -1
  124. package/dist/esm/components/ui/sidebar/sidebar-item.js +28 -4
  125. package/dist/esm/components/ui/sidebar/sidebar-provider.d.ts +37 -0
  126. package/dist/esm/components/ui/sidebar/sidebar-provider.d.ts.map +1 -1
  127. package/dist/esm/components/ui/sidebar/sidebar-provider.js +122 -1
  128. package/dist/esm/components/ui/sidebar/sidebar-root.d.ts +38 -1
  129. package/dist/esm/components/ui/sidebar/sidebar-root.d.ts.map +1 -1
  130. package/dist/esm/components/ui/sidebar/sidebar-root.js +102 -7
  131. package/dist/esm/components/ui/sidebar/sidebar-trigger.d.ts +18 -0
  132. package/dist/esm/components/ui/sidebar/sidebar-trigger.d.ts.map +1 -0
  133. package/dist/esm/components/ui/sidebar/sidebar-trigger.js +26 -0
  134. package/dist/esm/components/ui/toggle-group/index.d.ts +1 -1
  135. package/dist/esm/components/ui/toggle-group/index.d.ts.map +1 -1
  136. package/dist/esm/components/ui/toggle-group/index.js +73 -4
  137. package/dist/esm/enterprise/number-input/index.d.ts.map +1 -1
  138. package/dist/esm/enterprise/number-input/index.js +1 -1
  139. package/dist/globals.css +77 -31
  140. package/package.json +5 -3
@@ -7,7 +7,21 @@ import { SidebarFooter } from './sidebar-components.js';
7
7
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../../ui/tooltip/index.js';
8
8
  import { IconButton } from '../icon-button/index.js';
9
9
  export const SidebarExpandButton = ({ tooltip }) => {
10
- const { isCollapsed, toggleSidebar } = useSidebar();
10
+ const { isCollapsed, isDrawer, toggleSidebar } = useSidebar();
11
+ /**
12
+ * Nothing to collapse in a drawer. The expanded branch below hangs its button
13
+ * at `right-[-20px]`, twenty pixels outside the rail — off the canvas of an
14
+ * overlay that has no page beside it — and collapsing to a 72px strip inside
15
+ * a 244px sheet is not a state worth reaching. The drawer's own close control
16
+ * and the backdrop are how it is dismissed.
17
+ *
18
+ * ⚠️ `isDrawer`, NOT `isMobile`. A consumer left on the default
19
+ * `mobile="inline"` still has a real rail below 768px, and its collapse
20
+ * control has to come with it.
21
+ */
22
+ if (isDrawer) {
23
+ return null;
24
+ }
11
25
  return (_jsxs(React.Fragment, { children: [!isCollapsed && (_jsx("div", { "data-slot": "sidebar-expand-button", className: "border-border bg-card flex w-full", children: _jsx("div", { className: "absolute right-[-20px] bottom-4", children: _jsx(IconButton, { variant: "secondary", rounded: true, onClick: toggleSidebar, "aria-label": tooltip || 'Collapse sidebar', "aria-expanded": true, children: _jsx(PanelLeftClose, { className: "text-muted-foreground" }) }) }) })), isCollapsed && (_jsx(SidebarFooter, { children: _jsx(TooltipProvider, { children: _jsxs(Tooltip, { children: [_jsx(TooltipTrigger
12
26
  // `hover:text-accent-foreground` on the TRIGGER, not only on
13
27
  // the glyph below. The base ink here used to be base/400, which
@@ -9,5 +9,5 @@ export type SidebarItemProps = React.ComponentProps<typeof SidebarItemButton> &
9
9
  disabled?: boolean;
10
10
  children?: React.ReactNode;
11
11
  };
12
- export declare const SidebarItem: ({ active, href, children: _children, ...props }: SidebarItemProps) => React.JSX.Element;
12
+ export declare const SidebarItem: ({ active, href, children: _children, onClick, ...props }: SidebarItemProps) => React.JSX.Element;
13
13
  //# sourceMappingURL=sidebar-item.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sidebar-item.d.ts","sourceRoot":"","sources":["../../../../../src/components/ui/sidebar/sidebar-item.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,MAAM,OAAO,CAAA;AACzB,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAA;AACzD,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAA;AAIlE,MAAM,MAAM,gBAAgB,GAAG,KAAK,CAAC,cAAc,CAAC,OAAO,iBAAiB,CAAC,GAC3E,KAAK,CAAC,cAAc,CAAC,OAAO,qBAAqB,CAAC,GAAG;IACnD,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,KAAK,CAAC,SAAS,CAAA;IACrB,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CAC3B,CAAA;AAEH,eAAO,MAAM,WAAW,GAAI,iDAKzB,gBAAgB,sBA0BlB,CAAA"}
1
+ {"version":3,"file":"sidebar-item.d.ts","sourceRoot":"","sources":["../../../../../src/components/ui/sidebar/sidebar-item.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,MAAM,OAAO,CAAA;AACzB,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAA;AACzD,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAA;AAIlE,MAAM,MAAM,gBAAgB,GAAG,KAAK,CAAC,cAAc,CAAC,OAAO,iBAAiB,CAAC,GAC3E,KAAK,CAAC,cAAc,CAAC,OAAO,qBAAqB,CAAC,GAAG;IACnD,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,KAAK,CAAC,SAAS,CAAA;IACrB,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CAC3B,CAAA;AAEH,eAAO,MAAM,WAAW,GAAI,0DAMzB,gBAAgB,sBAsDlB,CAAA"}
@@ -4,13 +4,37 @@ import { SidebarItemButton } from './sidebar-item-button.js';
4
4
  import { SidebarItemIconButton } from './sidebar-item-icon-button.js';
5
5
  import { useSidebar } from './sidebar-provider.js';
6
6
  import { useSidebarRouter } from './sidebar-router.js';
7
- export const SidebarItem = ({ active, href, children: _children, ...props }) => {
7
+ export const SidebarItem = ({ active, href, children: _children, onClick, ...props }) => {
8
8
  const { usePathname } = useSidebarRouter();
9
9
  const pathname = usePathname();
10
- const { isCollapsed } = useSidebar();
10
+ const { isCollapsed, isDrawer, setOpenMobile } = useSidebar();
11
11
  const isActive = (href) => pathname === href;
12
+ /**
13
+ * ⛔ ACTIVATING A DESTINATION CLOSES THE DRAWER. It is the whole point of the
14
+ * drawer and it was the one interaction that left it open: Escape, the
15
+ * backdrop and the X all dismissed it, a navigation link did not.
16
+ *
17
+ * The overlay is modal, so `document.body` carries `pointer-events: none`
18
+ * while it is up, and a consumer's `SidebarItem` is a client-side `Link`
19
+ * under a provider that survives the route change — so the reader landed on
20
+ * the new page rendered INERT behind a drawer they had to dismiss a second
21
+ * time, on a phone.
22
+ *
23
+ * Here rather than in `SidebarRoot`, because this is the component that knows
24
+ * an activation happened; `SidebarBackButton` and every
25
+ * `SidebarItemCollapsibleContent` leaf render through it, so they are covered
26
+ * by construction. Guarded on `isDrawer` so an inline rail — where nothing is
27
+ * covering the page — behaves exactly as before, and the consumer's own
28
+ * handler still runs either way.
29
+ */
30
+ const handleClick = (event) => {
31
+ onClick?.(event);
32
+ if (isDrawer) {
33
+ setOpenMobile(false);
34
+ }
35
+ };
12
36
  if (isCollapsed) {
13
- return (_jsx(SidebarItemIconButton, { "data-slot": "sidebar-item", href: href, active: isActive(href) || active, ...props }));
37
+ return (_jsx(SidebarItemIconButton, { "data-slot": "sidebar-item", href: href, active: isActive(href) || active, onClick: handleClick, ...props }));
14
38
  }
15
- return (_jsx(SidebarItemButton, { "data-slot": "sidebar-item", href: href, active: isActive(href) || active, ...props }));
39
+ return (_jsx(SidebarItemButton, { "data-slot": "sidebar-item", href: href, active: isActive(href) || active, onClick: handleClick, ...props }));
16
40
  };
@@ -1,12 +1,49 @@
1
1
  import React from 'react';
2
+ /**
3
+ * The viewport below which the rail stops being a layout and becomes a drawer.
4
+ *
5
+ * 767px rather than 768px: this is the upper bound of `max-width`, so it is the
6
+ * complement of Tailwind's `md` breakpoint (`min-width: 768px`) and `md:hidden`
7
+ * on `SidebarTrigger` flips at exactly the same pixel the query does.
8
+ */
9
+ export declare const SIDEBAR_MOBILE_QUERY = "(max-width: 767px)";
2
10
  export type SidebarContextProps = {
3
11
  isCollapsed: boolean;
12
+ /**
13
+ * True only inside a drawer that `SidebarRoot` is actually rendering, which
14
+ * needs `mobile="drawer"` as well as a narrow viewport. `SidebarRoot`
15
+ * re-provides the context for that subtree; nothing else sets it.
16
+ */
17
+ isDrawer: boolean;
18
+ /**
19
+ * True while the viewport is narrow enough for a drawer. Reported in BOTH
20
+ * modes, so a consumer still on `mobile="inline"` can build its own
21
+ * responsive behaviour from it.
22
+ */
23
+ isMobile: boolean;
24
+ /** Whether the mobile drawer is showing. Meaningless while `isMobile` is false. */
25
+ openMobile: boolean;
26
+ setOpenMobile: (open: boolean) => void;
27
+ /**
28
+ * Puts focus back on whatever opened the drawer. `SidebarRoot` calls it when
29
+ * the drawer closes; nothing else should need it.
30
+ */
31
+ restoreDrawerFocus: () => void;
32
+ /** The drawer's DOM id, so `SidebarTrigger` can point `aria-controls` at it. */
33
+ sidebarId: string;
4
34
  items: Record<string, boolean>;
5
35
  setItems: React.Dispatch<React.SetStateAction<Record<string, boolean>>>;
6
36
  getItemCollapsed: (key: string) => boolean;
7
37
  setItemCollapsed: (key: string, value: boolean) => void;
8
38
  toggleSidebar: () => void;
9
39
  };
40
+ /**
41
+ * Exported so `SidebarRoot` can re-provide it for the drawer's subtree. That
42
+ * subtree is the only place `isCollapsed` and `isDrawer` differ from the
43
+ * provider's own values, and scoping the override to it is what keeps a
44
+ * consumer on `mobile="inline"` seeing exactly today's rail.
45
+ */
46
+ export declare const SidebarContext: React.Context<SidebarContextProps | undefined>;
10
47
  export declare const useSidebar: () => SidebarContextProps;
11
48
  export declare const SidebarProvider: ({ children }: React.PropsWithChildren) => React.JSX.Element;
12
49
  //# sourceMappingURL=sidebar-provider.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sidebar-provider.d.ts","sourceRoot":"","sources":["../../../../../src/components/ui/sidebar/sidebar-provider.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,MAAM,OAAO,CAAA;AAGzB,MAAM,MAAM,mBAAmB,GAAG;IAChC,WAAW,EAAE,OAAO,CAAA;IACpB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B,QAAQ,EAAE,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,cAAc,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,CAAA;IACvE,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAA;IAC1C,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;IACvD,aAAa,EAAE,MAAM,IAAI,CAAA;CAC1B,CAAA;AAMD,eAAO,MAAM,UAAU,2BAMtB,CAAA;AAED,eAAO,MAAM,eAAe,GAAI,cAAc,KAAK,CAAC,iBAAiB,sBAqDpE,CAAA"}
1
+ {"version":3,"file":"sidebar-provider.d.ts","sourceRoot":"","sources":["../../../../../src/components/ui/sidebar/sidebar-provider.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,MAAM,OAAO,CAAA;AAGzB;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,uBAAuB,CAAA;AAUxD,MAAM,MAAM,mBAAmB,GAAG;IAChC,WAAW,EAAE,OAAO,CAAA;IACpB;;;;OAIG;IACH,QAAQ,EAAE,OAAO,CAAA;IACjB;;;;OAIG;IACH,QAAQ,EAAE,OAAO,CAAA;IACjB,mFAAmF;IACnF,UAAU,EAAE,OAAO,CAAA;IACnB,aAAa,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACtC;;;OAGG;IACH,kBAAkB,EAAE,MAAM,IAAI,CAAA;IAC9B,gFAAgF;IAChF,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B,QAAQ,EAAE,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,cAAc,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,CAAA;IACvE,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAA;IAC1C,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;IACvD,aAAa,EAAE,MAAM,IAAI,CAAA;CAC1B,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,cAAc,gDAEf,CAAA;AAEZ,eAAO,MAAM,UAAU,2BAMtB,CAAA;AAED,eAAO,MAAM,eAAe,GAAI,cAAc,KAAK,CAAC,iBAAiB,sBAkKpE,CAAA"}
@@ -2,7 +2,28 @@
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import React from 'react';
4
4
  import { getStorage, getStorageObject } from '../../../lib/storage/index.js';
5
- const SidebarContext = React.createContext(undefined);
5
+ /**
6
+ * The viewport below which the rail stops being a layout and becomes a drawer.
7
+ *
8
+ * 767px rather than 768px: this is the upper bound of `max-width`, so it is the
9
+ * complement of Tailwind's `md` breakpoint (`min-width: 768px`) and `md:hidden`
10
+ * on `SidebarTrigger` flips at exactly the same pixel the query does.
11
+ */
12
+ export const SIDEBAR_MOBILE_QUERY = '(max-width: 767px)';
13
+ /**
14
+ * Where focus goes when the rail has to receive it. The rail's own content is
15
+ * links and buttons, so this is deliberately the short list rather than the
16
+ * full tabbable grammar: a `[tabindex]` sweep would have to exclude `-1`, and
17
+ * nothing in a sidebar carries one.
18
+ */
19
+ const FOCUSABLE = 'a[href], button:not([disabled])';
20
+ /**
21
+ * Exported so `SidebarRoot` can re-provide it for the drawer's subtree. That
22
+ * subtree is the only place `isCollapsed` and `isDrawer` differ from the
23
+ * provider's own values, and scoping the override to it is what keeps a
24
+ * consumer on `mobile="inline"` seeing exactly today's rail.
25
+ */
26
+ export const SidebarContext = React.createContext(undefined);
6
27
  export const useSidebar = () => {
7
28
  const context = React.useContext(SidebarContext);
8
29
  if (!context) {
@@ -14,6 +35,61 @@ export const SidebarProvider = ({ children }) => {
14
35
  const [hydrated, setHydrated] = React.useState(false);
15
36
  const [collapsed, setCollapsed] = React.useState(false);
16
37
  const [items, _setItems] = React.useState({});
38
+ const [isMobile, setIsMobile] = React.useState(false);
39
+ const [openMobile, _setOpenMobile] = React.useState(false);
40
+ const sidebarId = React.useId();
41
+ /**
42
+ * ⛔ RADIX CANNOT RESTORE THIS FOCUS BY ITSELF. `DialogContent` preventDefaults
43
+ * the focus scope's own restore and focuses `triggerRef.current` instead —
44
+ * the ref a `SheetTrigger` would have filled. The drawer is opened from this
45
+ * state rather than from a trigger inside the dialog, so that ref is null and
46
+ * closing dropped focus onto `<body>`: a keyboard reader who opened the
47
+ * navigation and pressed Escape was returned to the top of the document.
48
+ *
49
+ * Captured here rather than in `SidebarTrigger` so that a consumer opening
50
+ * the drawer from its own control gets the same behaviour.
51
+ */
52
+ const opener = React.useRef(null);
53
+ const setOpenMobile = React.useCallback((open) => {
54
+ if (open && typeof document !== 'undefined') {
55
+ opener.current = document.activeElement;
56
+ }
57
+ _setOpenMobile(open);
58
+ }, []);
59
+ /**
60
+ * ⛔ THE VIEWPORT AT THE MOMENT FOCUS MOVES, NOT AT THE MOMENT IT OPENED.
61
+ *
62
+ * Radix calls `restoreDrawerFocus` from the focus scope's UNMOUNT handler,
63
+ * which is the last thing in the commit to touch focus — later than any
64
+ * effect in this provider, so the decision has to be made inside the handler
65
+ * rather than beside it. By then `isMobile` has already flipped.
66
+ */
67
+ const isMobileRef = React.useRef(false);
68
+ isMobileRef.current = isMobile;
69
+ const restoreDrawerFocus = React.useCallback(() => {
70
+ // `isConnected`: the control that opened the drawer may itself have been
71
+ // unmounted by whatever the reader did inside it.
72
+ //
73
+ // ⛔ AND NOT AT ALL WHEN THE VIEWPORT IS THE ONE THAT DISMISSED IT. A
74
+ // tablet rotated from portrait to landscape with the navigation open
75
+ // crosses 768px, so the drawer is replaced by the rail and the opener is
76
+ // `SidebarTrigger` — which the same breakpoint just hid with `md:hidden`.
77
+ // `.focus()` on a `display: none` element is a silent no-op, and Radix's
78
+ // own restore is already `preventDefault()`ed, so focus fell to `<body>`
79
+ // and the reader lost their place (SC 2.4.3). Measured in Chromium at
80
+ // 390 → 1280: `{"rails":1,"triggerDisplay":"none","focus":"BODY:"}`.
81
+ if (isMobileRef.current && opener.current?.isConnected) {
82
+ opener.current.focus();
83
+ return;
84
+ }
85
+ // The rail is what the drawer became, so that is where the reader goes.
86
+ // Below the breakpoint there is nothing here to find — a closed drawer
87
+ // leaves no navigation in the DOM — and focus stays where Radix left it.
88
+ document
89
+ .getElementById(sidebarId)
90
+ ?.querySelector(FOCUSABLE)
91
+ ?.focus();
92
+ }, [sidebarId]);
17
93
  const toggleSidebar = () => setCollapsed((collapsed) => !collapsed);
18
94
  const getItemCollapsed = (key) => {
19
95
  return items[key] || false;
@@ -27,6 +103,37 @@ export const SidebarProvider = ({ children }) => {
27
103
  _setItems(getStorageObject('sidebar-items', {}));
28
104
  setHydrated(true);
29
105
  }, []);
106
+ /**
107
+ * ⚠️ FALSE UNTIL THE CLIENT SAYS OTHERWISE. There is no viewport on the
108
+ * server, so the first paint is always the rail and the drawer appears on
109
+ * hydration; starting from a guess would make the markup differ between the
110
+ * two renders. The `matchMedia` guard is the one `theme-provider.tsx` next
111
+ * door already carries: jsdom and some embedded runtimes ship without it, and
112
+ * a missing media API is a desktop, not a crash.
113
+ */
114
+ React.useEffect(() => {
115
+ if (typeof window.matchMedia !== 'function') {
116
+ return;
117
+ }
118
+ const media = window.matchMedia(SIDEBAR_MOBILE_QUERY);
119
+ const sync = () => setIsMobile(media.matches);
120
+ sync();
121
+ media.addEventListener('change', sync);
122
+ return () => media.removeEventListener('change', sync);
123
+ }, []);
124
+ /**
125
+ * A drawer left open while the viewport grows back would keep an overlay and
126
+ * a focus trap over a layout that is already showing its rail, with nothing
127
+ * on screen to dismiss them.
128
+ *
129
+ * Where focus goes afterwards is `restoreDrawerFocus`'s decision, not this
130
+ * effect's: Radix's unmount handler runs later than anything here.
131
+ */
132
+ React.useEffect(() => {
133
+ if (!isMobile) {
134
+ _setOpenMobile(false);
135
+ }
136
+ }, [isMobile]);
30
137
  // Only persist to localStorage after hydration to avoid overwriting stored values
31
138
  React.useEffect(() => {
32
139
  if (!hydrated) {
@@ -41,7 +148,21 @@ export const SidebarProvider = ({ children }) => {
41
148
  localStorage.setItem('sidebar-items', JSON.stringify(items));
42
149
  }, [items, hydrated]);
43
150
  return (_jsx(SidebarContext.Provider, { value: {
151
+ /**
152
+ * ⚠️ A NARROW VIEWPORT DOES NOT TOUCH THIS. It used to be forced false
153
+ * whenever `isMobile`, which silently changed the rail for every
154
+ * consumer — including the ones that never asked for a drawer.
155
+ * `SidebarRoot` overrides it for the drawer's own subtree instead, so
156
+ * the stored preference reaches an inline rail unchanged at every
157
+ * viewport.
158
+ */
44
159
  isCollapsed: collapsed,
160
+ isDrawer: false,
161
+ isMobile,
162
+ openMobile,
163
+ setOpenMobile,
164
+ restoreDrawerFocus,
165
+ sidebarId,
45
166
  items,
46
167
  setItems: _setItems,
47
168
  getItemCollapsed,
@@ -1,3 +1,40 @@
1
1
  import React from 'react';
2
- export declare const SidebarRoot: ({ className, ...props }: React.ComponentProps<"nav">) => React.JSX.Element;
2
+ export type SidebarRootProps = React.ComponentProps<'nav'> & {
3
+ /**
4
+ * What this rail becomes below 768px.
5
+ *
6
+ * - `'inline'` (default): nothing changes. The rail renders at every
7
+ * viewport exactly as it always has, and the reader's collapsed preference
8
+ * is untouched.
9
+ * - `'drawer'`: the inline rail is not rendered below the breakpoint and the
10
+ * navigation moves into an overlay drawer that `SidebarTrigger` opens.
11
+ *
12
+ * ⛔ THE DEFAULT IS NOT A TASTE, IT IS A CONSUMER-SAFETY RULE. `SidebarTrigger`
13
+ * is the only way back into the drawer and it must render inside
14
+ * `SidebarProvider`. A consumer that has not placed one yet — which is every
15
+ * consumer at the moment this shipped — would get a phone with NO navigation
16
+ * rather than a rail that takes 63% of it, and that is strictly worse. Opt in
17
+ * once there is somewhere to put the trigger.
18
+ */
19
+ mobile?: 'inline' | 'drawer';
20
+ /**
21
+ * The drawer's accessible name. A dialog needs one, and it is read out when
22
+ * the drawer opens. Unused while `mobile` is `'inline'`.
23
+ */
24
+ mobileTitle?: string;
25
+ };
26
+ /**
27
+ * ⛔ BELOW 768px A RAIL IS NOT A LAYOUT — once a consumer opts in.
28
+ *
29
+ * The widths above are a fixed share of a flex row, so at 390px the expanded
30
+ * rail took 63% of the screen and left the page 146px, and the only affordance
31
+ * — `SidebarExpandButton` — shrinks it to 72px, which is still 18% and still
32
+ * permanently on screen. There was nothing that removed it.
33
+ *
34
+ * With `mobile="drawer"` the rail is replaced below the breakpoint by an
35
+ * overlay drawer. Escape, the backdrop click, the focus trap and returning
36
+ * focus to whatever opened it are Radix Dialog's, through the kit's own
37
+ * `Sheet`; none of that is re-implemented here.
38
+ */
39
+ export declare const SidebarRoot: ({ className, id, mobile, mobileTitle, ...props }: SidebarRootProps) => React.JSX.Element;
3
40
  //# sourceMappingURL=sidebar-root.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sidebar-root.d.ts","sourceRoot":"","sources":["../../../../../src/components/ui/sidebar/sidebar-root.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,MAAM,OAAO,CAAA;AAoBzB,eAAO,MAAM,WAAW,GAAI,yBAGzB,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,sBAW7B,CAAA"}
1
+ {"version":3,"file":"sidebar-root.d.ts","sourceRoot":"","sources":["../../../../../src/components/ui/sidebar/sidebar-root.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,MAAM,OAAO,CAAA;AAqEzB,MAAM,MAAM,gBAAgB,GAAG,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,GAAG;IAC3D;;;;;;;;;;;;;;;OAeG;IACH,MAAM,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAA;IAC5B;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,CAAA;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,WAAW,GAAI,kDAMzB,gBAAgB,sBA0GlB,CAAA"}
@@ -1,20 +1,115 @@
1
1
  'use client';
2
- import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { cva } from 'class-variance-authority';
4
- import { useSidebar } from './sidebar-provider.js';
4
+ import { SidebarContext, useSidebar } from './sidebar-provider.js';
5
5
  import { cn } from '../../../lib/utils.js';
6
+ import { Sheet, SheetContent, SheetTitle } from '../sheet/index.js';
7
+ /**
8
+ * ⛔ THE WIDTHS ARE TOKENS, NOT LITERALS.
9
+ *
10
+ * They were `w-[72px]` and `w-[244px]`, which a consumer could only override by
11
+ * winning a specificity argument with `cn`. `--sidebar-width` and
12
+ * `--sidebar-width-collapsed` are declared in `globals.css`, so the default
13
+ * still ships with the stylesheet, and a consumer re-points either one with
14
+ * `className="[--sidebar-width:280px]"` ON THE RAIL, which moves the rail and
15
+ * the mobile drawer together (see `liftWidthOverrides` below), or with a rule
16
+ * on `:root`/`html`.
17
+ *
18
+ * ⚠️ A DECLARATION ON A REACT-TREE ANCESTOR REACHES THE RAIL AND NOT THE
19
+ * DRAWER. Custom properties inherit down the DOM tree, and the drawer is
20
+ * portalled to `document.body`, so a layout wrapper is not an ancestor of it.
21
+ * `:root` is, which is why it is named above and a wrapper is not.
22
+ */
6
23
  const sidebarVariants = cva('group/sidebar shadow-sidebar relative flex flex-col transition-[width] duration-300 ease-in-out', {
7
24
  variants: {
8
25
  collapsed: {
9
- true: 'w-[72px]',
10
- false: 'w-[244px]'
26
+ true: 'w-[var(--sidebar-width-collapsed)]',
27
+ false: 'w-[var(--sidebar-width)]'
11
28
  }
12
29
  },
13
30
  defaultVariants: {
14
31
  collapsed: false
15
32
  }
16
33
  });
17
- export const SidebarRoot = ({ className, ...props }) => {
18
- const { isCollapsed } = useSidebar();
19
- return (_jsx("nav", { "data-slot": "sidebar-root", className: cn(sidebarVariants({ collapsed: isCollapsed }), className), "data-collapsed": isCollapsed, ...props }));
34
+ /**
35
+ * Tailwind arbitrary-property classes that re-point the sidebar geometry, e.g.
36
+ * `[--sidebar-width:320px]` or `[--sidebar-width-collapsed:88px]`.
37
+ *
38
+ * ⚠️ ANCHORED TO A CLASS BOUNDARY, WHICH IS THE WHOLE POINT OF THE LOOKBEHIND.
39
+ * Unanchored it also matched the tail of `md:[--sidebar-width:320px]` and
40
+ * lifted it with the prefix stripped — an override a consumer scoped to
41
+ * ≥768px, applied unconditionally to the drawer that only exists below it.
42
+ */
43
+ const WIDTH_OVERRIDE = /(?<=^|\s)\[--sidebar-width(?:-collapsed)?:[^\]]+\]/g;
44
+ /**
45
+ * ⛔ THE ONE PART OF THE CONSUMER'S className THAT HAS TO CROSS THE PORTAL.
46
+ *
47
+ * `SheetContent` mounts through `SheetPortal` into `document.body`, and custom
48
+ * properties inherit down the DOM tree rather than the React tree. So the
49
+ * documented override — a class on the rail — set `--sidebar-width` on an
50
+ * element the drawer is not a descendant of, and the whole className that DOES
51
+ * travel lands on the inner `<nav>`, which is BELOW the element that reads the
52
+ * variable. Measured in Chromium: rail 320px, drawer 244px. Following the
53
+ * instruction handed to product-console produced a 280px rail on desktop and a
54
+ * 244px drawer on a phone, silently.
55
+ *
56
+ * ⚠️ ONLY THE GEOMETRY TOKENS. The rest of a rail className is rail-shaped —
57
+ * `h-full`, and the `data-[collapsed=false]:min-w-70` all eight console
58
+ * sidebars pass — and putting that on the sheet would reinstate the
59
+ * 280px-inside-a-244px-sheet overflow from the other side.
60
+ */
61
+ function liftWidthOverrides(className) {
62
+ return className?.match(WIDTH_OVERRIDE)?.join(' ');
63
+ }
64
+ /**
65
+ * ⛔ BELOW 768px A RAIL IS NOT A LAYOUT — once a consumer opts in.
66
+ *
67
+ * The widths above are a fixed share of a flex row, so at 390px the expanded
68
+ * rail took 63% of the screen and left the page 146px, and the only affordance
69
+ * — `SidebarExpandButton` — shrinks it to 72px, which is still 18% and still
70
+ * permanently on screen. There was nothing that removed it.
71
+ *
72
+ * With `mobile="drawer"` the rail is replaced below the breakpoint by an
73
+ * overlay drawer. Escape, the backdrop click, the focus trap and returning
74
+ * focus to whatever opened it are Radix Dialog's, through the kit's own
75
+ * `Sheet`; none of that is re-implemented here.
76
+ */
77
+ export const SidebarRoot = ({ className, id, mobile = 'inline', mobileTitle = 'Navigation', ...props }) => {
78
+ const context = useSidebar();
79
+ const { isCollapsed, isMobile, openMobile, setOpenMobile, restoreDrawerFocus, sidebarId } = context;
80
+ if (!isMobile || mobile === 'inline') {
81
+ return (_jsx("nav", { "data-slot": "sidebar-root", className: cn(sidebarVariants({ collapsed: isCollapsed }), className), "data-collapsed": isCollapsed, ...props, id: mobile === 'drawer' ? sidebarId : id }));
82
+ }
83
+ return (_jsx(SidebarContext.Provider, { value: { ...context, isCollapsed: false, isDrawer: true }, children: _jsx(Sheet, { open: openMobile, onOpenChange: setOpenMobile, children: _jsxs(SheetContent, { side: "left",
84
+ // The Sheet's own paddings are built for a form panel; a rail brings
85
+ // its own. `max-w-full` so a consumer widening `--sidebar-width` past
86
+ // the viewport still cannot push the drawer off screen. The lifted
87
+ // override goes LAST so it re-points the variable this element reads.
88
+ className: cn('w-[var(--sidebar-width)] max-w-full gap-0 p-0', liftWidthOverrides(className)), "aria-describedby": undefined,
89
+ // ⛔ RADIX'S OWN RESTORE GOES NOWHERE HERE. `DialogContent`
90
+ // preventDefaults the focus scope's restore and focuses the ref a
91
+ // `SheetTrigger` would have filled; this drawer is opened from provider
92
+ // state, so that ref is null and closing dropped focus onto `<body>`.
93
+ // Running first means `composeEventHandlers` skips Radix's half.
94
+ onCloseAutoFocus: (event) => {
95
+ event.preventDefault();
96
+ restoreDrawerFocus();
97
+ }, children: [_jsx(SheetTitle, { className: "sr-only", children: mobileTitle }), _jsx("nav", { "data-slot": "sidebar-root", "data-mobile": "true",
98
+ /* ⛔ NO `data-collapsed` HERE. The consumer className is forwarded
99
+ verbatim, and all eight console sidebars pass
100
+ `data-[collapsed=false]:min-w-70`; stamping the attribute armed
101
+ it, giving the navigation a 280px min-width inside a 244px sheet.
102
+ `min-width` resolves after `max-width`, so the sheet could not
103
+ claw it back and the contents were 36px wider than the drawer.
104
+ `data-mobile` already names this surface and a drawer is never
105
+ collapsed, so the attribute had nothing left to say. */
106
+ className: cn(sidebarVariants({ collapsed: false }), 'h-full w-full shadow-none', className), ...props,
107
+ /* ⛔ AFTER THE SPREAD, AND NOT THE CALLER'S. This is the target of
108
+ `SidebarTrigger`'s `aria-controls` and the handle the provider
109
+ uses to put focus back into the navigation when the viewport
110
+ grows. Spread first, a caller that set `id` replaced it and left
111
+ a dangling `aria-controls` — which axe reports as a CRITICAL
112
+ `aria-valid-attr-value` — and a focus restore that lands on
113
+ `<body>`. Both silent. */
114
+ id: sidebarId })] }) }) }));
20
115
  };
@@ -0,0 +1,18 @@
1
+ import React from 'react';
2
+ import { IconButton } from '../icon-button/index.js';
3
+ export type SidebarTriggerProps = React.ComponentProps<typeof IconButton>;
4
+ /**
5
+ * The control that opens the mobile drawer.
6
+ *
7
+ * `md:hidden` by default because above 768px the rail is already on screen and
8
+ * a second way to reach it is a second thing to explain; a consumer that wants
9
+ * it everywhere passes its own display class.
10
+ *
11
+ * ⚠️ `aria-controls` IS SET ONLY WHILE THE DRAWER EXISTS. Radix unmounts the
12
+ * dialog content when closed, and a reference to an element that is not in the
13
+ * DOM is what axe-core reports as a CRITICAL `aria-valid-attr-value` — the same
14
+ * defect the Autocomplete panel was repaired for (console-sdk#150). A toggle
15
+ * with `aria-expanded="false"` and no `aria-controls` is complete on its own.
16
+ */
17
+ export declare const SidebarTrigger: ({ className, onClick, "aria-label": ariaLabel, ...props }: SidebarTriggerProps) => React.JSX.Element;
18
+ //# sourceMappingURL=sidebar-trigger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sidebar-trigger.d.ts","sourceRoot":"","sources":["../../../../../src/components/ui/sidebar/sidebar-trigger.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,MAAM,OAAO,CAAA;AAGzB,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAG3C,MAAM,MAAM,mBAAmB,GAAG,KAAK,CAAC,cAAc,CAAC,OAAO,UAAU,CAAC,CAAA;AAEzE;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,cAAc,GAAI,2DAK5B,mBAAmB,sBAqBrB,CAAA"}
@@ -0,0 +1,26 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { Menu } from 'lucide-react';
4
+ import { cn } from '../../../lib/utils.js';
5
+ import { IconButton } from '../icon-button/index.js';
6
+ import { useSidebar } from './sidebar-provider.js';
7
+ /**
8
+ * The control that opens the mobile drawer.
9
+ *
10
+ * `md:hidden` by default because above 768px the rail is already on screen and
11
+ * a second way to reach it is a second thing to explain; a consumer that wants
12
+ * it everywhere passes its own display class.
13
+ *
14
+ * ⚠️ `aria-controls` IS SET ONLY WHILE THE DRAWER EXISTS. Radix unmounts the
15
+ * dialog content when closed, and a reference to an element that is not in the
16
+ * DOM is what axe-core reports as a CRITICAL `aria-valid-attr-value` — the same
17
+ * defect the Autocomplete panel was repaired for (console-sdk#150). A toggle
18
+ * with `aria-expanded="false"` and no `aria-controls` is complete on its own.
19
+ */
20
+ export const SidebarTrigger = ({ className, onClick, 'aria-label': ariaLabel = 'Open navigation', ...props }) => {
21
+ const { openMobile, setOpenMobile, sidebarId } = useSidebar();
22
+ return (_jsx(IconButton, { type: "button", variant: "outline", "data-slot": "sidebar-trigger", "aria-label": ariaLabel, "aria-expanded": openMobile, "aria-controls": openMobile ? sidebarId : undefined, className: cn('md:hidden', className), onClick: (event) => {
23
+ onClick?.(event);
24
+ setOpenMobile(!openMobile);
25
+ }, ...props, children: _jsx(Menu, { "aria-hidden": true }) }));
26
+ };
@@ -13,6 +13,6 @@ declare const toggleVariants: (props?: ({
13
13
  declare function ToggleGroup({ className, variant, size, spacing, children, style, ...props }: React.ComponentProps<typeof ToggleGroupPrimitive.Root> & VariantProps<typeof toggleVariants> & {
14
14
  spacing?: number;
15
15
  }): React.JSX.Element;
16
- declare function ToggleGroupItem({ className, children, variant, size, ...props }: React.ComponentProps<typeof ToggleGroupPrimitive.Item> & VariantProps<typeof toggleVariants>): React.JSX.Element;
16
+ declare function ToggleGroupItem({ className, children, variant, size, onFocus, ...props }: React.ComponentProps<typeof ToggleGroupPrimitive.Item> & VariantProps<typeof toggleVariants>): React.JSX.Element;
17
17
  export { ToggleGroup, ToggleGroupItem };
18
18
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/components/ui/toggle-group/index.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,KAAK,oBAAoB,MAAM,8BAA8B,CAAA;AACpE,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAIjE;;;;GAIG;AACH,QAAA,MAAM,cAAc;;;8EAoBnB,CAAA;AAYD,iBAAS,WAAW,CAAC,EACnB,SAAS,EACT,OAAO,EACP,IAAI,EACJ,OAAW,EACX,QAAQ,EACR,KAAK,EACL,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,oBAAoB,CAAC,IAAI,CAAC,GACvD,YAAY,CAAC,OAAO,cAAc,CAAC,GAAG;IACpC,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB,qBAwBF;AAED,iBAAS,eAAe,CAAC,EACvB,SAAS,EACT,QAAQ,EACR,OAAO,EACP,IAAI,EACJ,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,oBAAoB,CAAC,IAAI,CAAC,GACvD,YAAY,CAAC,OAAO,cAAc,CAAC,qBAuBpC;AAED,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/components/ui/toggle-group/index.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,KAAK,oBAAoB,MAAM,8BAA8B,CAAA;AACpE,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAIjE;;;;GAIG;AACH,QAAA,MAAM,cAAc;;;8EAoBnB,CAAA;AA4DD,iBAAS,WAAW,CAAC,EACnB,SAAS,EACT,OAAO,EACP,IAAI,EACJ,OAAW,EACX,QAAQ,EACR,KAAK,EACL,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,oBAAoB,CAAC,IAAI,CAAC,GACvD,YAAY,CAAC,OAAO,cAAc,CAAC,GAAG;IACpC,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB,qBA+BF;AAED,iBAAS,eAAe,CAAC,EACvB,SAAS,EACT,QAAQ,EACR,OAAO,EACP,IAAI,EACJ,OAAO,EACP,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,oBAAoB,CAAC,IAAI,CAAC,GACvD,YAAY,CAAC,OAAO,cAAc,CAAC,qBAiDpC;AAED,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,CAAA"}
@@ -26,12 +26,58 @@ const toggleVariants = cva("hover:bg-muted hover:text-muted-foreground focus-vis
26
26
  size: 'default'
27
27
  }
28
28
  });
29
+ const ARROW_KEYS = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'];
30
+ /**
31
+ * Is an arrow key down right now?
32
+ *
33
+ * This is how an item tells keyboard navigation apart from a pointer press or a
34
+ * programmatic focus, and it is the seam Radix uses for the same job in its own
35
+ * `RadioGroup`. It works because `RovingFocusGroup` defers the focus move to a
36
+ * `setTimeout`: the keydown has already bubbled to `document` and set the flag
37
+ * by the time the item receives `focus`.
38
+ *
39
+ * The distinction is not cosmetic. A pointer press focuses BEFORE the click
40
+ * lands, so selecting on every focus would turn one click into
41
+ * select-then-deselect and break mouse selection outright.
42
+ *
43
+ * Returns null when disabled, so `type="multiple"` carries no flag to read.
44
+ */
45
+ function useArrowKeyPressed(enabled) {
46
+ const arrowKeyPressed = React.useRef(false);
47
+ React.useEffect(() => {
48
+ if (!enabled)
49
+ return;
50
+ const onKeyDown = (event) => {
51
+ if (ARROW_KEYS.includes(event.key))
52
+ arrowKeyPressed.current = true;
53
+ };
54
+ const onKeyUp = () => {
55
+ arrowKeyPressed.current = false;
56
+ };
57
+ document.addEventListener('keydown', onKeyDown);
58
+ document.addEventListener('keyup', onKeyUp);
59
+ window.addEventListener('blur', onKeyUp);
60
+ return () => {
61
+ document.removeEventListener('keydown', onKeyDown);
62
+ document.removeEventListener('keyup', onKeyUp);
63
+ window.removeEventListener('blur', onKeyUp);
64
+ // A group unmounted mid-keystroke never sees the keyup.
65
+ arrowKeyPressed.current = false;
66
+ };
67
+ }, [enabled]);
68
+ return enabled ? arrowKeyPressed : null;
69
+ }
29
70
  const ToggleGroupContext = React.createContext({
30
71
  size: 'default',
31
72
  variant: 'default',
32
- spacing: 0
73
+ spacing: 0,
74
+ arrowKeyPressed: null
33
75
  });
34
76
  function ToggleGroup({ className, variant, size, spacing = 0, children, style, ...props }) {
77
+ // Read, never destructured: `type` is the discriminant of the Radix props
78
+ // union, and pulling it out then spreading it back widens the union so the
79
+ // single/multiple props stop resolving.
80
+ const arrowKeyPressed = useArrowKeyPressed(props.type === 'single');
35
81
  return (_jsx(ToggleGroupPrimitive.Root, { "data-slot": "toggle-group", "data-variant": variant, "data-size": size, "data-spacing": spacing,
36
82
  // Caller style first: `--gap` is ours and must survive, but everything
37
83
  // else the caller passes has to reach the element.
@@ -39,13 +85,36 @@ function ToggleGroup({ className, variant, size, spacing = 0, children, style, .
39
85
  // Legacy carried a `data-[spacing=default]:...:shadow-xs` rule here that
40
86
  // never matched — `data-spacing` is always a number. Removed rather than
41
87
  // activated, so the rendered pixels stay identical to sindarian-x.
42
- 'group/toggle-group flex w-fit items-center gap-[--spacing(var(--gap))] rounded-md', className), ...props, children: _jsx(ToggleGroupContext.Provider, { value: { variant, size, spacing }, children: children }) }));
88
+ 'group/toggle-group flex w-fit items-center gap-[--spacing(var(--gap))] rounded-md', className), ...props, children: _jsx(ToggleGroupContext.Provider, { value: { variant, size, spacing, arrowKeyPressed }, children: children }) }));
43
89
  }
44
- function ToggleGroupItem({ className, children, variant, size, ...props }) {
90
+ function ToggleGroupItem({ className, children, variant, size, onFocus, ...props }) {
45
91
  const context = React.useContext(ToggleGroupContext);
92
+ /**
93
+ * Selection follows focus, which is what `role="radio"` promises.
94
+ *
95
+ * With `type="single"` Radix puts the group on the ARIA radio pattern: the
96
+ * root is a `radiogroup`, each item a `radio` with `aria-checked`. Under that
97
+ * pattern an arrow key both moves focus and checks the item it lands on.
98
+ * Radix only moves focus, so a screen-reader operator used to arrow across
99
+ * "radio, not checked, 2 of 3" with nothing ever selected.
100
+ *
101
+ * `type="multiple"` is a `toolbar` of `aria-pressed` buttons and keeps its
102
+ * current behavior, where moving focus without pressing is correct: the flag
103
+ * is null there.
104
+ *
105
+ * Clicking the element rather than writing the value keeps a single
106
+ * activation path, so Radix's own toggle and any caller `onClick` both fire
107
+ * exactly as they do for a real click.
108
+ */
109
+ const handleFocus = (event) => {
110
+ onFocus?.(event);
111
+ if (context.arrowKeyPressed?.current) {
112
+ event.currentTarget.click();
113
+ }
114
+ };
46
115
  return (_jsx(ToggleGroupPrimitive.Item, { "data-slot": "toggle-group-item", "data-variant": context.variant || variant, "data-size": context.size || size, "data-spacing": context.spacing, className: cn(toggleVariants({
47
116
  variant: context.variant || variant,
48
117
  size: context.size || size
49
- }), 'w-auto min-w-0 shrink-0 px-3 focus:z-10 focus-visible:z-10', 'data-[spacing=0]:rounded-none data-[spacing=0]:shadow-none data-[spacing=0]:first:rounded-l-md data-[spacing=0]:last:rounded-r-md data-[spacing=0]:data-[variant=outline]:border-l-0 data-[spacing=0]:data-[variant=outline]:first:border-l', className), ...props, children: children }));
118
+ }), 'w-auto min-w-0 shrink-0 px-3 focus:z-10 focus-visible:z-10', 'data-[spacing=0]:rounded-none data-[spacing=0]:shadow-none data-[spacing=0]:first:rounded-l-md data-[spacing=0]:last:rounded-r-md data-[spacing=0]:data-[variant=outline]:border-l-0 data-[spacing=0]:data-[variant=outline]:first:border-l', className), ...props, onFocus: handleFocus, children: children }));
50
119
  }
51
120
  export { ToggleGroup, ToggleGroupItem };
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/enterprise/number-input/index.tsx"],"names":[],"mappings":"AAEA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAQ9B,MAAM,MAAM,gBAAgB,GAAG;IAC7B,wDAAwD;IACxD,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,+EAA+E;IAC/E,aAAa,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IAC5C,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,wCAAwC;IACxC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,sEAAsE;IACtE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,yEAAyE;IACzE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,kEAAkE;IAClE,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,8CAA8C;IAC9C,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,sDAAsD;IACtD,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,CAAA;AAmED;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAgBpE;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAMxE;AAED,wBAAgB,WAAW,CAAC,EAC1B,KAAK,EACL,aAAa,EACb,GAAG,EACH,GAAG,EACH,IAAQ,EACR,SAAS,EACT,MAAM,EACN,QAAgB,EAChB,EAAE,EACF,SAAS,EACT,WAAW,EACX,YAAY,EAAE,SAAS,EACvB,kBAAkB,EAAE,eAAe,EACpC,EAAE,gBAAgB,qBAgJlB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/enterprise/number-input/index.tsx"],"names":[],"mappings":"AAEA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAQ9B,MAAM,MAAM,gBAAgB,GAAG;IAC7B,wDAAwD;IACxD,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,+EAA+E;IAC/E,aAAa,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IAC5C,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,wCAAwC;IACxC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,sEAAsE;IACtE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,yEAAyE;IACzE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,kEAAkE;IAClE,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,8CAA8C;IAC9C,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,sDAAsD;IACtD,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,CAAA;AAmED;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAgBpE;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAMxE;AAED,wBAAgB,WAAW,CAAC,EAC1B,KAAK,EACL,aAAa,EACb,GAAG,EACH,GAAG,EACH,IAAQ,EACR,SAAS,EACT,MAAM,EACN,QAAgB,EAChB,EAAE,EACF,SAAS,EACT,WAAW,EACX,YAAY,EAAE,SAAS,EACvB,kBAAkB,EAAE,eAAe,EACpC,EAAE,gBAAgB,qBA2JlB"}