@aglyn/shared-ui-next 1.0.0-beta.231 → 1.0.0-beta.232

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/shared-ui-next",
3
- "version": "1.0.0-beta.231",
3
+ "version": "1.0.0-beta.232",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,11 +25,11 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/shared-data-enums": "1.0.0-beta.231",
29
- "@aglyn/shared-ui-jsx": "1.0.0-beta.231",
30
- "@aglyn/shared-ui-theme": "1.0.0-beta.231",
31
- "@aglyn/shared-util-dom": "1.0.0-beta.231",
32
- "@aglyn/shared-util-tools": "1.0.0-beta.231",
28
+ "@aglyn/shared-data-enums": "1.0.0-beta.232",
29
+ "@aglyn/shared-ui-jsx": "1.0.0-beta.232",
30
+ "@aglyn/shared-ui-theme": "1.0.0-beta.232",
31
+ "@aglyn/shared-util-dom": "1.0.0-beta.232",
32
+ "@aglyn/shared-util-tools": "1.0.0-beta.232",
33
33
  "@swc/helpers": "0.5.23"
34
34
  },
35
35
  "peerDependencies": {
@@ -32,9 +32,12 @@ import { useTabParam } from "../hooks/use-tab-param.js";
32
32
  */ function useRailLayout(forceStacked = false) {
33
33
  const theme = useTheme();
34
34
  const narrow = useMediaQuery(theme.breakpoints.down('sm'));
35
- const stacked = forceStacked || narrow;
35
+ // A vertical rail needs a quarter of the row; on a tablet that is ~180px
36
+ // and its labels wrap word by word, so the strip stays on top until `md`.
37
+ const stacked = useMediaQuery(theme.breakpoints.down('md')) || forceStacked;
36
38
  return {
37
39
  stacked,
40
+ narrow,
38
41
  /** Props every rail passes to its `Tabs`/`TabList`. */ tabsProps: {
39
42
  orientation: stacked ? 'horizontal' : 'vertical',
40
43
  variant: stacked ? 'scrollable' : 'standard',
@@ -61,7 +64,7 @@ import { useTabParam } from "../hooks/use-tab-param.js";
61
64
  * (unless `lazy`, which defers un-visited panels — see the prop).
62
65
  */ export function HubTabs(props) {
63
66
  const { tabs, navHeader = 'Navigation', lazy = false } = props;
64
- const { tabsProps } = useRailLayout();
67
+ const { narrow, tabsProps } = useRailLayout();
65
68
  /*
66
69
  * The SHARED resolver, not a second reading of the same parameter
67
70
  * (AGL-2486). This rail used to hold the incoming id in `useState`, which
@@ -113,16 +116,22 @@ import { useTabParam } from "../hooks/use-tab-param.js";
113
116
  children: /*#__PURE__*/ _jsx(GridItems, {
114
117
  // A navigation column beside its content, not a set of cards.
115
118
  masonry: false,
116
- spacing: 3,
119
+ spacing: {
120
+ xs: 2,
121
+ sm: 3
122
+ },
117
123
  items: [
118
124
  {
119
125
  size: {
120
126
  xs: 12,
121
- sm: 3
127
+ md: 3
122
128
  },
123
- children: /*#__PURE__*/ _jsx(CardDisplay, {
124
- header: navHeader,
129
+ children: // On a phone the strip says what it is on its own; a
130
+ // "Navigation" title above it is the first 60px of the screen.
131
+ /*#__PURE__*/ _jsx(CardDisplay, {
132
+ header: narrow ? undefined : navHeader,
125
133
  children: /*#__PURE__*/ _jsx(TabList, _extends({}, tabsProps, {
134
+ "aria-label": navHeader,
126
135
  onChange: handleChange,
127
136
  children: tabs.map((item)=>/*#__PURE__*/ _jsx(Tab, {
128
137
  value: item.id,
@@ -134,7 +143,7 @@ import { useTabParam } from "../hooks/use-tab-param.js";
134
143
  {
135
144
  size: {
136
145
  xs: 12,
137
- sm: 9
146
+ md: 9
138
147
  },
139
148
  children: /*#__PURE__*/ _jsx(_Fragment, {
140
149
  children: tabs.map((item)=>/*#__PURE__*/ _jsx(TabPanel, {
@@ -247,7 +256,7 @@ export function HubSections(props) {
247
256
  var _ref;
248
257
  var _useActiveSection;
249
258
  const { sections, children, navHeader = 'Navigation', wide = false } = props;
250
- const { tabsProps } = useRailLayout(wide);
259
+ const { narrow, tabsProps } = useRailLayout(wide);
251
260
  const shown = useMemo(()=>sections.filter((section)=>section.visible !== false), [
252
261
  sections
253
262
  ]);
@@ -255,18 +264,22 @@ export function HubSections(props) {
255
264
  return /*#__PURE__*/ _jsx(GridItems, {
256
265
  // The same navigation-beside-content split as above.
257
266
  masonry: false,
258
- spacing: 3,
267
+ spacing: {
268
+ xs: 2,
269
+ sm: 3
270
+ },
259
271
  items: [
260
272
  {
261
273
  size: wide ? {
262
274
  xs: 12
263
275
  } : {
264
276
  xs: 12,
265
- sm: 3
277
+ md: 3
266
278
  },
267
279
  children: /*#__PURE__*/ _jsx(CardDisplay, {
268
- header: navHeader,
280
+ header: narrow ? undefined : navHeader,
269
281
  children: /*#__PURE__*/ _jsx(Tabs, _extends({}, tabsProps, {
282
+ "aria-label": navHeader,
270
283
  value: activeHref != null ? activeHref : false,
271
284
  children: shown.map((section)=>/*#__PURE__*/ _jsx(Tab, {
272
285
  value: section.href,
@@ -285,7 +298,7 @@ export function HubSections(props) {
285
298
  xs: 12
286
299
  } : {
287
300
  xs: 12,
288
- sm: 9
301
+ md: 9
289
302
  },
290
303
  children
291
304
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../libs/shared/ui/next/src/lib/components/hub-tabs.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n AppLink,\n CardDisplay,\n GridItems,\n MdiIcon,\n mdiLockOutline,\n} from '@aglyn/shared-ui-jsx'\nimport { TabContext, TabList, TabPanel } from '@mui/lab'\nimport { Box, Tab, Tabs, useMediaQuery, useTheme } from '@mui/material'\nimport { usePathname } from 'next/navigation'\nimport {\n type ReactNode,\n useCallback,\n useEffect,\n useMemo,\n useState,\n} from 'react'\nimport { useTabParam } from '../hooks/use-tab-param'\n\n/**\n * The rail's own shape, shared by BOTH modes below (AGL-2501).\n *\n * `HubTabs` and `HubSections` are two ways of choosing a section and one way\n * of DRAWING that choice. Duplicating the card, the orientation switch and the\n * tab `sx` is how they drift into looking like two different products, which\n * is exactly what happened the first time this rail was rebuilt by hand.\n */\nfunction useRailLayout(forceStacked = false) {\n const theme = useTheme()\n const narrow = useMediaQuery(theme.breakpoints.down('sm'))\n const stacked = forceStacked || narrow\n return {\n stacked,\n /** Props every rail passes to its `Tabs`/`TabList`. */\n tabsProps: {\n orientation: stacked ? ('horizontal' as const) : ('vertical' as const),\n variant: stacked ? ('scrollable' as const) : ('standard' as const),\n allowScrollButtonsMobile: true,\n textColor: 'primary' as const,\n indicatorColor: 'primary' as const,\n sx: {\n ['.MuiTab-root']: {\n alignItems: stacked ? 'center' : 'start',\n maxWidth: 'unset',\n textTransform: 'none',\n },\n },\n },\n }\n}\n\nexport interface HubTab {\n id: string\n label: string\n content: ReactNode\n}\n\nexport interface HubTabsProps {\n tabs: HubTab[]\n /** Left nav card header (defaults to \"Navigation\"). */\n navHeader?: string\n /**\n * Defer mounting a panel's content until its tab is first activated, then\n * keep it mounted (AGL-785). Off by default so the standard behavior —\n * every panel mounted up front, subscriptions always live — is unchanged.\n * Opt in when the tabs host several data-heavy panels whose subscriptions\n * would otherwise all settle at once on load: mounting only the active\n * panel keeps that first-paint re-render burst small enough not to trip\n * React's nested-update limit. Panels stay mounted once visited, so\n * switching back is still instant.\n */\n lazy?: boolean\n}\n\n/**\n * Hub tab strip (AGL-354/382): the host-setup two-column pattern as a\n * shared component — a left \"Navigation\" CardDisplay with a vertical\n * TabList, content on the right. Collapses to horizontal tabs on small\n * screens. The active tab mirrors into the `?tab=` query param (shallow\n * replace) so hub views deep-link and survive back/forward; panels are\n * kept mounted so content and its data subscriptions are always present\n * (unless `lazy`, which defers un-visited panels — see the prop).\n */\nexport function HubTabs(props: HubTabsProps) {\n const { tabs, navHeader = 'Navigation', lazy = false } = props\n const { tabsProps } = useRailLayout()\n /*\n * The SHARED resolver, not a second reading of the same parameter\n * (AGL-2486). This rail used to hold the incoming id in `useState`, which\n * reads it once and then stops: back and forward are navigations between\n * two states of one mounted page, and a link into another section of a page\n * already open changes the parameter without remounting anything. Either\n * one left the rail on the old tab while the URL named a different one.\n *\n * `ids` is the tabs that exist right now, so an id naming a tab this hub\n * does not render falls back to the first rather than selecting a panel\n * nothing draws — which matters because several hubs build their tab list\n * from entitlements and render a different set per org.\n */\n const tabIds = useMemo(() => tabs.map((item) => item.id), [tabs])\n const { tab, onTabChange } = useTabParam({ ids: tabIds })\n // Which tabs have ever been active — the mount set when `lazy`. Seeded with\n // the tab resolved for first paint so it (and only it) mounts.\n const [activated, setActivated] = useState<Set<string>>(\n () => new Set(tab ? [tab] : []),\n )\n\n const handleChange = useCallback(\n (event: unknown, value: string) => {\n setActivated((prev) => (prev.has(value) ? prev : new Set(prev).add(value)))\n onTabChange(event, value)\n },\n [onTabChange],\n )\n\n /*\n * A panel reached by URL rather than by click has to stay in the mount set\n * too. `handleChange` is the only thing that grows the set, and it does not\n * fire when back/forward or an in-app link moves the parameter — so without\n * this, a `lazy` hub would drop such a panel again the moment the reader\n * moved on, and every return to it would remount and re-subscribe.\n *\n * The panel itself does not wait for this effect: the render below mounts\n * the ACTIVE tab unconditionally, so there is no frame in which the rail\n * shows a selected tab over an empty panel.\n */\n useEffect(() => {\n if (!lazy || !tab) return\n setActivated((prev) => (prev.has(tab) ? prev : new Set(prev).add(tab)))\n }, [lazy, tab])\n\n return (\n <TabContext value={tab}>\n <GridItems\n // A navigation column beside its content, not a set of cards.\n masonry={false}\n spacing={3}\n items={[\n {\n size: { xs: 12, sm: 3 },\n children: (\n <CardDisplay header={navHeader}>\n <TabList {...tabsProps} onChange={handleChange}>\n {tabs.map((item) => (\n <Tab key={item.id} value={item.id} label={item.label} />\n ))}\n </TabList>\n </CardDisplay>\n ),\n },\n {\n size: { xs: 12, sm: 9 },\n children: (\n <>\n {tabs.map((item) => (\n <TabPanel\n key={item.id}\n value={item.id}\n keepMounted\n sx={{ padding: 'unset' }}\n >\n {!lazy || item.id === tab || activated.has(item.id)\n ? item.content\n : null}\n </TabPanel>\n ))}\n </>\n ),\n },\n ]}\n />\n </TabContext>\n )\n}\nHubTabs.displayName = 'HubTabs'\n\nexport default HubTabs\n\n\nexport interface HubSection {\n /** Route this section lives at. What the rail links to. */\n href: string\n label: string\n /** Hidden entirely when false — a release or a role gate. */\n visible?: boolean\n /**\n * Shown and linked, with a lock — an entitlement the org's plan does not\n * carry (AGL-2611). Not hidden, deliberately: the page behind the link is\n * the shell's upgrade notice, which is the way to buy it, and a rail that\n * hid the section would hide the reason to upgrade.\n */\n locked?: boolean\n}\n\nexport interface HubSectionsProps {\n sections: readonly HubSection[]\n /** The active section's page, rendered beside the rail. */\n children: ReactNode\n /** Left nav card header (defaults to \"Navigation\"). */\n navHeader?: string\n /**\n * The page needs the row's whole width — a wide table, a record with a\n * timeline — so the rail is drawn ABOVE it as the strip a phone gets,\n * rather than beside it in a column that is empty below its few tabs\n * (AGL-3332). The same rail either way: same sections, same links, same\n * selected tab.\n */\n wide?: boolean\n}\n\n/**\n * The same rail, choosing a section by ROUTE rather than by panel (AGL-2501).\n *\n * ## Why this exists beside `HubTabs`\n *\n * `HubTabs` renders every panel and keeps them mounted — `keepMounted`, and\n * `lazy` is off by default and passed by nobody. That is deliberate for a hub\n * whose panels are cheap and want live subscriptions, and it is the wrong\n * default for a settings page: opening \"General\" mounts the API-keys card, the\n * SSO card and the data-export card, and every one of their reads runs.\n *\n * The code is the other half. A tabbed page imports every panel's module\n * statically, so a reader who only renames their workspace still downloads the\n * delete-org dialog.\n *\n * Sections as routes fix both at the framework level rather than by hand: Next\n * mounts one page and code-splits per route, so an unopened section costs\n * neither a read nor a byte. Three things come free that the tab version faked\n * or did without — a section is linkable, the back button walks sections, and\n * the active state is a fact about the URL rather than state kept in sync with\n * it.\n *\n * ## Active state\n *\n * By PREFIX, so a section stays selected on its own deeper routes, longest\n * match first so a nested section beats its parent. The separator boundary is\n * what stops `/settings` claiming `/settings-export`.\n */\n/**\n * The section the current URL is inside, or `null` when none matches.\n *\n * Exported because the RAIL is not the only thing that has to know. A hub's\n * breadcrumb ends at the hub — \"Site / Admin\" — while the reader is looking at\n * Plugins, so the trail names every level except the one they are on. Feeding\n * both from one resolver is what stops the two disagreeing: a section added to\n * the rail is in the breadcrumb by construction, rather than by somebody\n * remembering a second list.\n *\n * Matching is by PREFIX so a section stays selected on its own deeper routes,\n * longest match first so a nested section beats its parent, and on a separator\n * boundary so `/settings` cannot claim `/settings-export`.\n */\nexport function useActiveSection(\n sections: readonly HubSection[],\n): HubSection | null {\n const pathname = usePathname()\n return useMemo(() => {\n const onPath = (href: string) =>\n pathname === href || pathname.startsWith(`${href}/`)\n return (\n [...sections]\n .filter((section) => section.visible !== false && onPath(section.href))\n .sort((a, b) => b.href.length - a.href.length)[0] ?? null\n )\n }, [pathname, sections])\n}\n\n/**\n * A locked section's label: its name, then a lock, as one inline run\n * (AGL-2783).\n *\n * The lock lives inside the label rather than in `Tab`'s `icon` slot, because\n * MUI lays out a tab that has an icon as a different row. A tab with both an\n * icon and a label gets `MuiTab-labelIcon` — a 72px minimum height against a\n * plain tab's 48px, with its own vertical padding — and an icon beside the\n * label turns the tab from a column into a row, where the rail's\n * `alignItems: 'start'` stops meaning left and starts meaning top. Drawn that\n * way, a locked section is taller than its neighbors, centered in its row,\n * and its label is pinned to the row's top edge. Inside the label, a locked\n * tab is the same tab as an unlocked one: same classes, same height, same\n * alignment, on the vertical rail and the stacked one alike.\n *\n * The lock is named through `titleAccess`, which gives the svg `role=\"img\"`\n * and a `<title>`, so its words reach the tab's accessible name (AGL-2794).\n * An `aria-label` cannot: MUI's `SvgIcon` renders any icon without a title\n * `aria-hidden`, and a label on a hidden element is never read, so the tab\n * would announce itself exactly like a section the plan includes.\n */\nfunction LockedSectionLabel(props: { label: string }) {\n return (\n <Box\n component=\"span\"\n sx={{ display: 'inline-flex', alignItems: 'center', gap: 1 }}\n >\n {props.label}\n <MdiIcon\n path={mdiLockOutline.path}\n titleAccess=\"Not included in your plan\"\n />\n </Box>\n )\n}\n\nexport function HubSections(props: HubSectionsProps) {\n const { sections, children, navHeader = 'Navigation', wide = false } = props\n const { tabsProps } = useRailLayout(wide)\n const shown = useMemo(\n () => sections.filter((section) => section.visible !== false),\n [sections],\n )\n const activeHref = useActiveSection(sections)?.href ?? null\n\n return (\n <GridItems\n // The same navigation-beside-content split as above.\n masonry={false}\n spacing={3}\n items={[\n {\n size: wide ? { xs: 12 } : { xs: 12, sm: 3 },\n children: (\n <CardDisplay header={navHeader}>\n {/*\n * `Tabs`, not `TabList`: there is no `TabContext` here because\n * there are no panels to bind to — the content is a routed\n * page. `false` when nothing matches, because a `value` MUI\n * cannot find warns on every render and parks the indicator on\n * whichever tab happens to be first.\n */}\n <Tabs {...tabsProps} value={activeHref ?? false}>\n {shown.map((section) => (\n <Tab\n key={section.href}\n value={section.href}\n label={\n section.locked ? (\n <LockedSectionLabel label={section.label} />\n ) : (\n section.label\n )\n }\n component={AppLink}\n href={section.href}\n aria-current={\n section.href === activeHref ? 'page' : undefined\n }\n />\n ))}\n </Tabs>\n </CardDisplay>\n ),\n },\n { size: wide ? { xs: 12 } : { xs: 12, sm: 9 }, children },\n ]}\n />\n )\n}\nHubSections.displayName = 'HubSections'\n"],"names":["AppLink","CardDisplay","GridItems","MdiIcon","mdiLockOutline","TabContext","TabList","TabPanel","Box","Tab","Tabs","useMediaQuery","useTheme","usePathname","useCallback","useEffect","useMemo","useState","useTabParam","useRailLayout","forceStacked","theme","narrow","breakpoints","down","stacked","tabsProps","orientation","variant","allowScrollButtonsMobile","textColor","indicatorColor","sx","alignItems","maxWidth","textTransform","HubTabs","props","tabs","navHeader","lazy","tabIds","map","item","id","tab","onTabChange","ids","activated","setActivated","Set","handleChange","event","value","prev","has","add","masonry","spacing","items","size","xs","sm","children","header","onChange","label","keepMounted","padding","content","displayName","useActiveSection","sections","pathname","onPath","href","startsWith","filter","section","visible","sort","a","b","length","LockedSectionLabel","component","display","gap","path","titleAccess","HubSections","wide","shown","activeHref","locked","aria-current","undefined"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../../libs/shared/ui/next/src/lib/components/hub-tabs.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n AppLink,\n CardDisplay,\n GridItems,\n MdiIcon,\n mdiLockOutline,\n} from '@aglyn/shared-ui-jsx'\nimport { TabContext, TabList, TabPanel } from '@mui/lab'\nimport { Box, Tab, Tabs, useMediaQuery, useTheme } from '@mui/material'\nimport { usePathname } from 'next/navigation'\nimport {\n type ReactNode,\n useCallback,\n useEffect,\n useMemo,\n useState,\n} from 'react'\nimport { useTabParam } from '../hooks/use-tab-param'\n\n/**\n * The rail's own shape, shared by BOTH modes below (AGL-2501).\n *\n * `HubTabs` and `HubSections` are two ways of choosing a section and one way\n * of DRAWING that choice. Duplicating the card, the orientation switch and the\n * tab `sx` is how they drift into looking like two different products, which\n * is exactly what happened the first time this rail was rebuilt by hand.\n */\nfunction useRailLayout(forceStacked = false) {\n const theme = useTheme()\n const narrow = useMediaQuery(theme.breakpoints.down('sm'))\n // A vertical rail needs a quarter of the row; on a tablet that is ~180px\n // and its labels wrap word by word, so the strip stays on top until `md`.\n const stacked = useMediaQuery(theme.breakpoints.down('md')) || forceStacked\n return {\n stacked,\n narrow,\n /** Props every rail passes to its `Tabs`/`TabList`. */\n tabsProps: {\n orientation: stacked ? ('horizontal' as const) : ('vertical' as const),\n variant: stacked ? ('scrollable' as const) : ('standard' as const),\n allowScrollButtonsMobile: true,\n textColor: 'primary' as const,\n indicatorColor: 'primary' as const,\n sx: {\n ['.MuiTab-root']: {\n alignItems: stacked ? 'center' : 'start',\n maxWidth: 'unset',\n textTransform: 'none',\n },\n },\n },\n }\n}\n\nexport interface HubTab {\n id: string\n label: string\n content: ReactNode\n}\n\nexport interface HubTabsProps {\n tabs: HubTab[]\n /** Left nav card header (defaults to \"Navigation\"). */\n navHeader?: string\n /**\n * Defer mounting a panel's content until its tab is first activated, then\n * keep it mounted (AGL-785). Off by default so the standard behavior —\n * every panel mounted up front, subscriptions always live — is unchanged.\n * Opt in when the tabs host several data-heavy panels whose subscriptions\n * would otherwise all settle at once on load: mounting only the active\n * panel keeps that first-paint re-render burst small enough not to trip\n * React's nested-update limit. Panels stay mounted once visited, so\n * switching back is still instant.\n */\n lazy?: boolean\n}\n\n/**\n * Hub tab strip (AGL-354/382): the host-setup two-column pattern as a\n * shared component — a left \"Navigation\" CardDisplay with a vertical\n * TabList, content on the right. Collapses to horizontal tabs on small\n * screens. The active tab mirrors into the `?tab=` query param (shallow\n * replace) so hub views deep-link and survive back/forward; panels are\n * kept mounted so content and its data subscriptions are always present\n * (unless `lazy`, which defers un-visited panels — see the prop).\n */\nexport function HubTabs(props: HubTabsProps) {\n const { tabs, navHeader = 'Navigation', lazy = false } = props\n const { narrow, tabsProps } = useRailLayout()\n /*\n * The SHARED resolver, not a second reading of the same parameter\n * (AGL-2486). This rail used to hold the incoming id in `useState`, which\n * reads it once and then stops: back and forward are navigations between\n * two states of one mounted page, and a link into another section of a page\n * already open changes the parameter without remounting anything. Either\n * one left the rail on the old tab while the URL named a different one.\n *\n * `ids` is the tabs that exist right now, so an id naming a tab this hub\n * does not render falls back to the first rather than selecting a panel\n * nothing draws — which matters because several hubs build their tab list\n * from entitlements and render a different set per org.\n */\n const tabIds = useMemo(() => tabs.map((item) => item.id), [tabs])\n const { tab, onTabChange } = useTabParam({ ids: tabIds })\n // Which tabs have ever been active — the mount set when `lazy`. Seeded with\n // the tab resolved for first paint so it (and only it) mounts.\n const [activated, setActivated] = useState<Set<string>>(\n () => new Set(tab ? [tab] : []),\n )\n\n const handleChange = useCallback(\n (event: unknown, value: string) => {\n setActivated((prev) => (prev.has(value) ? prev : new Set(prev).add(value)))\n onTabChange(event, value)\n },\n [onTabChange],\n )\n\n /*\n * A panel reached by URL rather than by click has to stay in the mount set\n * too. `handleChange` is the only thing that grows the set, and it does not\n * fire when back/forward or an in-app link moves the parameter — so without\n * this, a `lazy` hub would drop such a panel again the moment the reader\n * moved on, and every return to it would remount and re-subscribe.\n *\n * The panel itself does not wait for this effect: the render below mounts\n * the ACTIVE tab unconditionally, so there is no frame in which the rail\n * shows a selected tab over an empty panel.\n */\n useEffect(() => {\n if (!lazy || !tab) return\n setActivated((prev) => (prev.has(tab) ? prev : new Set(prev).add(tab)))\n }, [lazy, tab])\n\n return (\n <TabContext value={tab}>\n <GridItems\n // A navigation column beside its content, not a set of cards.\n masonry={false}\n spacing={{ xs: 2, sm: 3 }}\n items={[\n {\n size: { xs: 12, md: 3 },\n children: (\n // On a phone the strip says what it is on its own; a\n // \"Navigation\" title above it is the first 60px of the screen.\n <CardDisplay header={narrow ? undefined : navHeader}>\n <TabList\n {...tabsProps}\n aria-label={navHeader}\n onChange={handleChange}\n >\n {tabs.map((item) => (\n <Tab key={item.id} value={item.id} label={item.label} />\n ))}\n </TabList>\n </CardDisplay>\n ),\n },\n {\n size: { xs: 12, md: 9 },\n children: (\n <>\n {tabs.map((item) => (\n <TabPanel\n key={item.id}\n value={item.id}\n keepMounted\n sx={{ padding: 'unset' }}\n >\n {!lazy || item.id === tab || activated.has(item.id)\n ? item.content\n : null}\n </TabPanel>\n ))}\n </>\n ),\n },\n ]}\n />\n </TabContext>\n )\n}\nHubTabs.displayName = 'HubTabs'\n\nexport default HubTabs\n\n\nexport interface HubSection {\n /** Route this section lives at. What the rail links to. */\n href: string\n label: string\n /** Hidden entirely when false — a release or a role gate. */\n visible?: boolean\n /**\n * Shown and linked, with a lock — an entitlement the org's plan does not\n * carry (AGL-2611). Not hidden, deliberately: the page behind the link is\n * the shell's upgrade notice, which is the way to buy it, and a rail that\n * hid the section would hide the reason to upgrade.\n */\n locked?: boolean\n}\n\nexport interface HubSectionsProps {\n sections: readonly HubSection[]\n /** The active section's page, rendered beside the rail. */\n children: ReactNode\n /** Left nav card header (defaults to \"Navigation\"). */\n navHeader?: string\n /**\n * The page needs the row's whole width — a wide table, a record with a\n * timeline — so the rail is drawn ABOVE it as the strip a phone gets,\n * rather than beside it in a column that is empty below its few tabs\n * (AGL-3332). The same rail either way: same sections, same links, same\n * selected tab.\n */\n wide?: boolean\n}\n\n/**\n * The same rail, choosing a section by ROUTE rather than by panel (AGL-2501).\n *\n * ## Why this exists beside `HubTabs`\n *\n * `HubTabs` renders every panel and keeps them mounted — `keepMounted`, and\n * `lazy` is off by default and passed by nobody. That is deliberate for a hub\n * whose panels are cheap and want live subscriptions, and it is the wrong\n * default for a settings page: opening \"General\" mounts the API-keys card, the\n * SSO card and the data-export card, and every one of their reads runs.\n *\n * The code is the other half. A tabbed page imports every panel's module\n * statically, so a reader who only renames their workspace still downloads the\n * delete-org dialog.\n *\n * Sections as routes fix both at the framework level rather than by hand: Next\n * mounts one page and code-splits per route, so an unopened section costs\n * neither a read nor a byte. Three things come free that the tab version faked\n * or did without — a section is linkable, the back button walks sections, and\n * the active state is a fact about the URL rather than state kept in sync with\n * it.\n *\n * ## Active state\n *\n * By PREFIX, so a section stays selected on its own deeper routes, longest\n * match first so a nested section beats its parent. The separator boundary is\n * what stops `/settings` claiming `/settings-export`.\n */\n/**\n * The section the current URL is inside, or `null` when none matches.\n *\n * Exported because the RAIL is not the only thing that has to know. A hub's\n * breadcrumb ends at the hub — \"Site / Admin\" — while the reader is looking at\n * Plugins, so the trail names every level except the one they are on. Feeding\n * both from one resolver is what stops the two disagreeing: a section added to\n * the rail is in the breadcrumb by construction, rather than by somebody\n * remembering a second list.\n *\n * Matching is by PREFIX so a section stays selected on its own deeper routes,\n * longest match first so a nested section beats its parent, and on a separator\n * boundary so `/settings` cannot claim `/settings-export`.\n */\nexport function useActiveSection(\n sections: readonly HubSection[],\n): HubSection | null {\n const pathname = usePathname()\n return useMemo(() => {\n const onPath = (href: string) =>\n pathname === href || pathname.startsWith(`${href}/`)\n return (\n [...sections]\n .filter((section) => section.visible !== false && onPath(section.href))\n .sort((a, b) => b.href.length - a.href.length)[0] ?? null\n )\n }, [pathname, sections])\n}\n\n/**\n * A locked section's label: its name, then a lock, as one inline run\n * (AGL-2783).\n *\n * The lock lives inside the label rather than in `Tab`'s `icon` slot, because\n * MUI lays out a tab that has an icon as a different row. A tab with both an\n * icon and a label gets `MuiTab-labelIcon` — a 72px minimum height against a\n * plain tab's 48px, with its own vertical padding — and an icon beside the\n * label turns the tab from a column into a row, where the rail's\n * `alignItems: 'start'` stops meaning left and starts meaning top. Drawn that\n * way, a locked section is taller than its neighbors, centered in its row,\n * and its label is pinned to the row's top edge. Inside the label, a locked\n * tab is the same tab as an unlocked one: same classes, same height, same\n * alignment, on the vertical rail and the stacked one alike.\n *\n * The lock is named through `titleAccess`, which gives the svg `role=\"img\"`\n * and a `<title>`, so its words reach the tab's accessible name (AGL-2794).\n * An `aria-label` cannot: MUI's `SvgIcon` renders any icon without a title\n * `aria-hidden`, and a label on a hidden element is never read, so the tab\n * would announce itself exactly like a section the plan includes.\n */\nfunction LockedSectionLabel(props: { label: string }) {\n return (\n <Box\n component=\"span\"\n sx={{ display: 'inline-flex', alignItems: 'center', gap: 1 }}\n >\n {props.label}\n <MdiIcon\n path={mdiLockOutline.path}\n titleAccess=\"Not included in your plan\"\n />\n </Box>\n )\n}\n\nexport function HubSections(props: HubSectionsProps) {\n const { sections, children, navHeader = 'Navigation', wide = false } = props\n const { narrow, tabsProps } = useRailLayout(wide)\n const shown = useMemo(\n () => sections.filter((section) => section.visible !== false),\n [sections],\n )\n const activeHref = useActiveSection(sections)?.href ?? null\n\n return (\n <GridItems\n // The same navigation-beside-content split as above.\n masonry={false}\n spacing={{ xs: 2, sm: 3 }}\n items={[\n {\n size: wide ? { xs: 12 } : { xs: 12, md: 3 },\n children: (\n <CardDisplay header={narrow ? undefined : navHeader}>\n {/*\n * `Tabs`, not `TabList`: there is no `TabContext` here because\n * there are no panels to bind to — the content is a routed\n * page. `false` when nothing matches, because a `value` MUI\n * cannot find warns on every render and parks the indicator on\n * whichever tab happens to be first.\n */}\n <Tabs\n {...tabsProps}\n aria-label={navHeader}\n value={activeHref ?? false}\n >\n {shown.map((section) => (\n <Tab\n key={section.href}\n value={section.href}\n label={\n section.locked ? (\n <LockedSectionLabel label={section.label} />\n ) : (\n section.label\n )\n }\n component={AppLink}\n href={section.href}\n aria-current={\n section.href === activeHref ? 'page' : undefined\n }\n />\n ))}\n </Tabs>\n </CardDisplay>\n ),\n },\n { size: wide ? { xs: 12 } : { xs: 12, md: 9 }, children },\n ]}\n />\n )\n}\nHubSections.displayName = 'HubSections'\n"],"names":["AppLink","CardDisplay","GridItems","MdiIcon","mdiLockOutline","TabContext","TabList","TabPanel","Box","Tab","Tabs","useMediaQuery","useTheme","usePathname","useCallback","useEffect","useMemo","useState","useTabParam","useRailLayout","forceStacked","theme","narrow","breakpoints","down","stacked","tabsProps","orientation","variant","allowScrollButtonsMobile","textColor","indicatorColor","sx","alignItems","maxWidth","textTransform","HubTabs","props","tabs","navHeader","lazy","tabIds","map","item","id","tab","onTabChange","ids","activated","setActivated","Set","handleChange","event","value","prev","has","add","masonry","spacing","xs","sm","items","size","md","children","header","undefined","aria-label","onChange","label","keepMounted","padding","content","displayName","useActiveSection","sections","pathname","onPath","href","startsWith","filter","section","visible","sort","a","b","length","LockedSectionLabel","component","display","gap","path","titleAccess","HubSections","wide","shown","activeHref","locked","aria-current"],"mappings":"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"}