@aglyn/shared-ui-next 1.0.0-beta.249 → 1.0.0-beta.252

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.249",
3
+ "version": "1.0.0-beta.252",
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.249",
29
- "@aglyn/shared-ui-jsx": "1.0.0-beta.249",
30
- "@aglyn/shared-ui-theme": "1.0.0-beta.249",
31
- "@aglyn/shared-util-dom": "1.0.0-beta.249",
32
- "@aglyn/shared-util-tools": "1.0.0-beta.249",
28
+ "@aglyn/shared-data-enums": "1.0.0-beta.252",
29
+ "@aglyn/shared-ui-jsx": "1.0.0-beta.252",
30
+ "@aglyn/shared-ui-theme": "1.0.0-beta.252",
31
+ "@aglyn/shared-util-dom": "1.0.0-beta.252",
32
+ "@aglyn/shared-util-tools": "1.0.0-beta.252",
33
33
  "@swc/helpers": "0.5.23"
34
34
  },
35
35
  "peerDependencies": {
@@ -47,6 +47,13 @@ export interface HubSection {
47
47
  * hid the section would hide the reason to upgrade.
48
48
  */
49
49
  locked?: boolean;
50
+ /**
51
+ * The group the section is drawn in, named (AGL-3713). Consecutive
52
+ * sections naming the same group are one run: the rail draws a divider
53
+ * between two runs and the group's name above its run. No group is the
54
+ * unnamed group, drawn without words.
55
+ */
56
+ group?: string;
50
57
  }
51
58
  export interface HubSectionsProps {
52
59
  sections: readonly HubSection[];
@@ -18,7 +18,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
18
18
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
19
19
  import { AppLink, CardDisplay, GridItems, MdiIcon, mdiLockOutline } from "@aglyn/shared-ui-jsx";
20
20
  import { TabContext, TabList, TabPanel } from "@mui/lab";
21
- import { Box, Tab, Tabs, useMediaQuery, useTheme } from "@mui/material";
21
+ import { Box, Divider, Tab, Tabs, Typography, useMediaQuery, useTheme } from "@mui/material";
22
22
  import { usePathname } from "next/navigation";
23
23
  import { useCallback, useEffect, useMemo, useState } from "react";
24
24
  import { useTabParam } from "../hooks/use-tab-param.js";
@@ -252,11 +252,90 @@ export default HubTabs;
252
252
  ]
253
253
  });
254
254
  }
255
+ /**
256
+ * Where a group of sections starts in the rail: a divider, and the group's
257
+ * name above it when it has one (AGL-3713) — MUI's guidance for dividing the
258
+ * sections of a list, a `Divider` and an overline subheader.
259
+ *
260
+ * A child of `Tabs`, which clones every child it is given with a tab's props
261
+ * (`value`, `selected`, `indicator`, `onChange` …). Those are taken here and
262
+ * dropped rather than reaching the DOM, and the divider is not a tab — the
263
+ * keyboard's roving focus only walks what registered as one. Presentational
264
+ * to assistive tech: the tablist owns tabs, and the group's name is read as
265
+ * the words it is.
266
+ *
267
+ * On the stacked strip (a phone, a wide page) the divider stands upright
268
+ * between two tabs and the name is left off: the strip is one scrolling row,
269
+ * and a word in it would read as a tab that does nothing.
270
+ */ function HubSectionGroupStart(props) {
271
+ const { label, first, stacked } = props;
272
+ if (stacked) return /*#__PURE__*/ _jsx(Divider, {
273
+ orientation: "vertical",
274
+ flexItem: true,
275
+ role: "presentation",
276
+ sx: {
277
+ my: 1
278
+ }
279
+ });
280
+ return /*#__PURE__*/ _jsxs(Box, {
281
+ role: "presentation",
282
+ "data-hub-section-group": label != null ? label : '',
283
+ children: [
284
+ first ? null : /*#__PURE__*/ _jsx(Divider, {
285
+ role: "presentation",
286
+ sx: {
287
+ my: 1
288
+ }
289
+ }),
290
+ label ? /*#__PURE__*/ _jsx(Typography, {
291
+ variant: "overline",
292
+ component: "div",
293
+ color: "text.secondary",
294
+ sx: {
295
+ px: 2,
296
+ lineHeight: 2.5
297
+ },
298
+ children: label
299
+ }) : null
300
+ ]
301
+ });
302
+ }
303
+ /**
304
+ * The rail's children: a tab per shown section, with a group start wherever
305
+ * the group changes, and above the first section when it names a group.
306
+ */ function hubSectionRailItems(shown, activeHref, stacked) {
307
+ const items = [];
308
+ shown.forEach((section, index)=>{
309
+ var _previous_group, _section_group, _section_group1;
310
+ const previous = index > 0 ? shown[index - 1] : undefined;
311
+ // Nothing above the first tab of the stacked strip, where only a divider
312
+ // is drawn: `Tabs` places its indicator by DOM child index, so every
313
+ // child pushed here must render an element.
314
+ const startsGroup = previous ? ((_previous_group = previous.group) != null ? _previous_group : '') !== ((_section_group = section.group) != null ? _section_group : '') : Boolean(section.group) && !stacked;
315
+ if (startsGroup) items.push(/*#__PURE__*/ _jsx(HubSectionGroupStart, {
316
+ // A value no tab carries, so `Tabs` never selects it.
317
+ value: `group:${index}`,
318
+ label: section.group,
319
+ first: !previous,
320
+ stacked: stacked
321
+ }, `group:${index}:${(_section_group1 = section.group) != null ? _section_group1 : ''}`));
322
+ items.push(/*#__PURE__*/ _jsx(Tab, {
323
+ value: section.href,
324
+ label: section.locked ? /*#__PURE__*/ _jsx(LockedSectionLabel, {
325
+ label: section.label
326
+ }) : section.label,
327
+ component: AppLink,
328
+ href: section.href,
329
+ "aria-current": section.href === activeHref ? 'page' : undefined
330
+ }, section.href));
331
+ });
332
+ return items;
333
+ }
255
334
  export function HubSections(props) {
256
335
  var _ref;
257
336
  var _useActiveSection;
258
337
  const { sections, children, navHeader = 'Navigation', wide = false } = props;
259
- const { narrow, tabsProps } = useRailLayout(wide);
338
+ const { narrow, stacked, tabsProps } = useRailLayout(wide);
260
339
  const shown = useMemo(()=>sections.filter((section)=>section.visible !== false), [
261
340
  sections
262
341
  ]);
@@ -281,15 +360,7 @@ export function HubSections(props) {
281
360
  children: /*#__PURE__*/ _jsx(Tabs, _extends({}, tabsProps, {
282
361
  "aria-label": navHeader,
283
362
  value: activeHref != null ? activeHref : false,
284
- children: shown.map((section)=>/*#__PURE__*/ _jsx(Tab, {
285
- value: section.href,
286
- label: section.locked ? /*#__PURE__*/ _jsx(LockedSectionLabel, {
287
- label: section.label
288
- }) : section.label,
289
- component: AppLink,
290
- href: section.href,
291
- "aria-current": section.href === activeHref ? 'page' : undefined
292
- }, section.href))
363
+ children: hubSectionRailItems(shown, activeHref, stacked)
293
364
  }))
294
365
  })
295
366
  },
@@ -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 // 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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"}
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 {\n Box,\n Divider,\n Tab,\n Tabs,\n Typography,\n useMediaQuery,\n useTheme,\n} 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 * The group the section is drawn in, named (AGL-3713). Consecutive\n * sections naming the same group are one run: the rail draws a divider\n * between two runs and the group's name above its run. No group is the\n * unnamed group, drawn without words.\n */\n group?: string\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\n/**\n * Where a group of sections starts in the rail: a divider, and the group's\n * name above it when it has one (AGL-3713) — MUI's guidance for dividing the\n * sections of a list, a `Divider` and an overline subheader.\n *\n * A child of `Tabs`, which clones every child it is given with a tab's props\n * (`value`, `selected`, `indicator`, `onChange` …). Those are taken here and\n * dropped rather than reaching the DOM, and the divider is not a tab — the\n * keyboard's roving focus only walks what registered as one. Presentational\n * to assistive tech: the tablist owns tabs, and the group's name is read as\n * the words it is.\n *\n * On the stacked strip (a phone, a wide page) the divider stands upright\n * between two tabs and the name is left off: the strip is one scrolling row,\n * and a word in it would read as a tab that does nothing.\n */\nfunction HubSectionGroupStart(props: {\n label?: string\n first: boolean\n stacked: boolean\n // The props `Tabs` adds to each child — swallowed, see above.\n value?: unknown\n}) {\n const { label, first, stacked } = props\n if (stacked)\n return (\n <Divider orientation=\"vertical\" flexItem role=\"presentation\" sx={{ my: 1 }} />\n )\n return (\n <Box role=\"presentation\" data-hub-section-group={label ?? ''}>\n {first ? null : <Divider role=\"presentation\" sx={{ my: 1 }} />}\n {label ? (\n <Typography\n variant=\"overline\"\n component=\"div\"\n color=\"text.secondary\"\n sx={{ px: 2, lineHeight: 2.5 }}\n >\n {label}\n </Typography>\n ) : null}\n </Box>\n )\n}\n\n/**\n * The rail's children: a tab per shown section, with a group start wherever\n * the group changes, and above the first section when it names a group.\n */\nfunction hubSectionRailItems(\n shown: readonly HubSection[],\n activeHref: string | null,\n stacked: boolean,\n): ReactNode[] {\n const items: ReactNode[] = []\n shown.forEach((section, index) => {\n const previous = index > 0 ? shown[index - 1] : undefined\n // Nothing above the first tab of the stacked strip, where only a divider\n // is drawn: `Tabs` places its indicator by DOM child index, so every\n // child pushed here must render an element.\n const startsGroup = previous\n ? (previous.group ?? '') !== (section.group ?? '')\n : Boolean(section.group) && !stacked\n if (startsGroup)\n items.push(\n <HubSectionGroupStart\n key={`group:${index}:${section.group ?? ''}`}\n // A value no tab carries, so `Tabs` never selects it.\n value={`group:${index}`}\n label={section.group}\n first={!previous}\n stacked={stacked}\n />,\n )\n items.push(\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={section.href === activeHref ? 'page' : undefined}\n />,\n )\n })\n return items\n}\n\nexport function HubSections(props: HubSectionsProps) {\n const { sections, children, navHeader = 'Navigation', wide = false } = props\n const { narrow, stacked, 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 {hubSectionRailItems(shown, activeHref, stacked)}\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","Divider","Tab","Tabs","Typography","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","HubSectionGroupStart","first","flexItem","role","my","data-hub-section-group","color","px","lineHeight","hubSectionRailItems","shown","activeHref","forEach","index","previous","startsGroup","group","Boolean","push","locked","aria-current","HubSections","wide"],"mappings":"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"}