@jini-ai/admin 0.1.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 (235) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +138 -0
  3. package/dist/browser/index.d.ts +8 -0
  4. package/dist/browser/index.d.ts.map +1 -0
  5. package/dist/browser/index.js +8 -0
  6. package/dist/browser/index.js.map +1 -0
  7. package/dist/browser/navigation.d.ts +39 -0
  8. package/dist/browser/navigation.d.ts.map +1 -0
  9. package/dist/browser/navigation.js +113 -0
  10. package/dist/browser/navigation.js.map +1 -0
  11. package/dist/core/gated/types.d.ts +87 -0
  12. package/dist/core/gated/types.d.ts.map +1 -0
  13. package/dist/core/gated/types.js +37 -0
  14. package/dist/core/gated/types.js.map +1 -0
  15. package/dist/core/index.d.ts +20 -0
  16. package/dist/core/index.d.ts.map +1 -0
  17. package/dist/core/index.js +15 -0
  18. package/dist/core/index.js.map +1 -0
  19. package/dist/core/manifest/rules.d.ts +84 -0
  20. package/dist/core/manifest/rules.d.ts.map +1 -0
  21. package/dist/core/manifest/rules.js +130 -0
  22. package/dist/core/manifest/rules.js.map +1 -0
  23. package/dist/core/manifest/types.d.ts +154 -0
  24. package/dist/core/manifest/types.d.ts.map +1 -0
  25. package/dist/core/manifest/types.js +31 -0
  26. package/dist/core/manifest/types.js.map +1 -0
  27. package/dist/core/permissions/rules.d.ts +32 -0
  28. package/dist/core/permissions/rules.d.ts.map +1 -0
  29. package/dist/core/permissions/rules.js +34 -0
  30. package/dist/core/permissions/rules.js.map +1 -0
  31. package/dist/core/ports/analytics.d.ts +35 -0
  32. package/dist/core/ports/analytics.d.ts.map +1 -0
  33. package/dist/core/ports/analytics.js +14 -0
  34. package/dist/core/ports/analytics.js.map +1 -0
  35. package/dist/core/ports/auth.d.ts +49 -0
  36. package/dist/core/ports/auth.d.ts.map +1 -0
  37. package/dist/core/ports/auth.js +19 -0
  38. package/dist/core/ports/auth.js.map +1 -0
  39. package/dist/core/ports/comments.d.ts +90 -0
  40. package/dist/core/ports/comments.d.ts.map +1 -0
  41. package/dist/core/ports/comments.js +24 -0
  42. package/dist/core/ports/comments.js.map +1 -0
  43. package/dist/core/ports/database.d.ts +101 -0
  44. package/dist/core/ports/database.d.ts.map +1 -0
  45. package/dist/core/ports/database.js +2 -0
  46. package/dist/core/ports/database.js.map +1 -0
  47. package/dist/core/ports/forms.d.ts +130 -0
  48. package/dist/core/ports/forms.d.ts.map +1 -0
  49. package/dist/core/ports/forms.js +62 -0
  50. package/dist/core/ports/forms.js.map +1 -0
  51. package/dist/core/ports/identity.d.ts +114 -0
  52. package/dist/core/ports/identity.d.ts.map +1 -0
  53. package/dist/core/ports/identity.js +28 -0
  54. package/dist/core/ports/identity.js.map +1 -0
  55. package/dist/core/ports/index.d.ts +21 -0
  56. package/dist/core/ports/index.d.ts.map +1 -0
  57. package/dist/core/ports/index.js +6 -0
  58. package/dist/core/ports/index.js.map +1 -0
  59. package/dist/core/ports/integrations.d.ts +81 -0
  60. package/dist/core/ports/integrations.d.ts.map +1 -0
  61. package/dist/core/ports/integrations.js +21 -0
  62. package/dist/core/ports/integrations.js.map +1 -0
  63. package/dist/core/ports/media.d.ts +74 -0
  64. package/dist/core/ports/media.d.ts.map +1 -0
  65. package/dist/core/ports/media.js +28 -0
  66. package/dist/core/ports/media.js.map +1 -0
  67. package/dist/core/ports/members.d.ts +50 -0
  68. package/dist/core/ports/members.d.ts.map +1 -0
  69. package/dist/core/ports/members.js +17 -0
  70. package/dist/core/ports/members.js.map +1 -0
  71. package/dist/core/ports/menus.d.ts +179 -0
  72. package/dist/core/ports/menus.d.ts.map +1 -0
  73. package/dist/core/ports/menus.js +70 -0
  74. package/dist/core/ports/menus.js.map +1 -0
  75. package/dist/core/ports/plugins.d.ts +68 -0
  76. package/dist/core/ports/plugins.d.ts.map +1 -0
  77. package/dist/core/ports/plugins.js +27 -0
  78. package/dist/core/ports/plugins.js.map +1 -0
  79. package/dist/core/ports/recovery.d.ts +171 -0
  80. package/dist/core/ports/recovery.d.ts.map +1 -0
  81. package/dist/core/ports/recovery.js +2 -0
  82. package/dist/core/ports/recovery.js.map +1 -0
  83. package/dist/core/ports/redirects.d.ts +139 -0
  84. package/dist/core/ports/redirects.d.ts.map +1 -0
  85. package/dist/core/ports/redirects.js +55 -0
  86. package/dist/core/ports/redirects.js.map +1 -0
  87. package/dist/core/ports/seo.d.ts +147 -0
  88. package/dist/core/ports/seo.d.ts.map +1 -0
  89. package/dist/core/ports/seo.js +39 -0
  90. package/dist/core/ports/seo.js.map +1 -0
  91. package/dist/core/ports/settings.d.ts +87 -0
  92. package/dist/core/ports/settings.d.ts.map +1 -0
  93. package/dist/core/ports/settings.js +23 -0
  94. package/dist/core/ports/settings.js.map +1 -0
  95. package/dist/core/ports/workspace.d.ts +44 -0
  96. package/dist/core/ports/workspace.d.ts.map +1 -0
  97. package/dist/core/ports/workspace.js +29 -0
  98. package/dist/core/ports/workspace.js.map +1 -0
  99. package/dist/core/routing/rules.d.ts +54 -0
  100. package/dist/core/routing/rules.d.ts.map +1 -0
  101. package/dist/core/routing/rules.js +113 -0
  102. package/dist/core/routing/rules.js.map +1 -0
  103. package/dist/core/routing/types.d.ts +36 -0
  104. package/dist/core/routing/types.d.ts.map +1 -0
  105. package/dist/core/routing/types.js +17 -0
  106. package/dist/core/routing/types.js.map +1 -0
  107. package/dist/core/transport/errors.d.ts +53 -0
  108. package/dist/core/transport/errors.d.ts.map +1 -0
  109. package/dist/core/transport/errors.js +63 -0
  110. package/dist/core/transport/errors.js.map +1 -0
  111. package/dist/core/transport/http.d.ts +63 -0
  112. package/dist/core/transport/http.d.ts.map +1 -0
  113. package/dist/core/transport/http.js +63 -0
  114. package/dist/core/transport/http.js.map +1 -0
  115. package/dist/core/transport/index.d.ts +5 -0
  116. package/dist/core/transport/index.d.ts.map +1 -0
  117. package/dist/core/transport/index.js +3 -0
  118. package/dist/core/transport/index.js.map +1 -0
  119. package/dist/core/transport/types.d.ts +81 -0
  120. package/dist/core/transport/types.d.ts.map +1 -0
  121. package/dist/core/transport/types.js +43 -0
  122. package/dist/core/transport/types.js.map +1 -0
  123. package/dist/react/components/ConfirmButton/ConfirmButton.d.ts +40 -0
  124. package/dist/react/components/ConfirmButton/ConfirmButton.d.ts.map +1 -0
  125. package/dist/react/components/ConfirmButton/ConfirmButton.hooks.d.ts +22 -0
  126. package/dist/react/components/ConfirmButton/ConfirmButton.hooks.d.ts.map +1 -0
  127. package/dist/react/components/ConfirmButton/ConfirmButton.hooks.js +59 -0
  128. package/dist/react/components/ConfirmButton/ConfirmButton.hooks.js.map +1 -0
  129. package/dist/react/components/ConfirmButton/ConfirmButton.js +40 -0
  130. package/dist/react/components/ConfirmButton/ConfirmButton.js.map +1 -0
  131. package/dist/react/components/ConfirmButton.d.ts +39 -0
  132. package/dist/react/components/ConfirmButton.d.ts.map +1 -0
  133. package/dist/react/components/ConfirmButton.js +79 -0
  134. package/dist/react/components/ConfirmButton.js.map +1 -0
  135. package/dist/react/components/ConfirmDialog/ConfirmDialog.d.ts +90 -0
  136. package/dist/react/components/ConfirmDialog/ConfirmDialog.d.ts.map +1 -0
  137. package/dist/react/components/ConfirmDialog/ConfirmDialog.hooks.d.ts +49 -0
  138. package/dist/react/components/ConfirmDialog/ConfirmDialog.hooks.d.ts.map +1 -0
  139. package/dist/react/components/ConfirmDialog/ConfirmDialog.hooks.js +81 -0
  140. package/dist/react/components/ConfirmDialog/ConfirmDialog.hooks.js.map +1 -0
  141. package/dist/react/components/ConfirmDialog/ConfirmDialog.js +92 -0
  142. package/dist/react/components/ConfirmDialog/ConfirmDialog.js.map +1 -0
  143. package/dist/react/components/ConfirmDialog.d.ts +57 -0
  144. package/dist/react/components/ConfirmDialog.d.ts.map +1 -0
  145. package/dist/react/components/ConfirmDialog.js +85 -0
  146. package/dist/react/components/ConfirmDialog.js.map +1 -0
  147. package/dist/react/components/DataTable.d.ts +97 -0
  148. package/dist/react/components/DataTable.d.ts.map +1 -0
  149. package/dist/react/components/DataTable.js +20 -0
  150. package/dist/react/components/DataTable.js.map +1 -0
  151. package/dist/react/components/InteractiveHtmlEditor/InteractiveHtmlEditor.d.ts +25 -0
  152. package/dist/react/components/InteractiveHtmlEditor/InteractiveHtmlEditor.d.ts.map +1 -0
  153. package/dist/react/components/InteractiveHtmlEditor/InteractiveHtmlEditor.js +148 -0
  154. package/dist/react/components/InteractiveHtmlEditor/InteractiveHtmlEditor.js.map +1 -0
  155. package/dist/react/components/RowMenu/RowMenu.d.ts +77 -0
  156. package/dist/react/components/RowMenu/RowMenu.d.ts.map +1 -0
  157. package/dist/react/components/RowMenu/RowMenu.hooks.d.ts +30 -0
  158. package/dist/react/components/RowMenu/RowMenu.hooks.d.ts.map +1 -0
  159. package/dist/react/components/RowMenu/RowMenu.hooks.js +154 -0
  160. package/dist/react/components/RowMenu/RowMenu.hooks.js.map +1 -0
  161. package/dist/react/components/RowMenu/RowMenu.js +46 -0
  162. package/dist/react/components/RowMenu/RowMenu.js.map +1 -0
  163. package/dist/react/components/RowMenu.d.ts +44 -0
  164. package/dist/react/components/RowMenu.d.ts.map +1 -0
  165. package/dist/react/components/RowMenu.js +149 -0
  166. package/dist/react/components/RowMenu.js.map +1 -0
  167. package/dist/react/components/Sidebar.d.ts +128 -0
  168. package/dist/react/components/Sidebar.d.ts.map +1 -0
  169. package/dist/react/components/Sidebar.js +168 -0
  170. package/dist/react/components/Sidebar.js.map +1 -0
  171. package/dist/react/hooks/use-nav-sections.d.ts +50 -0
  172. package/dist/react/hooks/use-nav-sections.d.ts.map +1 -0
  173. package/dist/react/hooks/use-nav-sections.js +112 -0
  174. package/dist/react/hooks/use-nav-sections.js.map +1 -0
  175. package/dist/react/hooks/use-sidebar-rail.d.ts +39 -0
  176. package/dist/react/hooks/use-sidebar-rail.d.ts.map +1 -0
  177. package/dist/react/hooks/use-sidebar-rail.js +93 -0
  178. package/dist/react/hooks/use-sidebar-rail.js.map +1 -0
  179. package/dist/react/index.d.ts +53 -0
  180. package/dist/react/index.d.ts.map +1 -0
  181. package/dist/react/index.js +53 -0
  182. package/dist/react/index.js.map +1 -0
  183. package/dist/react/types.d.ts +28 -0
  184. package/dist/react/types.d.ts.map +1 -0
  185. package/dist/react/types.js +23 -0
  186. package/dist/react/types.js.map +1 -0
  187. package/dist/server/composio/bounded-data.d.ts +36 -0
  188. package/dist/server/composio/bounded-data.d.ts.map +1 -0
  189. package/dist/server/composio/bounded-data.js +181 -0
  190. package/dist/server/composio/bounded-data.js.map +1 -0
  191. package/dist/server/composio/catalog.d.ts +147 -0
  192. package/dist/server/composio/catalog.d.ts.map +1 -0
  193. package/dist/server/composio/catalog.js +126 -0
  194. package/dist/server/composio/catalog.js.map +1 -0
  195. package/dist/server/composio/composio-config.d.ts +42 -0
  196. package/dist/server/composio/composio-config.d.ts.map +1 -0
  197. package/dist/server/composio/composio-config.js +145 -0
  198. package/dist/server/composio/composio-config.js.map +1 -0
  199. package/dist/server/composio/composio-descriptions.d.ts +16 -0
  200. package/dist/server/composio/composio-descriptions.d.ts.map +1 -0
  201. package/dist/server/composio/composio-descriptions.js +766 -0
  202. package/dist/server/composio/composio-descriptions.js.map +1 -0
  203. package/dist/server/composio/composio.d.ts +364 -0
  204. package/dist/server/composio/composio.d.ts.map +1 -0
  205. package/dist/server/composio/composio.js +1902 -0
  206. package/dist/server/composio/composio.js.map +1 -0
  207. package/dist/server/composio/errors.d.ts +15 -0
  208. package/dist/server/composio/errors.d.ts.map +1 -0
  209. package/dist/server/composio/errors.js +16 -0
  210. package/dist/server/composio/errors.js.map +1 -0
  211. package/dist/server/composio/file-lock.d.ts +13 -0
  212. package/dist/server/composio/file-lock.d.ts.map +1 -0
  213. package/dist/server/composio/file-lock.js +92 -0
  214. package/dist/server/composio/file-lock.js.map +1 -0
  215. package/dist/server/composio/index.d.ts +18 -0
  216. package/dist/server/composio/index.d.ts.map +1 -0
  217. package/dist/server/composio/index.js +7 -0
  218. package/dist/server/composio/index.js.map +1 -0
  219. package/dist/server/composio/json-schema.d.ts +93 -0
  220. package/dist/server/composio/json-schema.d.ts.map +1 -0
  221. package/dist/server/composio/json-schema.js +498 -0
  222. package/dist/server/composio/json-schema.js.map +1 -0
  223. package/dist/server/composio/output-protection.d.ts +32 -0
  224. package/dist/server/composio/output-protection.d.ts.map +1 -0
  225. package/dist/server/composio/output-protection.js +105 -0
  226. package/dist/server/composio/output-protection.js.map +1 -0
  227. package/dist/server/composio/service.d.ts +181 -0
  228. package/dist/server/composio/service.d.ts.map +1 -0
  229. package/dist/server/composio/service.js +758 -0
  230. package/dist/server/composio/service.js.map +1 -0
  231. package/dist/server/index.d.ts +15 -0
  232. package/dist/server/index.d.ts.map +1 -0
  233. package/dist/server/index.js +15 -0
  234. package/dist/server/index.js.map +1 -0
  235. package/package.json +92 -0
@@ -0,0 +1,128 @@
1
+ import { type FocusEvent, type MouseEvent, type ReactNode } from 'react';
2
+ import type { AdminNavGroup, AdminNavItem } from '../../core/manifest/rules.js';
3
+ /** Handlers that make any element participate in rail-mode tooltips. Spread onto a host's own
4
+ * control: `<button {...railTooltipProps('Log out')}>`. */
5
+ export interface RailTooltipHandlers {
6
+ onMouseEnter: (e: MouseEvent<HTMLElement>) => void;
7
+ onMouseLeave: () => void;
8
+ onFocus: (e: FocusEvent<HTMLElement>) => void;
9
+ onBlur: () => void;
10
+ }
11
+ export interface SidebarContextValue {
12
+ /** Whether the desktop rail is collapsed to icons. */
13
+ collapsed: boolean;
14
+ toggleRail: () => void;
15
+ /** Mobile drawer state, owned by the host. */
16
+ open: boolean;
17
+ /** Where the admin is mounted, applied to every item's route path. */
18
+ base: string;
19
+ /** The active panel id, matched against `AdminNavItem.id`. */
20
+ activeId: string;
21
+ /** Tooltip handlers for a host-supplied control. No-ops while the rail is expanded. */
22
+ railTooltipProps: (label: string) => RailTooltipHandlers;
23
+ }
24
+ /**
25
+ * Reads the enclosing `<Sidebar>`'s state. Throws outside one — see the file header on why this
26
+ * check is a runtime error rather than a type error.
27
+ */
28
+ export declare function useSidebar(): SidebarContextValue;
29
+ export interface SidebarProps {
30
+ /** The active panel's id — matched against `AdminNavItem.id` by `Sidebar.Nav`. */
31
+ activeId: string;
32
+ /** Where the admin is mounted. Defaults to `/core`'s `DEFAULT_ADMIN_BASE`. */
33
+ base?: string;
34
+ /** Mobile drawer state, owned by the host (it also renders the backdrop). Inert at desktop
35
+ * widths, and deliberately distinct from the desktop rail collapse — see the file header. */
36
+ open?: boolean;
37
+ /** `localStorage` key for the desktop rail preference. A host with a pre-existing key should
38
+ * supply it rather than strand every operator's saved preference behind this package's
39
+ * default. */
40
+ railStorageKey?: string;
41
+ /**
42
+ * Rail state for an operator who has **no stored preference yet**. A host whose admin is
43
+ * icon-first by design passes `true`; every existing host omits it and keeps the expanded
44
+ * default. Never overrides a real stored choice — once the operator has toggled the rail even
45
+ * once, their preference wins on every later load.
46
+ *
47
+ * @default false
48
+ */
49
+ railDefaultCollapsed?: boolean;
50
+ /** Accessible name for the `<nav>` landmark. @default "Admin" */
51
+ label?: string;
52
+ /** Extra class names appended to `.cms-nav`. */
53
+ className?: string;
54
+ /** DOM id, for a host's `aria-controls` on its mobile toggle. @default "admin-sidebar" */
55
+ id?: string;
56
+ children?: ReactNode;
57
+ }
58
+ declare function SidebarRoot(props: SidebarProps): import("react").JSX.Element;
59
+ export interface SidebarMobileHeaderProps {
60
+ onClose: () => void;
61
+ /** @default "Navigation" */
62
+ title?: ReactNode;
63
+ /** Accessible name for the close control. @default "Close navigation" */
64
+ closeLabel?: string;
65
+ }
66
+ /**
67
+ * The off-canvas drawer's own header row, hidden by the host stylesheet at desktop widths.
68
+ *
69
+ * Owns the open-transition focus move: when the drawer opens, focus lands on this close control
70
+ * rather than staying on the topbar toggle behind it, so a keyboard or screen-reader user is
71
+ * actually inside the drawer they just opened.
72
+ */
73
+ declare function SidebarMobileHeader(props: SidebarMobileHeaderProps): import("react").JSX.Element;
74
+ export interface SidebarNavProps {
75
+ /** From `/core`'s `buildNav(resolvePanels(panels, context))`. */
76
+ groups: readonly AdminNavGroup[];
77
+ /** Label on a `soon` item's badge. @default "Soon" */
78
+ soonLabel?: ReactNode;
79
+ /** Replaces the default row rendering entirely. `active` is precomputed from the root's
80
+ * `activeId`; `href` already has the base applied. Use for a host that needs a framework
81
+ * `<Link>` instead of an `<a>`. */
82
+ renderItem?: (item: AdminNavItem, ctx: {
83
+ active: boolean;
84
+ href: string;
85
+ collapsed: boolean;
86
+ }) => ReactNode;
87
+ /**
88
+ * Labels of groups whose heading toggles the section open/closed, persisted per
89
+ * `useNavSections`. Defaults to none, so a host that does not opt in renders exactly as before —
90
+ * this is additive, and an accordion appearing unannounced in an existing product would be a
91
+ * regression dressed as a feature.
92
+ *
93
+ * Matched against `AdminNavGroup.label` because that is the only stable identifier a group has
94
+ * (`buildNav` assigns no id), which also makes it the persistence key — see `useNavSections`.
95
+ */
96
+ collapsibleGroups?: readonly string[];
97
+ }
98
+ declare function SidebarNav(props: SidebarNavProps): import("react").JSX.Element;
99
+ export interface SidebarFooterProps {
100
+ children?: ReactNode;
101
+ className?: string;
102
+ }
103
+ /** The bottom region. Carries no content of its own — the host decides what belongs there. */
104
+ declare function SidebarFooter(props: SidebarFooterProps): import("react").JSX.Element;
105
+ export interface SidebarRailToggleProps {
106
+ /** @default "Expand sidebar" */
107
+ expandLabel?: string;
108
+ /** @default "Collapse sidebar" */
109
+ collapseLabel?: string;
110
+ }
111
+ /**
112
+ * Desktop rail collapse control. A real accessible name plus `aria-expanded` — not a bare icon
113
+ * button. Typically hidden by the host stylesheet inside the mobile drawer, which has its own close
114
+ * control and no rail concept.
115
+ */
116
+ declare function SidebarRailToggle(props: SidebarRailToggleProps): import("react").JSX.Element;
117
+ /**
118
+ * Grouped admin sidebar. See the file header for why this is a compound component and what the
119
+ * context costs.
120
+ */
121
+ export declare const Sidebar: typeof SidebarRoot & {
122
+ MobileHeader: typeof SidebarMobileHeader;
123
+ Nav: typeof SidebarNav;
124
+ Footer: typeof SidebarFooter;
125
+ RailToggle: typeof SidebarRailToggle;
126
+ };
127
+ export {};
128
+ //# sourceMappingURL=Sidebar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Sidebar.d.ts","sourceRoot":"","sources":["../../../src/react/components/Sidebar.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,168 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Fragment, createContext, useContext, useEffect, useMemo, useRef, useState, } from 'react';
3
+ import { createPortal } from 'react-dom';
4
+ import { DEFAULT_ADMIN_BASE, adminHref } from '../../core/routing/rules.js';
5
+ import { useNavSections } from '../hooks/use-nav-sections.js';
6
+ import { useSidebarRail } from '../hooks/use-sidebar-rail.js';
7
+ const SidebarContext = createContext(null);
8
+ /**
9
+ * Reads the enclosing `<Sidebar>`'s state. Throws outside one — see the file header on why this
10
+ * check is a runtime error rather than a type error.
11
+ */
12
+ export function useSidebar() {
13
+ const value = useContext(SidebarContext);
14
+ if (!value) {
15
+ throw new Error('useSidebar (and Sidebar.Nav / Sidebar.Footer / Sidebar.RailToggle / Sidebar.MobileHeader) ' +
16
+ 'must be rendered inside a <Sidebar>. The rail-collapse state they read is published by ' +
17
+ 'the Sidebar root.');
18
+ }
19
+ return value;
20
+ }
21
+ function Icon(props) {
22
+ return (_jsx("svg", { viewBox: "0 0 18 18", fill: "none", stroke: "currentColor", strokeWidth: 1.5, dangerouslySetInnerHTML: { __html: props.markup } }));
23
+ }
24
+ /**
25
+ * Rail-mode tooltip, portaled to `document.body`.
26
+ *
27
+ * Not a CSS-only `position: absolute` child of the hovered item (the first approach tried here) —
28
+ * `.cms-nav` needs `overflow-y: auto` for its own scrollbar, and the CSS overflow spec computes an
29
+ * omitted or even an explicitly-`visible` `overflow-x` to `auto` the instant the other axis isn't
30
+ * `visible` (verified live: setting `overflow-x: visible` alongside `overflow-y: auto` still read
31
+ * back as `overflow-x: auto` via `getComputedStyle`). That silently clips anything positioned to
32
+ * escape the rail's edge — `opacity`/`visibility` both read correctly in a probe, but nothing
33
+ * painted. A portal sidesteps the ancestor entirely: a DOM sibling of the app root, not a
34
+ * descendant of the clipping element, positioned with `position: fixed` from the hovered element's
35
+ * own `getBoundingClientRect()`.
36
+ */
37
+ function RailTooltip(props) {
38
+ if (!props.target)
39
+ return null;
40
+ return createPortal(_jsx("div", { className: "cms-tooltip-portal", style: { top: props.target.top, left: props.target.left }, role: "presentation", children: props.target.label }), document.body);
41
+ }
42
+ function SidebarRoot(props) {
43
+ const rail = useSidebarRail(props.railStorageKey, props.railDefaultCollapsed);
44
+ const [tooltip, setTooltip] = useState(null);
45
+ const open = props.open ?? false;
46
+ const base = props.base ?? DEFAULT_ADMIN_BASE;
47
+ // Rail state can change (toggled, or the viewport crossing the breakpoint) while a tooltip is
48
+ // showing; drop it rather than let a stale one float over content that no longer corresponds to
49
+ // a collapsed rail.
50
+ useEffect(() => {
51
+ setTooltip(null);
52
+ }, [rail.collapsed]);
53
+ const value = useMemo(() => {
54
+ function show(e, label) {
55
+ if (!rail.collapsed)
56
+ return;
57
+ const r = e.currentTarget.getBoundingClientRect();
58
+ setTooltip({ label, top: r.top + r.height / 2, left: r.right + 10 });
59
+ }
60
+ function hide() {
61
+ setTooltip(null);
62
+ }
63
+ return {
64
+ collapsed: rail.collapsed,
65
+ toggleRail: rail.toggle,
66
+ open,
67
+ base,
68
+ activeId: props.activeId,
69
+ railTooltipProps: (label) => ({
70
+ onMouseEnter: (e) => show(e, label),
71
+ onMouseLeave: hide,
72
+ onFocus: (e) => show(e, label),
73
+ onBlur: hide,
74
+ }),
75
+ };
76
+ }, [rail.collapsed, rail.toggle, open, base, props.activeId]);
77
+ const className = [
78
+ 'cms-nav scroll',
79
+ open ? 'is-open' : null,
80
+ rail.collapsed ? 'is-rail' : null,
81
+ props.className,
82
+ ]
83
+ .filter(Boolean)
84
+ .join(' ');
85
+ return (_jsx(SidebarContext.Provider, { value: value, children: _jsxs("nav", { className: className, "aria-label": props.label ?? 'Admin', id: props.id ?? 'admin-sidebar', children: [props.children, _jsx(RailTooltip, { target: tooltip })] }) }));
86
+ }
87
+ /**
88
+ * The off-canvas drawer's own header row, hidden by the host stylesheet at desktop widths.
89
+ *
90
+ * Owns the open-transition focus move: when the drawer opens, focus lands on this close control
91
+ * rather than staying on the topbar toggle behind it, so a keyboard or screen-reader user is
92
+ * actually inside the drawer they just opened.
93
+ */
94
+ function SidebarMobileHeader(props) {
95
+ const { open } = useSidebar();
96
+ const closeButtonRef = useRef(null);
97
+ useEffect(() => {
98
+ if (open)
99
+ closeButtonRef.current?.focus();
100
+ }, [open]);
101
+ return (_jsxs("div", { className: "cms-nav-mobile-head", children: [_jsx("span", { children: props.title ?? 'Navigation' }), _jsx("button", { ref: closeButtonRef, type: "button", className: "cms-nav-close", onClick: props.onClose, "aria-label": props.closeLabel ?? 'Close navigation', children: _jsx("svg", { viewBox: "0 0 18 18", fill: "none", stroke: "currentColor", strokeWidth: 1.5, children: _jsx("path", { d: "M5 5 13 13M13 5 5 13", strokeLinecap: "round" }) }) })] }));
102
+ }
103
+ function SidebarNav(props) {
104
+ const { activeId, base, collapsed, railTooltipProps } = useSidebar();
105
+ const sections = useNavSections();
106
+ return (_jsx(_Fragment, { children: props.groups.map((group, gi) => {
107
+ /**
108
+ * Rail mode force-expands every section, deliberately. Collapsed to a rail the group
109
+ * heading is a 1px divider with no visible label (see `.cms-nav.is-rail .cms-group`), so a
110
+ * closed section there would be items missing from the nav with nothing on screen to
111
+ * indicate a control exists, let alone where. The stored preference is untouched and
112
+ * returns intact when the rail expands — this only overrides what is rendered.
113
+ */
114
+ const collapsible = Boolean(group.label) && !collapsed && (props.collapsibleGroups ?? []).includes(group.label);
115
+ const open = !collapsible || sections.isOpen(group.label);
116
+ const panelId = `cms-section-${(group.label ?? `top-${gi}`).toLowerCase().replace(/[^a-z0-9]+/g, '-')}`;
117
+ return (_jsxs("div", { className: "cms-section", children: [group.label && collapsible ? (
118
+ // A real `<button>`, not a click-handled `<div>`: keyboard focus, Enter/Space
119
+ // activation, and the button role all come free, and `aria-expanded` only carries its
120
+ // meaning on an element that is actually a control.
121
+ _jsxs("button", { type: "button", className: `cms-group cms-group-toggle${open ? '' : ' is-closed'}`, "aria-expanded": open, "aria-controls": panelId, onClick: () => sections.toggle(group.label), children: [_jsx("span", { children: group.label }), _jsx("svg", { className: "cms-group-chevron", viewBox: "0 0 18 18", fill: "none", "aria-hidden": "true", children: _jsx("path", { d: "M5 7l4 4 4-4", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round" }) })] })) : group.label ? (_jsx("div", { className: "cms-group", children: group.label })) : null, _jsx("div", { className: "cms-section-items", id: panelId, hidden: !open, children: group.items.map((item) => {
122
+ const active = item.id === activeId;
123
+ const href = adminHref(item.href, base);
124
+ if (props.renderItem) {
125
+ return _jsx(Fragment, { children: props.renderItem(item, { active, href, collapsed }) }, item.id);
126
+ }
127
+ if (item.soon && !item.soonPreviewable) {
128
+ return (_jsxs("div", { className: "cms-item is-soon", "aria-disabled": "true", children: [_jsx(Icon, { markup: item.icon ?? '' }), _jsx("span", { children: item.label }), _jsx("span", { className: "soon", children: props.soonLabel ?? 'Soon' })] }, item.id));
129
+ }
130
+ if (item.soon) {
131
+ // `soonPreviewable`: same real `<a>` as a shipped row (active state, aria-current,
132
+ // rail tooltip, cmd-click/copy-link all work identically) with the `soon` badge kept
133
+ // alongside the label — the announcement stays honest, but the row is no longer inert.
134
+ return (_jsxs("a", { className: `cms-item is-soon${active ? ' active' : ''}`, href: href, "aria-current": active ? 'page' : undefined, ...railTooltipProps(item.label), children: [_jsx(Icon, { markup: item.icon ?? '' }), _jsx("span", { children: item.label }), _jsx("span", { className: "soon", children: props.soonLabel ?? 'Soon' })] }, item.id));
135
+ }
136
+ return (_jsxs("a", { className: `cms-item${active ? ' active' : ''}`,
137
+ // `AdminNavItem.href` is a route path; the base is applied here so the panel
138
+ // manifest never has to know it. A real URL, so cmd-click and "copy link address"
139
+ // behave normally.
140
+ href: href, "aria-current": active ? 'page' : undefined, ...railTooltipProps(item.label), children: [_jsx(Icon, { markup: item.icon ?? '' }), _jsx("span", { children: item.label })] }, item.id));
141
+ }) })] }, group.label ?? `top-${gi}`));
142
+ }) }));
143
+ }
144
+ /** The bottom region. Carries no content of its own — the host decides what belongs there. */
145
+ function SidebarFooter(props) {
146
+ return _jsx("div", { className: ['cms-foot', props.className].filter(Boolean).join(' '), children: props.children });
147
+ }
148
+ /**
149
+ * Desktop rail collapse control. A real accessible name plus `aria-expanded` — not a bare icon
150
+ * button. Typically hidden by the host stylesheet inside the mobile drawer, which has its own close
151
+ * control and no rail concept.
152
+ */
153
+ function SidebarRailToggle(props) {
154
+ const { collapsed, toggleRail } = useSidebar();
155
+ const label = collapsed ? (props.expandLabel ?? 'Expand sidebar') : (props.collapseLabel ?? 'Collapse sidebar');
156
+ return (_jsx("div", { className: "cms-rail-toggle-row", children: _jsx("button", { type: "button", className: "cms-rail-toggle", onClick: toggleRail, "aria-expanded": !collapsed, "aria-label": label, title: label, children: _jsx("svg", { viewBox: "0 0 18 18", fill: "none", stroke: "currentColor", strokeWidth: 1.5, "aria-hidden": "true", children: _jsx("path", { d: "M6.5 4.5 3.5 9l3 4.5M11.5 4.5 14.5 9l-3 4.5" }) }) }) }));
157
+ }
158
+ /**
159
+ * Grouped admin sidebar. See the file header for why this is a compound component and what the
160
+ * context costs.
161
+ */
162
+ export const Sidebar = Object.assign(SidebarRoot, {
163
+ MobileHeader: SidebarMobileHeader,
164
+ Nav: SidebarNav,
165
+ Footer: SidebarFooter,
166
+ RailToggle: SidebarRailToggle,
167
+ });
168
+ //# sourceMappingURL=Sidebar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Sidebar.js","sourceRoot":"","sources":["../../../src/react/components/Sidebar.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * @file Per-section open/closed state for the sidebar's grouped nav (persisted).
3
+ *
4
+ * Sibling of `use-sidebar-rail.ts` and deliberately modelled on it — same defensive `localStorage`
5
+ * access, same "absent is not false" rule, same cross-tab sync. Read that file's header first; this
6
+ * one only records where the two differ.
7
+ *
8
+ * ## Why one key holding a map, rather than one key per section
9
+ *
10
+ * The unit a user thinks in is "how my sidebar is arranged", not "the Content section specifically".
11
+ * A single JSON map is written and read atomically, so a tab that toggles two sections in quick
12
+ * succession can never leave storage half-updated, and the cross-tab listener below fires once with
13
+ * the whole picture instead of once per section with no ordering guarantee between them. It also
14
+ * means adding a collapsible section later costs no new key and no migration.
15
+ *
16
+ * The cost is that a corrupt or hand-edited value loses every section's state at once rather than
17
+ * one. That is the right trade here: the fallback is "everything open", which is exactly the
18
+ * pre-accordion behavior and therefore never leaves the user unable to reach a nav item.
19
+ *
20
+ * ## Why the default is open
21
+ *
22
+ * A section whose state has never been stored renders open, matching how the nav behaved before it
23
+ * was collapsible. A closed-by-default accordion would hide working navigation from an operator who
24
+ * never asked for it and has no memory of a section existing — the one failure mode that turns a
25
+ * convenience into a support ticket.
26
+ */
27
+ /** Storage key used when the host does not supply one. Namespaced to this package for the same
28
+ * reason {@link DEFAULT_SIDEBAR_RAIL_STORAGE_KEY} is — two products embedding the admin under one
29
+ * origin must not silently share a layout preference. */
30
+ export declare const DEFAULT_NAV_SECTIONS_STORAGE_KEY = "jini-admin-nav-sections";
31
+ /** `{ [groupLabel]: isOpen }`. A label absent from the map has no stored preference and defaults to
32
+ * open — see this file's header. */
33
+ export type NavSectionState = Readonly<Record<string, boolean>>;
34
+ export interface NavSections {
35
+ /** `true` when the section should render its items. Any label with no stored preference is open. */
36
+ isOpen: (groupLabel: string) => boolean;
37
+ toggle: (groupLabel: string) => void;
38
+ }
39
+ /**
40
+ * @param storageKey `localStorage` key to persist under. Defaults to
41
+ * {@link DEFAULT_NAV_SECTIONS_STORAGE_KEY}. Read on first render only — same contract as
42
+ * `useSidebarRail`'s, for the same reason (no host changes this mid-session).
43
+ *
44
+ * @complexity O(1) per `isOpen` (one object lookup) and O(s) per `toggle` in the number of stored
45
+ * sections, from the single JSON serialization. `s` is the count of collapsible nav sections — a
46
+ * handful, fixed by the host's panel manifest, never user-variable.
47
+ * @overallScore 100
48
+ */
49
+ export declare function useNavSections(storageKey?: string): NavSections;
50
+ //# sourceMappingURL=use-nav-sections.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-nav-sections.d.ts","sourceRoot":"","sources":["../../../src/react/hooks/use-nav-sections.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH;;0DAE0D;AAC1D,eAAO,MAAM,gCAAgC,4BAA4B,CAAC;AAE1E;qCACqC;AACrC,MAAM,MAAM,eAAe,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAyChE,MAAM,WAAW,WAAW;IAC1B,oGAAoG;IACpG,MAAM,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC;IACxC,MAAM,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;CACtC;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,UAAU,GAAE,MAAyC,GAAG,WAAW,CAiCjG"}
@@ -0,0 +1,112 @@
1
+ import { useCallback, useEffect, useState } from 'react';
2
+ /**
3
+ * @file Per-section open/closed state for the sidebar's grouped nav (persisted).
4
+ *
5
+ * Sibling of `use-sidebar-rail.ts` and deliberately modelled on it — same defensive `localStorage`
6
+ * access, same "absent is not false" rule, same cross-tab sync. Read that file's header first; this
7
+ * one only records where the two differ.
8
+ *
9
+ * ## Why one key holding a map, rather than one key per section
10
+ *
11
+ * The unit a user thinks in is "how my sidebar is arranged", not "the Content section specifically".
12
+ * A single JSON map is written and read atomically, so a tab that toggles two sections in quick
13
+ * succession can never leave storage half-updated, and the cross-tab listener below fires once with
14
+ * the whole picture instead of once per section with no ordering guarantee between them. It also
15
+ * means adding a collapsible section later costs no new key and no migration.
16
+ *
17
+ * The cost is that a corrupt or hand-edited value loses every section's state at once rather than
18
+ * one. That is the right trade here: the fallback is "everything open", which is exactly the
19
+ * pre-accordion behavior and therefore never leaves the user unable to reach a nav item.
20
+ *
21
+ * ## Why the default is open
22
+ *
23
+ * A section whose state has never been stored renders open, matching how the nav behaved before it
24
+ * was collapsible. A closed-by-default accordion would hide working navigation from an operator who
25
+ * never asked for it and has no memory of a section existing — the one failure mode that turns a
26
+ * convenience into a support ticket.
27
+ */
28
+ /** Storage key used when the host does not supply one. Namespaced to this package for the same
29
+ * reason {@link DEFAULT_SIDEBAR_RAIL_STORAGE_KEY} is — two products embedding the admin under one
30
+ * origin must not silently share a layout preference. */
31
+ export const DEFAULT_NAV_SECTIONS_STORAGE_KEY = 'jini-admin-nav-sections';
32
+ /**
33
+ * Reads the persisted map once, defensively. `localStorage` can throw in a locked-down embed
34
+ * (hardened browser configs, historically Safari private mode), the key may be absent on first run,
35
+ * and the value may be unparseable or the wrong shape if it was hand-edited or written by an older
36
+ * version.
37
+ *
38
+ * Every one of those resolves to an empty map — "no stored preference for anything" — rather than a
39
+ * thrown error, because the consequence of guessing wrong here is a nav that renders with all
40
+ * sections open, which is the safe direction. A malformed value is deliberately not repaired or
41
+ * re-written; the next real toggle overwrites it wholesale.
42
+ *
43
+ * Non-boolean entries are dropped individually rather than rejecting the whole map, so one bad key
44
+ * cannot discard a user's other sections.
45
+ */
46
+ function readPersisted(storageKey) {
47
+ try {
48
+ const raw = localStorage.getItem(storageKey);
49
+ if (raw === null)
50
+ return {};
51
+ const parsed = JSON.parse(raw);
52
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
53
+ return {};
54
+ const clean = {};
55
+ for (const [label, value] of Object.entries(parsed)) {
56
+ if (typeof value === 'boolean')
57
+ clean[label] = value;
58
+ }
59
+ return clean;
60
+ }
61
+ catch {
62
+ return {};
63
+ }
64
+ }
65
+ function writePersisted(storageKey, state) {
66
+ try {
67
+ localStorage.setItem(storageKey, JSON.stringify(state));
68
+ }
69
+ catch {
70
+ // Best-effort, exactly as in `use-sidebar-rail.ts`: a user who cannot persist still gets a
71
+ // working accordion for this tab, and failing loudly would break the nav over a preference.
72
+ }
73
+ }
74
+ /**
75
+ * @param storageKey `localStorage` key to persist under. Defaults to
76
+ * {@link DEFAULT_NAV_SECTIONS_STORAGE_KEY}. Read on first render only — same contract as
77
+ * `useSidebarRail`'s, for the same reason (no host changes this mid-session).
78
+ *
79
+ * @complexity O(1) per `isOpen` (one object lookup) and O(s) per `toggle` in the number of stored
80
+ * sections, from the single JSON serialization. `s` is the count of collapsible nav sections — a
81
+ * handful, fixed by the host's panel manifest, never user-variable.
82
+ * @overallScore 100
83
+ */
84
+ export function useNavSections(storageKey = DEFAULT_NAV_SECTIONS_STORAGE_KEY) {
85
+ const [state, setState] = useState(() => readPersisted(storageKey));
86
+ const isOpen = useCallback((groupLabel) => state[groupLabel] ?? true, [state]);
87
+ const toggle = useCallback((groupLabel) => {
88
+ setState((current) => {
89
+ // `?? true` here and in `isOpen` must agree: toggling a section that has never been stored
90
+ // has to close it (it is currently rendered open), not open an already-open section — which
91
+ // is what a bare `!current[label]` would do on the `undefined` first press.
92
+ const next = { ...current, [groupLabel]: !(current[groupLabel] ?? true) };
93
+ writePersisted(storageKey, next);
94
+ return next;
95
+ });
96
+ }, [storageKey]);
97
+ /** Cross-tab sync — two admin tabs should agree on the layout rather than diverging the moment
98
+ * either one collapses a section. Mirrors `useSidebarRail`'s listener, including treating a
99
+ * removed key (`newValue === null`, another tab clearing storage) as "no stored preference"
100
+ * rather than as "everything closed". */
101
+ useEffect(() => {
102
+ function onStorage(event) {
103
+ if (event.key !== storageKey)
104
+ return;
105
+ setState(event.newValue === null ? {} : readPersisted(storageKey));
106
+ }
107
+ window.addEventListener('storage', onStorage);
108
+ return () => window.removeEventListener('storage', onStorage);
109
+ }, [storageKey]);
110
+ return { isOpen, toggle };
111
+ }
112
+ //# sourceMappingURL=use-nav-sections.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-nav-sections.js","sourceRoot":"","sources":["../../../src/react/hooks/use-nav-sections.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * @file Desktop sidebar rail-collapse preference (persisted).
3
+ *
4
+ * Deliberately its own hook rather than a `useState` inline in `Sidebar.tsx` — the persistence and
5
+ * the cross-tab sync below are both real behavior worth naming and testing on their own.
6
+ *
7
+ * This is the DESKTOP collapse: a user preference, both states fully usable, persisted so it
8
+ * survives navigation and reload. It is a different mechanism from the mobile off-canvas drawer
9
+ * (`Sidebar`'s `open`/`onClose` props, which the host owns): that one is session-only overlay state
10
+ * that *should* reset on every navigation, where resetting this preference on navigation would be
11
+ * the bug the original design called out — a collapse that resets on every page change is worse
12
+ * than no collapse. Driving both off one boolean would make each navigation either fight the user's
13
+ * rail choice or leave the phone drawer stuck open; they are two pieces of state on purpose.
14
+ */
15
+ /** Storage key used when the host does not supply one. Namespaced to this package so two products
16
+ * embedding the admin under one origin do not silently share a preference.
17
+ *
18
+ * A host migrating from its own pre-existing key should pass that key to `useSidebarRail` rather
19
+ * than accept this default — switching keys reads as "every operator's collapsed rail sprang back
20
+ * open", which is indistinguishable from a regression. */
21
+ export declare const DEFAULT_SIDEBAR_RAIL_STORAGE_KEY = "jini-admin-sidebar-rail-collapsed";
22
+ export interface SidebarRail {
23
+ collapsed: boolean;
24
+ toggle: () => void;
25
+ }
26
+ /**
27
+ * @param storageKey `localStorage` key to persist under. Defaults to
28
+ * {@link DEFAULT_SIDEBAR_RAIL_STORAGE_KEY}. Read on first render only — changing it on a mounted
29
+ * hook re-points the *writes* and the cross-tab listener, but does not re-read the new key's value,
30
+ * since a key that changes mid-session is not a case any host has.
31
+ *
32
+ * @param defaultCollapsed State for a visitor with **no stored preference**. Defaults to `false`
33
+ * (expanded), which is what every existing host gets by not passing it. A stored preference always
34
+ * takes precedence — see {@link readPersisted} for why absent and `'0'` must stay distinguishable.
35
+ *
36
+ * @complexity O(1) — one state read/write per toggle, no derived computation.
37
+ */
38
+ export declare function useSidebarRail(storageKey?: string, defaultCollapsed?: boolean): SidebarRail;
39
+ //# sourceMappingURL=use-sidebar-rail.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-sidebar-rail.d.ts","sourceRoot":"","sources":["../../../src/react/hooks/use-sidebar-rail.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;GAaG;AAEH;;;;;2DAK2D;AAC3D,eAAO,MAAM,gCAAgC,sCAAsC,CAAC;AAiCpF,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,OAAO,CAAC;IACnB,MAAM,EAAE,MAAM,IAAI,CAAC;CACpB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAC5B,UAAU,GAAE,MAAyC,EACrD,gBAAgB,UAAQ,GACvB,WAAW,CA2Bb"}
@@ -0,0 +1,93 @@
1
+ import { useCallback, useEffect, useState } from 'react';
2
+ /**
3
+ * @file Desktop sidebar rail-collapse preference (persisted).
4
+ *
5
+ * Deliberately its own hook rather than a `useState` inline in `Sidebar.tsx` — the persistence and
6
+ * the cross-tab sync below are both real behavior worth naming and testing on their own.
7
+ *
8
+ * This is the DESKTOP collapse: a user preference, both states fully usable, persisted so it
9
+ * survives navigation and reload. It is a different mechanism from the mobile off-canvas drawer
10
+ * (`Sidebar`'s `open`/`onClose` props, which the host owns): that one is session-only overlay state
11
+ * that *should* reset on every navigation, where resetting this preference on navigation would be
12
+ * the bug the original design called out — a collapse that resets on every page change is worse
13
+ * than no collapse. Driving both off one boolean would make each navigation either fight the user's
14
+ * rail choice or leave the phone drawer stuck open; they are two pieces of state on purpose.
15
+ */
16
+ /** Storage key used when the host does not supply one. Namespaced to this package so two products
17
+ * embedding the admin under one origin do not silently share a preference.
18
+ *
19
+ * A host migrating from its own pre-existing key should pass that key to `useSidebarRail` rather
20
+ * than accept this default — switching keys reads as "every operator's collapsed rail sprang back
21
+ * open", which is indistinguishable from a regression. */
22
+ export const DEFAULT_SIDEBAR_RAIL_STORAGE_KEY = 'jini-admin-sidebar-rail-collapsed';
23
+ /**
24
+ * Reads the persisted value once, defensively — `localStorage` can throw in a locked-down
25
+ * embed/iframe context (Safari private mode historically, some hardened browser configs), and a
26
+ * first-run visitor has no key at all.
27
+ *
28
+ * Returns `undefined` for "no stored preference" rather than collapsing it into `false`. That
29
+ * distinction is load-bearing once a host can default to collapsed (`defaultCollapsed` below): if
30
+ * absent and `'0'` both read as `false`, they behave identically under an expanded default but
31
+ * diverge sharply under a collapsed one — an operator who deliberately expanded the rail would find
32
+ * it collapsed again on every reload, because their explicit `'0'` would be indistinguishable from
33
+ * never having chosen. A stored value always wins over the default; only its absence defers.
34
+ */
35
+ function readPersisted(storageKey) {
36
+ try {
37
+ const raw = localStorage.getItem(storageKey);
38
+ if (raw === null)
39
+ return undefined;
40
+ return raw === '1';
41
+ }
42
+ catch {
43
+ return undefined;
44
+ }
45
+ }
46
+ function writePersisted(storageKey, collapsed) {
47
+ try {
48
+ localStorage.setItem(storageKey, collapsed ? '1' : '0');
49
+ }
50
+ catch {
51
+ // Best-effort — a user who cannot persist the preference still gets a working toggle for the
52
+ // current tab; failing loudly here would break the rail over a non-essential preference write.
53
+ }
54
+ }
55
+ /**
56
+ * @param storageKey `localStorage` key to persist under. Defaults to
57
+ * {@link DEFAULT_SIDEBAR_RAIL_STORAGE_KEY}. Read on first render only — changing it on a mounted
58
+ * hook re-points the *writes* and the cross-tab listener, but does not re-read the new key's value,
59
+ * since a key that changes mid-session is not a case any host has.
60
+ *
61
+ * @param defaultCollapsed State for a visitor with **no stored preference**. Defaults to `false`
62
+ * (expanded), which is what every existing host gets by not passing it. A stored preference always
63
+ * takes precedence — see {@link readPersisted} for why absent and `'0'` must stay distinguishable.
64
+ *
65
+ * @complexity O(1) — one state read/write per toggle, no derived computation.
66
+ */
67
+ export function useSidebarRail(storageKey = DEFAULT_SIDEBAR_RAIL_STORAGE_KEY, defaultCollapsed = false) {
68
+ const [collapsed, setCollapsed] = useState(() => readPersisted(storageKey) ?? defaultCollapsed);
69
+ const toggle = useCallback(() => {
70
+ setCollapsed((current) => {
71
+ const next = !current;
72
+ writePersisted(storageKey, next);
73
+ return next;
74
+ });
75
+ }, [storageKey]);
76
+ /** Cross-tab sync: two admin tabs open side by side should agree on the rail state rather than
77
+ * silently diverging the moment either one is toggled. */
78
+ useEffect(() => {
79
+ function onStorage(event) {
80
+ if (event.key !== storageKey)
81
+ return;
82
+ // `newValue` is null when the key is REMOVED (another tab clearing storage), which is the
83
+ // same "no stored preference" state `readPersisted` defers on — so fall back to the host's
84
+ // default rather than hard-coding expanded, or a clear in one tab would silently override a
85
+ // collapsed-by-default host in every other one.
86
+ setCollapsed(event.newValue === null ? defaultCollapsed : event.newValue === '1');
87
+ }
88
+ window.addEventListener('storage', onStorage);
89
+ return () => window.removeEventListener('storage', onStorage);
90
+ }, [storageKey, defaultCollapsed]);
91
+ return { collapsed, toggle };
92
+ }
93
+ //# sourceMappingURL=use-sidebar-rail.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-sidebar-rail.js","sourceRoot":"","sources":["../../../src/react/hooks/use-sidebar-rail.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAEzD;;;;;;;;;;;;;GAaG;AAEH;;;;;2DAK2D;AAC3D,MAAM,CAAC,MAAM,gCAAgC,GAAG,mCAAmC,CAAC;AAEpF;;;;;;;;;;;GAWG;AACH,SAAS,aAAa,CAAC,UAAkB;IACvC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,YAAY,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;QAC7C,IAAI,GAAG,KAAK,IAAI;YAAE,OAAO,SAAS,CAAC;QACnC,OAAO,GAAG,KAAK,GAAG,CAAC;IACrB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED,SAAS,cAAc,CAAC,UAAkB,EAAE,SAAkB;IAC5D,IAAI,CAAC;QACH,YAAY,CAAC,OAAO,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IAC1D,CAAC;IAAC,MAAM,CAAC;QACP,6FAA6F;QAC7F,+FAA+F;IACjG,CAAC;AACH,CAAC;AAOD;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,cAAc,CAC5B,aAAqB,gCAAgC,EACrD,gBAAgB,GAAG,KAAK;IAExB,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,QAAQ,CAAU,GAAG,EAAE,CAAC,aAAa,CAAC,UAAU,CAAC,IAAI,gBAAgB,CAAC,CAAC;IAEzG,MAAM,MAAM,GAAG,WAAW,CAAC,GAAG,EAAE;QAC9B,YAAY,CAAC,CAAC,OAAO,EAAE,EAAE;YACvB,MAAM,IAAI,GAAG,CAAC,OAAO,CAAC;YACtB,cAAc,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;YACjC,OAAO,IAAI,CAAC;QACd,CAAC,CAAC,CAAC;IACL,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC;IAEjB;+DAC2D;IAC3D,SAAS,CAAC,GAAG,EAAE;QACb,SAAS,SAAS,CAAC,KAAmB;YACpC,IAAI,KAAK,CAAC,GAAG,KAAK,UAAU;gBAAE,OAAO;YACrC,0FAA0F;YAC1F,2FAA2F;YAC3F,4FAA4F;YAC5F,gDAAgD;YAChD,YAAY,CAAC,KAAK,CAAC,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,KAAK,GAAG,CAAC,CAAC;QACpF,CAAC;QACD,MAAM,CAAC,gBAAgB,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;QAC9C,OAAO,GAAG,EAAE,CAAC,MAAM,CAAC,mBAAmB,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;IAChE,CAAC,EAAE,CAAC,UAAU,EAAE,gBAAgB,CAAC,CAAC,CAAC;IAEnC,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC;AAC/B,CAAC"}