@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.
- package/LICENSE +201 -0
- package/README.md +138 -0
- package/dist/browser/index.d.ts +8 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +8 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/browser/navigation.d.ts +39 -0
- package/dist/browser/navigation.d.ts.map +1 -0
- package/dist/browser/navigation.js +113 -0
- package/dist/browser/navigation.js.map +1 -0
- package/dist/core/gated/types.d.ts +87 -0
- package/dist/core/gated/types.d.ts.map +1 -0
- package/dist/core/gated/types.js +37 -0
- package/dist/core/gated/types.js.map +1 -0
- package/dist/core/index.d.ts +20 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +15 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/manifest/rules.d.ts +84 -0
- package/dist/core/manifest/rules.d.ts.map +1 -0
- package/dist/core/manifest/rules.js +130 -0
- package/dist/core/manifest/rules.js.map +1 -0
- package/dist/core/manifest/types.d.ts +154 -0
- package/dist/core/manifest/types.d.ts.map +1 -0
- package/dist/core/manifest/types.js +31 -0
- package/dist/core/manifest/types.js.map +1 -0
- package/dist/core/permissions/rules.d.ts +32 -0
- package/dist/core/permissions/rules.d.ts.map +1 -0
- package/dist/core/permissions/rules.js +34 -0
- package/dist/core/permissions/rules.js.map +1 -0
- package/dist/core/ports/analytics.d.ts +35 -0
- package/dist/core/ports/analytics.d.ts.map +1 -0
- package/dist/core/ports/analytics.js +14 -0
- package/dist/core/ports/analytics.js.map +1 -0
- package/dist/core/ports/auth.d.ts +49 -0
- package/dist/core/ports/auth.d.ts.map +1 -0
- package/dist/core/ports/auth.js +19 -0
- package/dist/core/ports/auth.js.map +1 -0
- package/dist/core/ports/comments.d.ts +90 -0
- package/dist/core/ports/comments.d.ts.map +1 -0
- package/dist/core/ports/comments.js +24 -0
- package/dist/core/ports/comments.js.map +1 -0
- package/dist/core/ports/database.d.ts +101 -0
- package/dist/core/ports/database.d.ts.map +1 -0
- package/dist/core/ports/database.js +2 -0
- package/dist/core/ports/database.js.map +1 -0
- package/dist/core/ports/forms.d.ts +130 -0
- package/dist/core/ports/forms.d.ts.map +1 -0
- package/dist/core/ports/forms.js +62 -0
- package/dist/core/ports/forms.js.map +1 -0
- package/dist/core/ports/identity.d.ts +114 -0
- package/dist/core/ports/identity.d.ts.map +1 -0
- package/dist/core/ports/identity.js +28 -0
- package/dist/core/ports/identity.js.map +1 -0
- package/dist/core/ports/index.d.ts +21 -0
- package/dist/core/ports/index.d.ts.map +1 -0
- package/dist/core/ports/index.js +6 -0
- package/dist/core/ports/index.js.map +1 -0
- package/dist/core/ports/integrations.d.ts +81 -0
- package/dist/core/ports/integrations.d.ts.map +1 -0
- package/dist/core/ports/integrations.js +21 -0
- package/dist/core/ports/integrations.js.map +1 -0
- package/dist/core/ports/media.d.ts +74 -0
- package/dist/core/ports/media.d.ts.map +1 -0
- package/dist/core/ports/media.js +28 -0
- package/dist/core/ports/media.js.map +1 -0
- package/dist/core/ports/members.d.ts +50 -0
- package/dist/core/ports/members.d.ts.map +1 -0
- package/dist/core/ports/members.js +17 -0
- package/dist/core/ports/members.js.map +1 -0
- package/dist/core/ports/menus.d.ts +179 -0
- package/dist/core/ports/menus.d.ts.map +1 -0
- package/dist/core/ports/menus.js +70 -0
- package/dist/core/ports/menus.js.map +1 -0
- package/dist/core/ports/plugins.d.ts +68 -0
- package/dist/core/ports/plugins.d.ts.map +1 -0
- package/dist/core/ports/plugins.js +27 -0
- package/dist/core/ports/plugins.js.map +1 -0
- package/dist/core/ports/recovery.d.ts +171 -0
- package/dist/core/ports/recovery.d.ts.map +1 -0
- package/dist/core/ports/recovery.js +2 -0
- package/dist/core/ports/recovery.js.map +1 -0
- package/dist/core/ports/redirects.d.ts +139 -0
- package/dist/core/ports/redirects.d.ts.map +1 -0
- package/dist/core/ports/redirects.js +55 -0
- package/dist/core/ports/redirects.js.map +1 -0
- package/dist/core/ports/seo.d.ts +147 -0
- package/dist/core/ports/seo.d.ts.map +1 -0
- package/dist/core/ports/seo.js +39 -0
- package/dist/core/ports/seo.js.map +1 -0
- package/dist/core/ports/settings.d.ts +87 -0
- package/dist/core/ports/settings.d.ts.map +1 -0
- package/dist/core/ports/settings.js +23 -0
- package/dist/core/ports/settings.js.map +1 -0
- package/dist/core/ports/workspace.d.ts +44 -0
- package/dist/core/ports/workspace.d.ts.map +1 -0
- package/dist/core/ports/workspace.js +29 -0
- package/dist/core/ports/workspace.js.map +1 -0
- package/dist/core/routing/rules.d.ts +54 -0
- package/dist/core/routing/rules.d.ts.map +1 -0
- package/dist/core/routing/rules.js +113 -0
- package/dist/core/routing/rules.js.map +1 -0
- package/dist/core/routing/types.d.ts +36 -0
- package/dist/core/routing/types.d.ts.map +1 -0
- package/dist/core/routing/types.js +17 -0
- package/dist/core/routing/types.js.map +1 -0
- package/dist/core/transport/errors.d.ts +53 -0
- package/dist/core/transport/errors.d.ts.map +1 -0
- package/dist/core/transport/errors.js +63 -0
- package/dist/core/transport/errors.js.map +1 -0
- package/dist/core/transport/http.d.ts +63 -0
- package/dist/core/transport/http.d.ts.map +1 -0
- package/dist/core/transport/http.js +63 -0
- package/dist/core/transport/http.js.map +1 -0
- package/dist/core/transport/index.d.ts +5 -0
- package/dist/core/transport/index.d.ts.map +1 -0
- package/dist/core/transport/index.js +3 -0
- package/dist/core/transport/index.js.map +1 -0
- package/dist/core/transport/types.d.ts +81 -0
- package/dist/core/transport/types.d.ts.map +1 -0
- package/dist/core/transport/types.js +43 -0
- package/dist/core/transport/types.js.map +1 -0
- package/dist/react/components/ConfirmButton/ConfirmButton.d.ts +40 -0
- package/dist/react/components/ConfirmButton/ConfirmButton.d.ts.map +1 -0
- package/dist/react/components/ConfirmButton/ConfirmButton.hooks.d.ts +22 -0
- package/dist/react/components/ConfirmButton/ConfirmButton.hooks.d.ts.map +1 -0
- package/dist/react/components/ConfirmButton/ConfirmButton.hooks.js +59 -0
- package/dist/react/components/ConfirmButton/ConfirmButton.hooks.js.map +1 -0
- package/dist/react/components/ConfirmButton/ConfirmButton.js +40 -0
- package/dist/react/components/ConfirmButton/ConfirmButton.js.map +1 -0
- package/dist/react/components/ConfirmButton.d.ts +39 -0
- package/dist/react/components/ConfirmButton.d.ts.map +1 -0
- package/dist/react/components/ConfirmButton.js +79 -0
- package/dist/react/components/ConfirmButton.js.map +1 -0
- package/dist/react/components/ConfirmDialog/ConfirmDialog.d.ts +90 -0
- package/dist/react/components/ConfirmDialog/ConfirmDialog.d.ts.map +1 -0
- package/dist/react/components/ConfirmDialog/ConfirmDialog.hooks.d.ts +49 -0
- package/dist/react/components/ConfirmDialog/ConfirmDialog.hooks.d.ts.map +1 -0
- package/dist/react/components/ConfirmDialog/ConfirmDialog.hooks.js +81 -0
- package/dist/react/components/ConfirmDialog/ConfirmDialog.hooks.js.map +1 -0
- package/dist/react/components/ConfirmDialog/ConfirmDialog.js +92 -0
- package/dist/react/components/ConfirmDialog/ConfirmDialog.js.map +1 -0
- package/dist/react/components/ConfirmDialog.d.ts +57 -0
- package/dist/react/components/ConfirmDialog.d.ts.map +1 -0
- package/dist/react/components/ConfirmDialog.js +85 -0
- package/dist/react/components/ConfirmDialog.js.map +1 -0
- package/dist/react/components/DataTable.d.ts +97 -0
- package/dist/react/components/DataTable.d.ts.map +1 -0
- package/dist/react/components/DataTable.js +20 -0
- package/dist/react/components/DataTable.js.map +1 -0
- package/dist/react/components/InteractiveHtmlEditor/InteractiveHtmlEditor.d.ts +25 -0
- package/dist/react/components/InteractiveHtmlEditor/InteractiveHtmlEditor.d.ts.map +1 -0
- package/dist/react/components/InteractiveHtmlEditor/InteractiveHtmlEditor.js +148 -0
- package/dist/react/components/InteractiveHtmlEditor/InteractiveHtmlEditor.js.map +1 -0
- package/dist/react/components/RowMenu/RowMenu.d.ts +77 -0
- package/dist/react/components/RowMenu/RowMenu.d.ts.map +1 -0
- package/dist/react/components/RowMenu/RowMenu.hooks.d.ts +30 -0
- package/dist/react/components/RowMenu/RowMenu.hooks.d.ts.map +1 -0
- package/dist/react/components/RowMenu/RowMenu.hooks.js +154 -0
- package/dist/react/components/RowMenu/RowMenu.hooks.js.map +1 -0
- package/dist/react/components/RowMenu/RowMenu.js +46 -0
- package/dist/react/components/RowMenu/RowMenu.js.map +1 -0
- package/dist/react/components/RowMenu.d.ts +44 -0
- package/dist/react/components/RowMenu.d.ts.map +1 -0
- package/dist/react/components/RowMenu.js +149 -0
- package/dist/react/components/RowMenu.js.map +1 -0
- package/dist/react/components/Sidebar.d.ts +128 -0
- package/dist/react/components/Sidebar.d.ts.map +1 -0
- package/dist/react/components/Sidebar.js +168 -0
- package/dist/react/components/Sidebar.js.map +1 -0
- package/dist/react/hooks/use-nav-sections.d.ts +50 -0
- package/dist/react/hooks/use-nav-sections.d.ts.map +1 -0
- package/dist/react/hooks/use-nav-sections.js +112 -0
- package/dist/react/hooks/use-nav-sections.js.map +1 -0
- package/dist/react/hooks/use-sidebar-rail.d.ts +39 -0
- package/dist/react/hooks/use-sidebar-rail.d.ts.map +1 -0
- package/dist/react/hooks/use-sidebar-rail.js +93 -0
- package/dist/react/hooks/use-sidebar-rail.js.map +1 -0
- package/dist/react/index.d.ts +53 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +53 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react/types.d.ts +28 -0
- package/dist/react/types.d.ts.map +1 -0
- package/dist/react/types.js +23 -0
- package/dist/react/types.js.map +1 -0
- package/dist/server/composio/bounded-data.d.ts +36 -0
- package/dist/server/composio/bounded-data.d.ts.map +1 -0
- package/dist/server/composio/bounded-data.js +181 -0
- package/dist/server/composio/bounded-data.js.map +1 -0
- package/dist/server/composio/catalog.d.ts +147 -0
- package/dist/server/composio/catalog.d.ts.map +1 -0
- package/dist/server/composio/catalog.js +126 -0
- package/dist/server/composio/catalog.js.map +1 -0
- package/dist/server/composio/composio-config.d.ts +42 -0
- package/dist/server/composio/composio-config.d.ts.map +1 -0
- package/dist/server/composio/composio-config.js +145 -0
- package/dist/server/composio/composio-config.js.map +1 -0
- package/dist/server/composio/composio-descriptions.d.ts +16 -0
- package/dist/server/composio/composio-descriptions.d.ts.map +1 -0
- package/dist/server/composio/composio-descriptions.js +766 -0
- package/dist/server/composio/composio-descriptions.js.map +1 -0
- package/dist/server/composio/composio.d.ts +364 -0
- package/dist/server/composio/composio.d.ts.map +1 -0
- package/dist/server/composio/composio.js +1902 -0
- package/dist/server/composio/composio.js.map +1 -0
- package/dist/server/composio/errors.d.ts +15 -0
- package/dist/server/composio/errors.d.ts.map +1 -0
- package/dist/server/composio/errors.js +16 -0
- package/dist/server/composio/errors.js.map +1 -0
- package/dist/server/composio/file-lock.d.ts +13 -0
- package/dist/server/composio/file-lock.d.ts.map +1 -0
- package/dist/server/composio/file-lock.js +92 -0
- package/dist/server/composio/file-lock.js.map +1 -0
- package/dist/server/composio/index.d.ts +18 -0
- package/dist/server/composio/index.d.ts.map +1 -0
- package/dist/server/composio/index.js +7 -0
- package/dist/server/composio/index.js.map +1 -0
- package/dist/server/composio/json-schema.d.ts +93 -0
- package/dist/server/composio/json-schema.d.ts.map +1 -0
- package/dist/server/composio/json-schema.js +498 -0
- package/dist/server/composio/json-schema.js.map +1 -0
- package/dist/server/composio/output-protection.d.ts +32 -0
- package/dist/server/composio/output-protection.d.ts.map +1 -0
- package/dist/server/composio/output-protection.js +105 -0
- package/dist/server/composio/output-protection.js.map +1 -0
- package/dist/server/composio/service.d.ts +181 -0
- package/dist/server/composio/service.d.ts.map +1 -0
- package/dist/server/composio/service.js +758 -0
- package/dist/server/composio/service.js.map +1 -0
- package/dist/server/index.d.ts +15 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +15 -0
- package/dist/server/index.js.map +1 -0
- 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"}
|