@12-apps/ui 5.3.1 → 5.5.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.
@@ -4,7 +4,7 @@ import {
4
4
  import "../chunk-PZ5AY32C.js";
5
5
 
6
6
  // src/components/layout/SettingsLayout/SettingsLayout.tsx
7
- import { Box as Box6 } from "@mui/material";
7
+ import { Box as Box7 } from "@mui/material";
8
8
  import { useMemo, useState } from "react";
9
9
 
10
10
  // src/components/layout/SettingsLayout/SettingsLayout.filters.ts
@@ -473,13 +473,14 @@ function SettingsRail({
473
473
  }
474
474
 
475
475
  // src/components/layout/SettingsLayout/SettingsPanel.tsx
476
- import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
477
- import { Box as Box5 } from "@mui/material";
476
+ import ChevronLeftIcon2 from "@mui/icons-material/ChevronLeft";
477
+ import { Box as Box6 } from "@mui/material";
478
478
 
479
479
  // src/components/layout/SettingsLayout/SettingsSectionChips.tsx
480
480
  import { alpha as alpha2, Box as Box4, useTheme as useTheme2 } from "@mui/material";
481
481
  import { useEffect, useRef } from "react";
482
482
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
483
+ var CHIP_HEIGHT = 34;
483
484
  function prefersReducedMotion() {
484
485
  if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
485
486
  return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
@@ -520,7 +521,15 @@ function SectionChip({
520
521
  alignItems: "center",
521
522
  gap: 0.75,
522
523
  flex: "0 0 auto",
523
- minHeight: TOUCH_TARGET,
524
+ position: "relative",
525
+ // The chip LOOKS this tall; it is still tapped at `TOUCH_TARGET`.
526
+ //
527
+ // Shrinking `minHeight` alone would have bought a slimmer strip with a
528
+ // smaller hit area — on the one width where the strip IS the navigation
529
+ // and the input is a thumb. The `::after` below keeps the tappable
530
+ // region at the full target while the pill itself takes less room, so
531
+ // this is a visual change and not an accessibility trade.
532
+ minHeight: CHIP_HEIGHT,
524
533
  px: 1.5,
525
534
  borderRadius: 999,
526
535
  cursor: "pointer",
@@ -531,7 +540,18 @@ function SectionChip({
531
540
  whiteSpace: "nowrap",
532
541
  color: active ? "primary.main" : "text.secondary",
533
542
  bgcolor: active ? alpha2(theme.palette.primary.main, 0.12) : "transparent",
534
- border: `1px solid ${active ? alpha2(theme.palette.primary.main, 0.4) : theme.palette.divider}`
543
+ border: `1px solid ${active ? alpha2(theme.palette.primary.main, 0.4) : theme.palette.divider}`,
544
+ // Spans the chip's width only, so neighbours never overlap: the strip
545
+ // is a row, and the extra reach is vertical.
546
+ "&::after": {
547
+ content: '""',
548
+ position: "absolute",
549
+ insetInlineStart: 0,
550
+ insetInlineEnd: 0,
551
+ top: "50%",
552
+ transform: "translateY(-50%)",
553
+ height: TOUCH_TARGET
554
+ }
535
555
  },
536
556
  children: [
537
557
  item.status ? /* @__PURE__ */ jsx4(
@@ -570,7 +590,7 @@ function SettingsSectionChips({
570
590
  alignItems: "center",
571
591
  gap: 0.75,
572
592
  px: 0.25,
573
- py: 0.75,
593
+ py: 0.25,
574
594
  overflowX: "auto",
575
595
  overflowY: "hidden",
576
596
  scrollbarWidth: "none",
@@ -593,17 +613,129 @@ function SettingsSectionChips({
593
613
  }
594
614
  SettingsSectionChips.displayName = "SettingsSectionChips";
595
615
 
596
- // src/components/layout/SettingsLayout/SettingsPanel.tsx
616
+ // src/components/layout/SettingsLayout/SettingsSectionHeader.tsx
617
+ import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
618
+ import { Box as Box5 } from "@mui/material";
597
619
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
598
- function BackLink({
620
+ var CLIP = {
621
+ overflow: "hidden",
622
+ textOverflow: "ellipsis",
623
+ whiteSpace: "nowrap"
624
+ };
625
+ function HeaderText({
626
+ title,
627
+ description
628
+ }) {
629
+ return /* @__PURE__ */ jsxs5(Box5, { sx: { minWidth: 0 }, children: [
630
+ /* @__PURE__ */ jsx5(
631
+ Box5,
632
+ {
633
+ component: "h2",
634
+ sx: { m: 0, font: "inherit", fontSize: "1rem", fontWeight: 600, lineHeight: 1.3, ...CLIP },
635
+ children: title
636
+ }
637
+ ),
638
+ description !== void 0 && description !== "" && /* @__PURE__ */ jsx5(
639
+ Box5,
640
+ {
641
+ component: "p",
642
+ sx: {
643
+ m: 0,
644
+ font: "inherit",
645
+ fontSize: "0.8125rem",
646
+ color: "text.secondary",
647
+ lineHeight: 1.35,
648
+ ...CLIP
649
+ },
650
+ children: description
651
+ }
652
+ )
653
+ ] });
654
+ }
655
+ function SettingsSectionHeader({
599
656
  href,
600
- label,
657
+ backLabel,
658
+ title,
659
+ description,
601
660
  breakpoint,
602
661
  linkComponent,
603
662
  testIdPrefix
604
663
  }) {
605
664
  return /* @__PURE__ */ jsxs5(
606
665
  Box5,
666
+ {
667
+ "data-testid": `${testIdPrefix}-section-header`,
668
+ sx: (theme) => ({
669
+ ...displayAcrossRail(theme, breakpoint, "flex", "none"),
670
+ alignItems: "center",
671
+ gap: 1.5,
672
+ minWidth: 0,
673
+ // ITS OWN SURFACE, not the panel's.
674
+ //
675
+ // The header, the chip strip and the first card were all the same
676
+ // white, so nothing said where "you are here" ended and the screen
677
+ // began — the eye had only the type sizes to go on. A tint plus the
678
+ // rule below turns three stacked runs of text into two blocks.
679
+ //
680
+ // `paper` against a `default` panel rather than a hand-picked grey, so
681
+ // it tracks the host's theme (and its dark mode) instead of being a
682
+ // light-mode constant that goes wrong the moment the palette does.
683
+ bgcolor: "background.paper",
684
+ px: 1.5,
685
+ pt: 1.5,
686
+ pb: 1.5,
687
+ mb: 1,
688
+ borderRadius: 1,
689
+ // A rule under the header, because what follows it is the sibling strip
690
+ // — a row of other sections. Without a line the section's own name and
691
+ // the chip naming a DIFFERENT section sit in one undifferentiated
692
+ // block, which reads as one navigation rather than as "you are here"
693
+ // above "go elsewhere".
694
+ borderBottom: 1,
695
+ borderColor: "divider"
696
+ }),
697
+ children: [
698
+ /* @__PURE__ */ jsx5(
699
+ Box5,
700
+ {
701
+ component: linkComponent,
702
+ href,
703
+ "aria-label": backLabel,
704
+ "data-testid": `${testIdPrefix}-back`,
705
+ sx: (theme) => ({
706
+ flex: "0 0 auto",
707
+ display: "inline-flex",
708
+ alignItems: "center",
709
+ justifyContent: "center",
710
+ width: TOUCH_TARGET,
711
+ height: TOUCH_TARGET,
712
+ border: 1,
713
+ borderColor: "divider",
714
+ borderRadius: 2,
715
+ color: "text.primary",
716
+ textDecoration: "none",
717
+ "&:hover": { backgroundColor: theme.palette.action.hover }
718
+ }),
719
+ children: /* @__PURE__ */ jsx5(ChevronLeftIcon, { fontSize: "small" })
720
+ }
721
+ ),
722
+ /* @__PURE__ */ jsx5(HeaderText, { title, description })
723
+ ]
724
+ }
725
+ );
726
+ }
727
+
728
+ // src/components/layout/SettingsLayout/SettingsPanel.tsx
729
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
730
+ function BackLink({
731
+ href,
732
+ label,
733
+ breakpoint,
734
+ linkComponent,
735
+ testIdPrefix
736
+ }) {
737
+ return /* @__PURE__ */ jsxs6(
738
+ Box6,
607
739
  {
608
740
  component: linkComponent,
609
741
  href,
@@ -621,12 +753,46 @@ function BackLink({
621
753
  fontSize: "0.875rem"
622
754
  }),
623
755
  children: [
624
- /* @__PURE__ */ jsx5(ChevronLeftIcon, { fontSize: "small" }),
756
+ /* @__PURE__ */ jsx6(ChevronLeftIcon2, { fontSize: "small" }),
625
757
  label
626
758
  ]
627
759
  }
628
760
  );
629
761
  }
762
+ function PanelBack({
763
+ href,
764
+ backLabel,
765
+ sectionTitle,
766
+ sectionDescription,
767
+ breakpoint,
768
+ linkComponent,
769
+ testIdPrefix
770
+ }) {
771
+ if (sectionTitle === void 0) {
772
+ return /* @__PURE__ */ jsx6(
773
+ BackLink,
774
+ {
775
+ href,
776
+ label: backLabel,
777
+ breakpoint,
778
+ linkComponent,
779
+ testIdPrefix
780
+ }
781
+ );
782
+ }
783
+ return /* @__PURE__ */ jsx6(
784
+ SettingsSectionHeader,
785
+ {
786
+ href,
787
+ backLabel,
788
+ title: sectionTitle,
789
+ description: sectionDescription,
790
+ breakpoint,
791
+ linkComponent,
792
+ testIdPrefix
793
+ }
794
+ );
795
+ }
630
796
  function panelSx(breakpoint, hideOnNarrow, fillHeight) {
631
797
  return (theme) => ({
632
798
  display: hideOnNarrow ? "none" : "block",
@@ -650,6 +816,8 @@ function SettingsPanel({
650
816
  breakpoint,
651
817
  indexHref,
652
818
  backLabel,
819
+ sectionTitle,
820
+ sectionDescription,
653
821
  ariaLabel,
654
822
  sectionChips,
655
823
  activeItemId,
@@ -661,31 +829,33 @@ function SettingsPanel({
661
829
  }) {
662
830
  const showBack = inSection && indexHref !== void 0 && linkComponent !== void 0;
663
831
  const showChips = inSection && sectionChips !== void 0 && sectionChips.length > 0;
664
- return /* @__PURE__ */ jsxs5(
665
- Box5,
832
+ return /* @__PURE__ */ jsxs6(
833
+ Box6,
666
834
  {
667
835
  "data-testid": `${testIdPrefix}-panel`,
668
836
  sx: panelSx(breakpoint, hideOnNarrow, fillHeight),
669
837
  children: [
670
- showBack ? /* @__PURE__ */ jsx5(
671
- BackLink,
838
+ showBack ? /* @__PURE__ */ jsx6(
839
+ PanelBack,
672
840
  {
673
841
  href: indexHref,
674
- label: backLabel,
842
+ backLabel,
843
+ sectionTitle,
844
+ sectionDescription,
675
845
  breakpoint,
676
846
  linkComponent,
677
847
  testIdPrefix
678
848
  }
679
849
  ) : null,
680
- showChips ? /* @__PURE__ */ jsx5(
681
- Box5,
850
+ showChips ? /* @__PURE__ */ jsx6(
851
+ Box6,
682
852
  {
683
853
  sx: (theme) => ({
684
854
  ...displayAcrossRail(theme, breakpoint, "block", "none"),
685
855
  mb: 1,
686
856
  minWidth: 0
687
857
  }),
688
- children: /* @__PURE__ */ jsx5(
858
+ children: /* @__PURE__ */ jsx6(
689
859
  SettingsSectionChips,
690
860
  {
691
861
  items: sectionChips,
@@ -705,7 +875,7 @@ function SettingsPanel({
705
875
  }
706
876
 
707
877
  // src/components/layout/SettingsLayout/SettingsLayout.tsx
708
- import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
878
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
709
879
  var DEFAULTS = {
710
880
  searchPlaceholder: "Search settings",
711
881
  defaultQuery: "",
@@ -761,15 +931,15 @@ var SettingsLayout = (props) => {
761
931
  const activeLabel = useMemo(() => activeItemLabel(groups, activeItemId), [groups, activeItemId]);
762
932
  const drilldown = navVariant === "drilldown";
763
933
  const ariaLabel = typeof title === "string" ? title : "Settings";
764
- return /* @__PURE__ */ jsxs6(
765
- Box6,
934
+ return /* @__PURE__ */ jsxs7(
935
+ Box7,
766
936
  {
767
937
  "data-testid": testIdPrefix,
768
938
  "data-nav-variant": navVariant,
769
939
  "data-at-index": atIndex ? "true" : "false",
770
940
  sx: shellSx(railBreakpoint, fillHeight),
771
941
  children: [
772
- /* @__PURE__ */ jsx6(
942
+ /* @__PURE__ */ jsx7(
773
943
  SettingsRail,
774
944
  {
775
945
  title,
@@ -793,13 +963,15 @@ var SettingsLayout = (props) => {
793
963
  fillHeight
794
964
  }
795
965
  ),
796
- /* @__PURE__ */ jsx6(
966
+ /* @__PURE__ */ jsx7(
797
967
  SettingsPanel,
798
968
  {
799
969
  inSection: drilldown && !atIndex,
800
970
  hideOnNarrow: drilldown && atIndex,
801
971
  breakpoint: railBreakpoint,
802
972
  indexHref,
973
+ sectionTitle: props.sectionTitle,
974
+ sectionDescription: props.sectionDescription,
803
975
  backLabel,
804
976
  ariaLabel,
805
977
  sectionChips,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/layout/SettingsLayout/SettingsLayout.tsx","../../src/components/layout/SettingsLayout/SettingsLayout.filters.ts","../../src/components/layout/SettingsLayout/SettingsLayout.rail.tsx","../../src/components/layout/SettingsLayout/SettingsLayout.items.tsx","../../src/components/layout/SettingsLayout/SettingsLayout.styles.ts","../../src/components/layout/SettingsLayout/SettingsStatusMarker.tsx","../../src/components/layout/SettingsLayout/SettingsPanel.tsx","../../src/components/layout/SettingsLayout/SettingsSectionChips.tsx"],"sourcesContent":["'use client';\n\nimport { Box, type Theme } from '@mui/material';\nimport React, { useMemo, useState } from 'react';\n\nimport { withDefaults } from '../../../utils/withDefaults';\nimport { activeItemLabel, filterGroups } from './SettingsLayout.filters';\nimport { SettingsRail } from './SettingsLayout.rail';\nimport { atLeastRail } from './SettingsLayout.styles';\nimport { SettingsPanel } from './SettingsPanel';\nimport type { SettingsLayoutProps } from './SettingsLayout.types';\n\n/**\n * The props with every default already applied.\n *\n * A table through `withDefaults` rather than a `= default` per destructured\n * prop: each of those is a branch, and seven of them put the component over the\n * complexity bar before it renders anything. The single cast is what buys the\n * body non-optional values — without it every use site would restate the\n * default as a `??`, which is the same default written twice and free to drift.\n */\ntype ResolvedProps = SettingsLayoutProps &\n Required<\n Pick<\n SettingsLayoutProps,\n | 'searchPlaceholder'\n | 'defaultQuery'\n | 'emptySearchLabel'\n | 'testIdPrefix'\n | 'railBreakpoint'\n | 'navVariant'\n | 'atIndex'\n | 'backLabel'\n >\n >;\n\nconst DEFAULTS = {\n searchPlaceholder: 'Search settings',\n defaultQuery: '',\n emptySearchLabel: 'No settings match your search.',\n testIdPrefix: 'settings',\n railBreakpoint: 'md',\n navVariant: 'switcher',\n atIndex: false,\n backLabel: 'Back',\n} satisfies Partial<SettingsLayoutProps>;\n\n/** Stacked below the rail's breakpoint, two columns at and above it. */\nfunction shellSx(breakpoint: SettingsLayoutProps['railBreakpoint'], fillHeight: boolean) {\n return (theme: Theme) => ({\n display: 'flex',\n flexDirection: 'column' as const,\n alignItems: 'stretch',\n gap: 1.5,\n width: '100%',\n minWidth: 0,\n // Only ever at the wide shape. Narrow, the rail is a disclosure or the page\n // itself, and two independent scrollbars on a phone is a trap rather than a\n // convenience.\n [atLeastRail(theme, breakpoint ?? 'md')]: {\n flexDirection: 'row',\n gap: 3,\n ...(fillHeight ? { height: '100%', minHeight: 0 } : {}),\n },\n });\n}\n\n/**\n * A reusable, agnostic Facebook-style settings shell: a searchable left rail of\n * grouped categories/subcategories and a central panel that renders the selected\n * configuration screen (`children`). Navigation is data-driven via `groups`; the\n * host owns routing (pass `linkComponent` + item `href`, or handle `onSelectItem`).\n *\n * ## Two narrow-width shapes\n *\n * `navVariant=\"switcher\"` (the default) folds the rail into a disclosure above\n * the panel. `navVariant=\"drilldown\"` gives the phone shape instead: at the\n * area's index the LIST is the page, and inside a section the panel is the page\n * with a back link and a scrollable strip of sibling sections above it.\n *\n * ## Why the width decision is CSS and not JavaScript\n *\n * Every part that changes between the two shapes does it through a `display`\n * pair keyed on `railBreakpoint`. Nothing here asks a media query in JS and\n * renders one branch.\n *\n * That is not a performance preference. A layout that RENDERS one navigation and\n * not the other can offer the narrow width less than the wide one, and no test\n * or review catches it — the missing control simply is not in the tree to be\n * asserted about. With both forms always mounted, \"the narrow width reaches\n * everything the wide one does\" holds by construction, and an audit that counts\n * controls per width gets the same answer at both.\n */\nexport const SettingsLayout: React.FC<SettingsLayoutProps> = (props) => {\n const {\n title,\n groups,\n activeItemId,\n onSelectItem,\n searchPlaceholder,\n defaultQuery,\n emptySearchLabel,\n emptySearchAction,\n linkComponent,\n children,\n testIdPrefix,\n railBreakpoint,\n fillHeight = false,\n navVariant,\n atIndex,\n indexHref,\n backLabel,\n sectionChips,\n } = withDefaults(props, DEFAULTS) as ResolvedProps;\n const [query, setQuery] = useState(defaultQuery);\n const [navOpen, setNavOpen] = useState(false);\n const filteredGroups = useMemo(() => filterGroups(groups, query), [groups, query]);\n const activeLabel = useMemo(() => activeItemLabel(groups, activeItemId), [groups, activeItemId]);\n\n const drilldown = navVariant === 'drilldown';\n const ariaLabel = typeof title === 'string' ? title : 'Settings';\n\n return (\n <Box\n data-testid={testIdPrefix}\n data-nav-variant={navVariant}\n data-at-index={atIndex ? 'true' : 'false'}\n sx={shellSx(railBreakpoint, fillHeight)}\n >\n <SettingsRail\n title={title}\n activeLabel={activeLabel}\n navOpen={navOpen}\n onToggleNav={() => setNavOpen((open) => !open)}\n query={query}\n onQueryChange={setQuery}\n searchPlaceholder={searchPlaceholder}\n emptySearchLabel={emptySearchLabel}\n emptySearchAction={emptySearchAction}\n filteredGroups={filteredGroups}\n activeItemId={activeItemId}\n linkComponent={linkComponent}\n onSelectItem={onSelectItem}\n onNavigate={() => setNavOpen(false)}\n testIdPrefix={testIdPrefix}\n variant={navVariant}\n breakpoint={railBreakpoint}\n atIndex={atIndex}\n fillHeight={fillHeight}\n />\n\n <SettingsPanel\n inSection={drilldown && !atIndex}\n hideOnNarrow={drilldown && atIndex}\n breakpoint={railBreakpoint}\n indexHref={indexHref}\n backLabel={backLabel}\n ariaLabel={ariaLabel}\n sectionChips={sectionChips}\n activeItemId={activeItemId}\n linkComponent={linkComponent}\n onSelectItem={onSelectItem}\n testIdPrefix={testIdPrefix}\n fillHeight={fillHeight}\n >\n {children}\n </SettingsPanel>\n </Box>\n );\n};\n\nSettingsLayout.displayName = 'SettingsLayout';\n","import type { SettingsNavGroup, SettingsNavItem } from './SettingsLayout.types';\n\n/** Case-insensitive filter over item label + keywords; drops emptied groups. */\nexport function filterGroups(groups: SettingsNavGroup[], query: string): SettingsNavGroup[] {\n const needle = query.trim().toLowerCase();\n if (!needle) return groups;\n const matches = (item: SettingsNavItem): boolean =>\n item.label.toLowerCase().includes(needle) ||\n (item.keywords ?? []).some((keyword) => keyword.toLowerCase().includes(needle));\n return groups\n .map((group) => ({ ...group, items: group.items.filter(matches) }))\n .filter((group) => group.items.length > 0);\n}\n\n/** Label of the active item, shown in the `switcher`'s collapsed header. */\nexport function activeItemLabel(\n groups: SettingsNavGroup[],\n activeItemId?: string,\n): string | undefined {\n return groups.flatMap((group) => group.items).find((item) => item.id === activeItemId)?.label;\n}\n","'use client';\n\nimport ExpandMoreIcon from '@mui/icons-material/ExpandMore';\nimport SearchIcon from '@mui/icons-material/Search';\nimport { Box, Button, InputAdornment, TextField, Typography } from '@mui/material';\nimport React from 'react';\n\nimport { SettingsRailGroup, type RailShape } from './SettingsLayout.items';\nimport { atLeastRail, displayAcrossRail, RAIL_WIDTH, TOUCH_TARGET } from './SettingsLayout.styles';\nimport type {\n SettingsEmptySearchAction,\n SettingsLayoutProps,\n SettingsNavGroup,\n SettingsRailBreakpoint,\n} from './SettingsLayout.types';\n\ninterface RailToggleProps {\n title?: React.ReactNode;\n activeLabel?: string;\n open: boolean;\n onToggle: () => void;\n breakpoint: SettingsRailBreakpoint;\n testIdPrefix: string;\n}\n\n/**\n * `switcher`-only collapse control: shows the current section (a compact\n * switcher) so the panel content stays at the top of the screen. Hidden once the\n * rail has its own column.\n */\nfunction SettingsRailToggle({\n title,\n activeLabel,\n open,\n onToggle,\n breakpoint,\n testIdPrefix,\n}: RailToggleProps): React.JSX.Element {\n return (\n <Box\n component=\"button\"\n type=\"button\"\n onClick={onToggle}\n aria-expanded={open}\n aria-label={typeof title === 'string' ? title : 'Settings'}\n data-testid={`${testIdPrefix}-toggle`}\n sx={(theme) => ({\n ...displayAcrossRail(theme, breakpoint, 'flex', 'none'),\n alignItems: 'center',\n justifyContent: 'space-between',\n gap: 1,\n width: '100%',\n minHeight: TOUCH_TARGET,\n px: 1.5,\n py: 1,\n mb: 1,\n textAlign: 'left',\n cursor: 'pointer',\n color: 'text.primary',\n background: 'transparent',\n border: `1px solid ${theme.palette.divider}`,\n borderRadius: 1.5,\n })}\n >\n <Box sx={{ minWidth: 0 }}>\n {title ? (\n <Typography variant=\"caption\" sx={{ display: 'block', color: 'text.secondary' }}>\n {title}\n </Typography>\n ) : null}\n <Typography variant=\"body2\" noWrap sx={{ fontWeight: 600 }}>\n {activeLabel ?? ''}\n </Typography>\n </Box>\n <ExpandMoreIcon\n fontSize=\"small\"\n sx={{\n flex: '0 0 auto',\n color: 'text.secondary',\n transition: 'transform 150ms ease',\n transform: open ? 'rotate(180deg)' : 'none',\n '@media (prefers-reduced-motion: reduce)': { transition: 'none' },\n }}\n />\n </Box>\n );\n}\n\n/** The empty-search state, with the way out of it rendered inside. */\nfunction SettingsSearchEmpty({\n label,\n action,\n onClear,\n testIdPrefix,\n}: {\n label: React.ReactNode;\n action?: SettingsEmptySearchAction;\n onClear: () => void;\n testIdPrefix: string;\n}): React.JSX.Element {\n return (\n <Box data-testid={`${testIdPrefix}-empty`} sx={{ px: 1, py: 2 }}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {label}\n </Typography>\n {action ? (\n <Button\n size=\"small\"\n variant=\"outlined\"\n onClick={() => {\n onClear();\n action.onClear?.();\n }}\n data-testid={`${testIdPrefix}-empty-action`}\n sx={{ mt: 1.5, minHeight: TOUCH_TARGET }}\n >\n {action.label}\n </Button>\n ) : null}\n </Box>\n );\n}\n\nexport interface RailBodyProps extends RailShape {\n navOpen: boolean;\n query: string;\n onQueryChange: (value: string) => void;\n searchPlaceholder: string;\n emptySearchLabel: React.ReactNode;\n emptySearchAction?: SettingsEmptySearchAction;\n filteredGroups: SettingsNavGroup[];\n activeItemId?: string;\n linkComponent?: SettingsLayoutProps['linkComponent'];\n onSelectItem?: (id: string) => void;\n onNavigate?: () => void;\n testIdPrefix: string;\n}\n\n/** The searchable rail body (search + groups). */\nfunction SettingsRailBody({\n navOpen,\n query,\n onQueryChange,\n searchPlaceholder,\n emptySearchLabel,\n emptySearchAction,\n filteredGroups,\n activeItemId,\n linkComponent,\n onSelectItem,\n onNavigate,\n testIdPrefix,\n ...shape\n}: RailBodyProps): React.JSX.Element {\n // `drilldown` has no disclosure to be closed behind: below the breakpoint the\n // body is the page, above it the rail. Only `switcher` hides it.\n const hidden = navOpen ? 'block' : 'none';\n return (\n <Box\n data-testid={`${testIdPrefix}-rail-body`}\n sx={(theme) =>\n shape.variant === 'drilldown'\n ? { display: 'block' }\n : displayAcrossRail(theme, shape.breakpoint, hidden, 'block')\n }\n >\n <TextField\n value={query}\n onChange={(event) => onQueryChange(event.target.value)}\n placeholder={searchPlaceholder}\n size=\"small\"\n fullWidth\n aria-label={searchPlaceholder}\n data-testid={`${testIdPrefix}-search`}\n InputProps={{\n startAdornment: (\n <InputAdornment position=\"start\">\n <SearchIcon fontSize=\"small\" sx={{ color: 'text.secondary' }} />\n </InputAdornment>\n ),\n }}\n sx={{ mb: 2 }}\n />\n {filteredGroups.length === 0 ? (\n <SettingsSearchEmpty\n label={emptySearchLabel}\n action={emptySearchAction}\n onClear={() => onQueryChange('')}\n testIdPrefix={testIdPrefix}\n />\n ) : (\n filteredGroups.map((group) => (\n <SettingsRailGroup\n key={group.id}\n group={group}\n activeItemId={activeItemId}\n linkComponent={linkComponent}\n onSelectItem={onSelectItem}\n onNavigate={onNavigate}\n testIdPrefix={testIdPrefix}\n {...shape}\n />\n ))\n )}\n </Box>\n );\n}\n\n/** Props for {@link SettingsRail}: the rail body's inputs plus the mobile chrome. */\nexport type SettingsRailProps = RailBodyProps & {\n title?: React.ReactNode;\n activeLabel?: string;\n onToggleNav: () => void;\n /** Scroll inside the rail rather than with the page. See `fillHeight`. */\n fillHeight?: boolean;\n};\n\n/** The navigation column: title, the `switcher`'s disclosure, and the body. */\nexport function SettingsRail({\n title,\n activeLabel,\n navOpen,\n onToggleNav,\n testIdPrefix,\n fillHeight = false,\n ...bodyProps\n}: SettingsRailProps): React.JSX.Element {\n const { variant, breakpoint, atIndex } = bodyProps;\n // In `drilldown` the rail is the index's list below the breakpoint and the\n // left column above it — so inside a section it steps aside for the chip\n // strip. It is `display: none`, never unmounted: the two navigation forms\n // both stay in the DOM at every width, which is what makes \"the narrow width\n // offers what the wide one offers\" true by construction instead of by review.\n const hiddenWhileInSection = variant === 'drilldown' && !atIndex;\n\n return (\n <Box\n component=\"nav\"\n aria-label={typeof title === 'string' ? title : 'Settings'}\n data-testid={`${testIdPrefix}-rail`}\n sx={(theme) => ({\n display: hiddenWhileInSection ? 'none' : 'block',\n flex: '0 0 auto',\n width: '100%',\n [atLeastRail(theme, breakpoint)]: {\n display: 'block',\n flex: `0 0 ${RAIL_WIDTH}px`,\n width: RAIL_WIDTH,\n borderRight: `1px solid ${theme.palette.divider}`,\n pr: 2,\n // Its own scrollbar, so a settings area with twenty categories does\n // not make the thing you NAVIGATE with scroll away while you read the\n // panel. `minHeight: 0` is the load-bearing half: without it a flex\n // child refuses to shrink below its content and `overflow` never\n // engages.\n ...(fillHeight\n ? { height: '100%', minHeight: 0, overflowY: 'auto' as const, overscrollBehavior: 'contain' as const }\n : {}),\n },\n })}\n >\n {title ? (\n <Typography\n variant=\"h6\"\n data-testid={`${testIdPrefix}-title`}\n sx={(theme) => ({\n ...(variant === 'drilldown'\n ? { display: 'block' }\n : displayAcrossRail(theme, breakpoint, 'none', 'block')),\n fontWeight: 700,\n mb: 1.5,\n })}\n >\n {title}\n </Typography>\n ) : null}\n {variant === 'switcher' ? (\n <SettingsRailToggle\n title={title}\n activeLabel={activeLabel}\n open={navOpen}\n onToggle={onToggleNav}\n breakpoint={breakpoint}\n testIdPrefix={testIdPrefix}\n />\n ) : null}\n <SettingsRailBody navOpen={navOpen} testIdPrefix={testIdPrefix} {...bodyProps} />\n </Box>\n );\n}\n","'use client';\n\nimport {\n alpha,\n Box,\n List,\n ListItem,\n ListItemButton,\n ListItemIcon,\n ListItemText,\n Typography,\n useTheme,\n} from '@mui/material';\nimport React from 'react';\n\nimport { atLeastRail, TOUCH_TARGET } from './SettingsLayout.styles';\nimport { SettingsStatusMarker } from './SettingsStatusMarker';\nimport type {\n SettingsLayoutProps,\n SettingsNavGroup,\n SettingsNavItem,\n SettingsNavVariant,\n SettingsRailBreakpoint,\n} from './SettingsLayout.types';\n\n/** The shape-deciding inputs every rail part needs to know about. */\nexport interface RailShape {\n variant: SettingsNavVariant;\n breakpoint: SettingsRailBreakpoint;\n atIndex: boolean;\n}\n\ninterface RailItemProps extends RailShape {\n item: SettingsNavItem;\n active: boolean;\n linkComponent?: SettingsLayoutProps['linkComponent'];\n onSelectItem?: (id: string) => void;\n /** Called on activation so the `switcher`'s rail can collapse after a choice. */\n onNavigate?: () => void;\n testIdPrefix: string;\n}\n\n/** Label, plus the situation marker when the host resolved one. */\nfunction RailItemContent({\n item,\n active,\n testIdPrefix,\n}: {\n item: SettingsNavItem;\n active: boolean;\n testIdPrefix: string;\n}): React.JSX.Element {\n return (\n <>\n {item.icon ? (\n <ListItemIcon sx={{ minWidth: 0, color: 'inherit' }}>{item.icon}</ListItemIcon>\n ) : null}\n <ListItemText\n primary={item.label}\n primaryTypographyProps={{ variant: 'body2', noWrap: true, fontWeight: active ? 600 : 500 }}\n />\n {item.status ? (\n <SettingsStatusMarker\n status={item.status}\n label={item.statusLabel}\n testId={`${testIdPrefix}-status-${item.id}`}\n />\n ) : null}\n </>\n );\n}\n\n/** One rail entry — a link when it has `href`, inert text when it says so, else a button. */\nexport function SettingsRailItem({\n item,\n active,\n linkComponent,\n onSelectItem,\n onNavigate,\n testIdPrefix,\n variant,\n breakpoint,\n}: RailItemProps): React.JSX.Element {\n const theme = useTheme();\n // In `drilldown` the narrow rail IS the page's list, so its rows are the\n // primary touch targets rather than a secondary column — hence the 44px floor\n // below the breakpoint. Above it, the row goes back to rail density.\n const touchFloor =\n variant === 'drilldown'\n ? { minHeight: TOUCH_TARGET, [atLeastRail(theme, breakpoint)]: { minHeight: 'auto' } }\n : null;\n const shared = {\n 'data-testid': `${testIdPrefix}-item-${item.id}`,\n sx: {\n borderRadius: 1.5,\n mb: 0.25,\n gap: 1.5,\n ...touchFloor,\n color: active ? 'primary.main' : 'text.secondary',\n '&.Mui-selected': {\n bgcolor: alpha(theme.palette.primary.main, 0.12),\n '&:hover': { bgcolor: alpha(theme.palette.primary.main, 0.18) },\n },\n },\n };\n const content = <RailItemContent item={item} active={active} testIdPrefix={testIdPrefix} />;\n\n // Listed but not reachable from here. Deliberately not a disabled button: a\n // disabled control says \"you may not\", and the true statement is \"not from\n // this screen\" — the section exists and someone else's screen owns it.\n if (item.inert) {\n return (\n <ListItem {...shared} data-inert=\"true\">\n {content}\n </ListItem>\n );\n }\n\n const interactive = {\n ...shared,\n selected: active,\n 'aria-current': active ? ('page' as const) : undefined,\n };\n\n if (item.href && linkComponent) {\n const LinkComponent = linkComponent;\n return (\n <ListItemButton\n component={LinkComponent}\n href={item.href}\n onClick={onNavigate}\n {...interactive}\n >\n {content}\n </ListItemButton>\n );\n }\n return (\n <ListItemButton\n onClick={() => {\n onSelectItem?.(item.id);\n onNavigate?.();\n }}\n {...interactive}\n >\n {content}\n </ListItemButton>\n );\n}\n\ninterface RailGroupProps extends RailShape {\n group: SettingsNavGroup;\n activeItemId?: string;\n linkComponent?: SettingsLayoutProps['linkComponent'];\n onSelectItem?: (id: string) => void;\n onNavigate?: () => void;\n testIdPrefix: string;\n}\n\n/**\n * The list's own grid, in `drilldown` at the index only.\n *\n * At the index on a wide phone or a tablet the list has room for two columns;\n * the rail column never does, so the second column is undone at exactly the\n * width where this stops being the page.\n */\nfunction indexListSx(shape: RailShape) {\n if (shape.variant !== 'drilldown' || !shape.atIndex) return null;\n return (theme: import('@mui/material').Theme) => ({\n display: 'grid',\n gridTemplateColumns: '1fr',\n columnGap: 8,\n [theme.breakpoints.up('sm')]: { gridTemplateColumns: '1fr 1fr' },\n [atLeastRail(theme, shape.breakpoint)]: { gridTemplateColumns: '1fr' },\n });\n}\n\n/** A category: header + optional description + its items. */\nexport function SettingsRailGroup({\n group,\n activeItemId,\n linkComponent,\n onSelectItem,\n onNavigate,\n testIdPrefix,\n ...shape\n}: RailGroupProps): React.JSX.Element {\n const grid = indexListSx(shape);\n return (\n <Box data-testid={`${testIdPrefix}-group-${group.id}`} sx={{ mb: 2 }}>\n <Typography\n variant=\"caption\"\n sx={{\n display: 'block',\n px: 1,\n color: 'text.secondary',\n fontWeight: 600,\n textTransform: 'uppercase',\n letterSpacing: '0.06em',\n }}\n >\n {group.label}\n </Typography>\n {group.description ? (\n <Typography variant=\"caption\" component=\"p\" sx={{ px: 1, mt: 0.25, color: 'text.disabled' }}>\n {group.description}\n </Typography>\n ) : null}\n <List\n disablePadding\n sx={(theme) => ({ mt: 0.5, ...(grid ? grid(theme) : null) })}\n >\n {group.items.map((item) => (\n <SettingsRailItem\n key={item.id}\n item={item}\n active={item.id === activeItemId}\n linkComponent={linkComponent}\n onSelectItem={onSelectItem}\n onNavigate={onNavigate}\n testIdPrefix={testIdPrefix}\n {...shape}\n />\n ))}\n </List>\n </Box>\n );\n}\n","import type { CSSObject, Theme } from '@mui/material';\n\nimport type { SettingsNavStatus, SettingsRailBreakpoint } from './SettingsLayout.types';\n\n/** Fixed width of the left navigation rail once it has its own column. */\nexport const RAIL_WIDTH = 300;\n\n/** Visually hidden, still read aloud — for text a marker's colour cannot carry. */\nexport const SR_ONLY_SX: CSSObject = {\n position: 'absolute',\n width: '1px',\n height: '1px',\n padding: 0,\n margin: '-1px',\n overflow: 'hidden',\n clip: 'rect(0, 0, 0, 0)',\n whiteSpace: 'nowrap',\n border: 0,\n};\n\n/**\n * The media query for \"the rail has its own column\".\n *\n * `theme.breakpoints.up` takes a NUMBER as happily as a name, which is why\n * `railBreakpoint` accepts one: a settings area whose structural switch is at\n * 1024px has no MUI breakpoint to name (the default `lg` is 1200), and rounding\n * the design to the nearest named breakpoint moves 176px of behaviour to keep a\n * string tidy.\n */\nexport function atLeastRail(theme: Theme, breakpoint: SettingsRailBreakpoint): string {\n return theme.breakpoints.up(breakpoint);\n}\n\n/**\n * A `display` pair across that query: `narrow` below it, `wide` at and above.\n *\n * Everything that changes shape between the two navigation modes goes through\n * this, so \"which width am I?\" is answered once, in CSS, for every part at\n * once — rather than by each part asking a media query in JavaScript and\n * answering slightly differently.\n */\nexport function displayAcrossRail(\n theme: Theme,\n breakpoint: SettingsRailBreakpoint,\n narrow: string,\n wide: string,\n): CSSObject {\n return { display: narrow, [atLeastRail(theme, breakpoint)]: { display: wide } };\n}\n\n/** Minimum touch target, in px. Held down to 320px-wide phones, deliberately. */\nexport const TOUCH_TARGET = 44;\n\n/**\n * Palette role per situation.\n *\n * Returned as theme keys rather than hex so a marker follows the host's theme —\n * and so light and dark do not need two copies of this table.\n */\nexport const STATUS_COLOR: Record<Exclude<SettingsNavStatus, 'locked'>, string> = {\n ok: 'success.main',\n off: 'text.disabled',\n new: 'error.main',\n};\n","'use client';\n\nimport LockOutlinedIcon from '@mui/icons-material/LockOutlined';\nimport { Box } from '@mui/material';\nimport React from 'react';\n\nimport { SR_ONLY_SX, STATUS_COLOR } from './SettingsLayout.styles';\nimport type { SettingsNavStatus } from './SettingsLayout.types';\n\nexport interface SettingsStatusMarkerProps {\n /** Which situation to draw. */\n status: SettingsNavStatus;\n /** What it means, in the host's language — the only carrier a screen reader gets. */\n label?: string;\n /** `data-testid` for the marker element. */\n testId?: string;\n}\n\n/**\n * The situation marker beside a settings entry: a coloured dot, or a padlock\n * when the plan does not include the section.\n *\n * The label is rendered as visually-hidden text INSIDE the marker rather than as\n * an `aria-label` on it, so it lands in the accessible name of the row that\n * contains it. A row reading \"Endereço, ligado\" is one utterance; a decorative\n * dot with a title attribute beside it is two, and the second one is skipped.\n *\n * Colour alone never says which of the three states this is — that is the whole\n * reason `label` exists, and why a marker without one is a marker with no\n * meaning attached.\n */\nexport function SettingsStatusMarker({\n status,\n label,\n testId,\n}: SettingsStatusMarkerProps): React.JSX.Element {\n const hidden = label ? <Box component=\"span\" sx={SR_ONLY_SX}>{label}</Box> : null;\n\n if (status === 'locked') {\n return (\n <Box\n component=\"span\"\n data-testid={testId}\n data-status={status}\n sx={{ display: 'inline-flex', alignItems: 'center', color: 'text.disabled' }}\n >\n <LockOutlinedIcon sx={{ fontSize: 16 }} />\n {hidden}\n </Box>\n );\n }\n\n return (\n <Box\n component=\"span\"\n data-testid={testId}\n data-status={status}\n sx={{ display: 'inline-flex', alignItems: 'center' }}\n >\n <Box\n component=\"span\"\n sx={{\n width: 8,\n height: 8,\n borderRadius: '50%',\n flex: '0 0 auto',\n bgcolor: STATUS_COLOR[status],\n }}\n />\n {hidden}\n </Box>\n );\n}\n\nSettingsStatusMarker.displayName = 'SettingsStatusMarker';\n","\"use client\";\n\nimport ChevronLeftIcon from \"@mui/icons-material/ChevronLeft\";\nimport type { CSSObject, Theme } from \"@mui/material\";\nimport { Box } from \"@mui/material\";\nimport React from \"react\";\n\nimport {\n atLeastRail,\n displayAcrossRail,\n TOUCH_TARGET,\n} from \"./SettingsLayout.styles\";\nimport { SettingsSectionChips } from \"./SettingsSectionChips\";\nimport type {\n SettingsLayoutProps,\n SettingsNavItem,\n SettingsRailBreakpoint,\n} from \"./SettingsLayout.types\";\n\nexport interface SettingsPanelProps {\n /** True in `drilldown`, inside a section rather than at the index. */\n inSection: boolean;\n /** True in `drilldown`, at the area's index. */\n hideOnNarrow: boolean;\n breakpoint: SettingsRailBreakpoint;\n indexHref?: string;\n backLabel: string;\n ariaLabel: string;\n sectionChips?: SettingsNavItem[];\n activeItemId?: string;\n linkComponent?: SettingsLayoutProps[\"linkComponent\"];\n onSelectItem?: (id: string) => void;\n testIdPrefix: string;\n children: React.ReactNode;\n /** Scroll inside the panel rather than with the page. See `fillHeight`. */\n fillHeight?: boolean;\n}\n\n/** The way back to the area's index — narrow widths only. */\nfunction BackLink({\n href,\n label,\n breakpoint,\n linkComponent,\n testIdPrefix,\n}: {\n href: string;\n label: string;\n breakpoint: SettingsRailBreakpoint;\n linkComponent: NonNullable<SettingsLayoutProps[\"linkComponent\"]>;\n testIdPrefix: string;\n}): React.JSX.Element {\n return (\n <Box\n component={linkComponent}\n href={href}\n aria-label={label}\n data-testid={`${testIdPrefix}-back`}\n sx={(theme) => ({\n ...displayAcrossRail(theme, breakpoint, \"inline-flex\", \"none\"),\n alignItems: \"center\",\n gap: 0.5,\n minHeight: TOUCH_TARGET,\n pr: 1,\n textDecoration: \"none\",\n color: \"text.secondary\",\n font: \"inherit\",\n fontSize: \"0.875rem\",\n })}\n >\n <ChevronLeftIcon fontSize=\"small\" />\n {label}\n </Box>\n );\n}\n\n/**\n * The panel's own box, as ONE object per media query rather than two.\n *\n * Both the `display` rule and `fillHeight`'s belong at the same wide-shape\n * query, and spreading them as separate `{ [query]: … }` objects makes the\n * second REPLACE the first — so turning `fillHeight` on silently dropped the\n * `display: block` that `hideOnNarrow` puts there and left the panel\n * `display: none` at every width. That only shows at a drilldown INDEX, where\n * `hideOnNarrow` is true, which is why a section route looked fine.\n *\n * `overflowY` here is also what lets a `position: sticky` toolbar inside\n * `children` pin to the TOP OF THE PANEL — under the host's header — instead\n * of to a document that is not the scroller.\n */\nfunction panelSx(\n breakpoint: SettingsRailBreakpoint,\n hideOnNarrow: boolean,\n fillHeight: boolean,\n): (theme: Theme) => CSSObject {\n return (theme) => ({\n display: hideOnNarrow ? \"none\" : \"block\",\n flex: \"1 1 auto\",\n minWidth: 0,\n width: \"100%\",\n [atLeastRail(theme, breakpoint)]: {\n ...(hideOnNarrow ? { display: \"block\" } : {}),\n ...(fillHeight\n ? {\n height: \"100%\",\n minHeight: 0,\n overflowY: \"auto\",\n overscrollBehavior: \"contain\",\n }\n : {}),\n },\n });\n}\n\n/**\n * The central column: the open screen, and — in `drilldown`, inside a section —\n * the back link and sibling-section strip above it.\n *\n * At the index in `drilldown` this whole column is `display: none` below the\n * breakpoint and the rail becomes the page. Mounted either way: the panel and\n * the list are one tree with `display` between them, not two branches of a\n * render.\n */\nexport function SettingsPanel({\n inSection,\n hideOnNarrow,\n breakpoint,\n indexHref,\n backLabel,\n ariaLabel,\n sectionChips,\n activeItemId,\n linkComponent,\n onSelectItem,\n testIdPrefix,\n fillHeight = false,\n children,\n}: SettingsPanelProps): React.JSX.Element {\n const showBack =\n inSection && indexHref !== undefined && linkComponent !== undefined;\n const showChips =\n inSection && sectionChips !== undefined && sectionChips.length > 0;\n\n return (\n <Box\n data-testid={`${testIdPrefix}-panel`}\n sx={panelSx(breakpoint, hideOnNarrow, fillHeight)}\n >\n {showBack ? (\n <BackLink\n href={indexHref}\n label={backLabel}\n breakpoint={breakpoint}\n linkComponent={linkComponent}\n testIdPrefix={testIdPrefix}\n />\n ) : null}\n\n {showChips ? (\n <Box\n sx={(theme) => ({\n ...displayAcrossRail(theme, breakpoint, \"block\", \"none\"),\n mb: 1,\n minWidth: 0,\n })}\n >\n <SettingsSectionChips\n items={sectionChips}\n activeItemId={activeItemId}\n ariaLabel={ariaLabel}\n linkComponent={linkComponent}\n onSelectItem={onSelectItem}\n testIdPrefix={testIdPrefix}\n />\n </Box>\n ) : null}\n\n {children}\n </Box>\n );\n}\n","'use client';\n\nimport { alpha, Box, useTheme, type Theme } from '@mui/material';\nimport React, { useEffect, useRef, type RefObject } from 'react';\n\nimport { SettingsStatusMarker } from './SettingsStatusMarker';\nimport { TOUCH_TARGET } from './SettingsLayout.styles';\nimport type { SettingsLayoutProps, SettingsNavItem } from './SettingsLayout.types';\n\nexport interface SettingsSectionChipsProps {\n /** The sibling sections to offer — usually the open item's own group. */\n items: SettingsNavItem[];\n /** Which one is open. */\n activeItemId?: string;\n /** Accessible name of the strip, in the host's language. */\n ariaLabel: string;\n linkComponent?: SettingsLayoutProps['linkComponent'];\n onSelectItem?: (id: string) => void;\n testIdPrefix: string;\n}\n\n/** True when the visitor asked the OS for less movement. Re-read per scroll. */\nfunction prefersReducedMotion(): boolean {\n if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return false;\n return window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n}\n\n/**\n * Keep the open section's chip in view, however the visitor got here.\n *\n * Keyed on `activeItemId` in an effect rather than done in a click handler:\n * arriving by the desktop rail, by a card, by a pasted link and by the browser's\n * back button all have to land the same way, and only one of those is a click on\n * a chip. Every re-render otherwise starts the strip at scroll 0, which parks\n * the active chip off the right edge on any list longer than a screen — the\n * visitor is told where they are by an element they cannot see.\n *\n * `itemsKey` rather than `items`: a host almost always rebuilds that array per\n * render (`groups.flatMap(...)`), and depending on it would re-run this on every\n * render and yank the strip back mid-drag.\n *\n * The key joins on NUL — as the ESCAPE `'\\u0000'`, which it must stay. A\n * literal NUL byte works identically at runtime and makes git classify this file\n * as BINARY: `git diff` prints \"Binary file not shown\" and the GitHub review UI\n * shows nothing at all. This file shipped that way once, and the cost was not\n * the byte — it was a whole change nobody could read.\n */\nfunction useCentreActiveChip(\n stripRef: RefObject<HTMLDivElement | null>,\n activeItemId: string | undefined,\n itemsKey: string,\n): void {\n useEffect(() => {\n const strip = stripRef.current;\n if (!strip || !activeItemId) return;\n // Walked rather than selected: an id is host data, and a section id carrying\n // a quote would turn a selector string into a thrown SyntaxError — which\n // here would take the whole strip down to centre one chip.\n const chip = Array.from(strip.children).find(\n (node): node is HTMLElement =>\n node instanceof HTMLElement && node.dataset.chipId === activeItemId,\n );\n if (!chip) return;\n // Feature-checked, not assumed. `Element.scrollTo` is absent in jsdom and in\n // a few embedded webviews, and an unguarded call there THROWS out of an\n // effect — which React escalates to the nearest error boundary. The whole\n // settings area would go down to centre one chip, and the host's own tests\n // could not render the shell at all. Same reasoning as walking the children\n // instead of building a selector: nothing in here is worth a crash.\n if (typeof strip.scrollTo !== 'function') return;\n const target = chip.offsetLeft + chip.offsetWidth / 2 - strip.clientWidth / 2;\n const left = Math.max(0, Math.min(target, strip.scrollWidth - strip.clientWidth));\n strip.scrollTo({ left, behavior: prefersReducedMotion() ? 'auto' : 'smooth' });\n }, [stripRef, activeItemId, itemsKey]);\n}\n\n/** One chip: a link when the host gave it a target, else a button. */\nfunction SectionChip({\n item,\n active,\n theme,\n linkComponent,\n onSelectItem,\n testIdPrefix,\n}: {\n item: SettingsNavItem;\n active: boolean;\n theme: Theme;\n linkComponent?: SettingsLayoutProps['linkComponent'];\n onSelectItem?: (id: string) => void;\n testIdPrefix: string;\n}): React.JSX.Element {\n const asLink = item.href !== undefined && linkComponent !== undefined;\n const behaviour = asLink\n ? { component: linkComponent, href: item.href }\n : { component: 'button' as const, type: 'button' as const, onClick: () => onSelectItem?.(item.id) };\n\n return (\n <Box\n {...behaviour}\n data-chip-id={item.id}\n data-testid={`${testIdPrefix}-chip-${item.id}`}\n aria-current={active ? 'page' : undefined}\n sx={{\n display: 'inline-flex',\n alignItems: 'center',\n gap: 0.75,\n flex: '0 0 auto',\n minHeight: TOUCH_TARGET,\n px: 1.5,\n borderRadius: 999,\n cursor: 'pointer',\n font: 'inherit',\n fontSize: '0.8125rem',\n fontWeight: active ? 700 : 500,\n textDecoration: 'none',\n whiteSpace: 'nowrap',\n color: active ? 'primary.main' : 'text.secondary',\n bgcolor: active ? alpha(theme.palette.primary.main, 0.12) : 'transparent',\n border: `1px solid ${active ? alpha(theme.palette.primary.main, 0.4) : theme.palette.divider}`,\n }}\n >\n {item.status ? (\n <SettingsStatusMarker\n status={item.status}\n label={item.statusLabel}\n testId={`${testIdPrefix}-chip-status-${item.id}`}\n />\n ) : null}\n {item.label}\n </Box>\n );\n}\n\n/**\n * The narrow-width section strip: horizontally scrollable chips for the sections\n * beside the open one.\n *\n * It clips its own overflow — `overflow-x: auto` on the strip, never on an\n * ancestor. A strip that pushes the page wide makes every OTHER screen at that\n * width scroll sideways too, and the cause is nowhere near the symptom.\n */\nexport function SettingsSectionChips({\n items,\n activeItemId,\n ariaLabel,\n linkComponent,\n onSelectItem,\n testIdPrefix,\n}: SettingsSectionChipsProps): React.JSX.Element {\n const theme = useTheme();\n const stripRef = useRef<HTMLDivElement | null>(null);\n useCentreActiveChip(stripRef, activeItemId, items.map((item) => item.id).join('\\u0000'));\n\n return (\n <Box\n ref={stripRef}\n component=\"nav\"\n aria-label={ariaLabel}\n data-testid={`${testIdPrefix}-chips`}\n sx={{\n display: 'flex',\n alignItems: 'center',\n gap: 0.75,\n px: 0.25,\n py: 0.75,\n overflowX: 'auto',\n overflowY: 'hidden',\n scrollbarWidth: 'none',\n '&::-webkit-scrollbar': { display: 'none' },\n }}\n >\n {items.map((item) => (\n <SectionChip\n key={item.id}\n item={item}\n active={item.id === activeItemId}\n theme={theme}\n linkComponent={linkComponent}\n onSelectItem={onSelectItem}\n testIdPrefix={testIdPrefix}\n />\n ))}\n </Box>\n );\n}\n\nSettingsSectionChips.displayName = 'SettingsSectionChips';\n"],"mappings":"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","names":["Box","Box","Typography","Box","jsx","jsxs","Box","jsx","jsxs","Box","Typography","Box","alpha","Box","useTheme","jsx","jsxs","Box","alpha","useTheme","jsx","jsxs","Box","jsx","jsxs","Box"]}
1
+ {"version":3,"sources":["../../src/components/layout/SettingsLayout/SettingsLayout.tsx","../../src/components/layout/SettingsLayout/SettingsLayout.filters.ts","../../src/components/layout/SettingsLayout/SettingsLayout.rail.tsx","../../src/components/layout/SettingsLayout/SettingsLayout.items.tsx","../../src/components/layout/SettingsLayout/SettingsLayout.styles.ts","../../src/components/layout/SettingsLayout/SettingsStatusMarker.tsx","../../src/components/layout/SettingsLayout/SettingsPanel.tsx","../../src/components/layout/SettingsLayout/SettingsSectionChips.tsx","../../src/components/layout/SettingsLayout/SettingsSectionHeader.tsx"],"sourcesContent":["'use client';\n\nimport { Box, type Theme } from '@mui/material';\nimport React, { useMemo, useState } from 'react';\n\nimport { withDefaults } from '../../../utils/withDefaults';\nimport { activeItemLabel, filterGroups } from './SettingsLayout.filters';\nimport { SettingsRail } from './SettingsLayout.rail';\nimport { atLeastRail } from './SettingsLayout.styles';\nimport { SettingsPanel } from './SettingsPanel';\nimport type { SettingsLayoutProps } from './SettingsLayout.types';\n\n/**\n * The props with every default already applied.\n *\n * A table through `withDefaults` rather than a `= default` per destructured\n * prop: each of those is a branch, and seven of them put the component over the\n * complexity bar before it renders anything. The single cast is what buys the\n * body non-optional values — without it every use site would restate the\n * default as a `??`, which is the same default written twice and free to drift.\n */\ntype ResolvedProps = SettingsLayoutProps &\n Required<\n Pick<\n SettingsLayoutProps,\n | 'searchPlaceholder'\n | 'defaultQuery'\n | 'emptySearchLabel'\n | 'testIdPrefix'\n | 'railBreakpoint'\n | 'navVariant'\n | 'atIndex'\n | 'backLabel'\n >\n >;\n\nconst DEFAULTS = {\n searchPlaceholder: 'Search settings',\n defaultQuery: '',\n emptySearchLabel: 'No settings match your search.',\n testIdPrefix: 'settings',\n railBreakpoint: 'md',\n navVariant: 'switcher',\n atIndex: false,\n backLabel: 'Back',\n} satisfies Partial<SettingsLayoutProps>;\n\n/** Stacked below the rail's breakpoint, two columns at and above it. */\nfunction shellSx(breakpoint: SettingsLayoutProps['railBreakpoint'], fillHeight: boolean) {\n return (theme: Theme) => ({\n display: 'flex',\n flexDirection: 'column' as const,\n alignItems: 'stretch',\n gap: 1.5,\n width: '100%',\n minWidth: 0,\n // Only ever at the wide shape. Narrow, the rail is a disclosure or the page\n // itself, and two independent scrollbars on a phone is a trap rather than a\n // convenience.\n [atLeastRail(theme, breakpoint ?? 'md')]: {\n flexDirection: 'row',\n gap: 3,\n ...(fillHeight ? { height: '100%', minHeight: 0 } : {}),\n },\n });\n}\n\n/**\n * A reusable, agnostic Facebook-style settings shell: a searchable left rail of\n * grouped categories/subcategories and a central panel that renders the selected\n * configuration screen (`children`). Navigation is data-driven via `groups`; the\n * host owns routing (pass `linkComponent` + item `href`, or handle `onSelectItem`).\n *\n * ## Two narrow-width shapes\n *\n * `navVariant=\"switcher\"` (the default) folds the rail into a disclosure above\n * the panel. `navVariant=\"drilldown\"` gives the phone shape instead: at the\n * area's index the LIST is the page, and inside a section the panel is the page\n * with a back link and a scrollable strip of sibling sections above it.\n *\n * ## Why the width decision is CSS and not JavaScript\n *\n * Every part that changes between the two shapes does it through a `display`\n * pair keyed on `railBreakpoint`. Nothing here asks a media query in JS and\n * renders one branch.\n *\n * That is not a performance preference. A layout that RENDERS one navigation and\n * not the other can offer the narrow width less than the wide one, and no test\n * or review catches it — the missing control simply is not in the tree to be\n * asserted about. With both forms always mounted, \"the narrow width reaches\n * everything the wide one does\" holds by construction, and an audit that counts\n * controls per width gets the same answer at both.\n */\nexport const SettingsLayout: React.FC<SettingsLayoutProps> = (props) => {\n const {\n title,\n groups,\n activeItemId,\n onSelectItem,\n searchPlaceholder,\n defaultQuery,\n emptySearchLabel,\n emptySearchAction,\n linkComponent,\n children,\n testIdPrefix,\n railBreakpoint,\n fillHeight = false,\n navVariant,\n atIndex,\n indexHref,\n backLabel,\n sectionChips,\n } = withDefaults(props, DEFAULTS) as ResolvedProps;\n const [query, setQuery] = useState(defaultQuery);\n const [navOpen, setNavOpen] = useState(false);\n const filteredGroups = useMemo(() => filterGroups(groups, query), [groups, query]);\n const activeLabel = useMemo(() => activeItemLabel(groups, activeItemId), [groups, activeItemId]);\n\n const drilldown = navVariant === 'drilldown';\n const ariaLabel = typeof title === 'string' ? title : 'Settings';\n\n return (\n <Box\n data-testid={testIdPrefix}\n data-nav-variant={navVariant}\n data-at-index={atIndex ? 'true' : 'false'}\n sx={shellSx(railBreakpoint, fillHeight)}\n >\n <SettingsRail\n title={title}\n activeLabel={activeLabel}\n navOpen={navOpen}\n onToggleNav={() => setNavOpen((open) => !open)}\n query={query}\n onQueryChange={setQuery}\n searchPlaceholder={searchPlaceholder}\n emptySearchLabel={emptySearchLabel}\n emptySearchAction={emptySearchAction}\n filteredGroups={filteredGroups}\n activeItemId={activeItemId}\n linkComponent={linkComponent}\n onSelectItem={onSelectItem}\n onNavigate={() => setNavOpen(false)}\n testIdPrefix={testIdPrefix}\n variant={navVariant}\n breakpoint={railBreakpoint}\n atIndex={atIndex}\n fillHeight={fillHeight}\n />\n\n <SettingsPanel\n inSection={drilldown && !atIndex}\n hideOnNarrow={drilldown && atIndex}\n breakpoint={railBreakpoint}\n indexHref={indexHref}\n sectionTitle={props.sectionTitle}\n sectionDescription={props.sectionDescription}\n backLabel={backLabel}\n ariaLabel={ariaLabel}\n sectionChips={sectionChips}\n activeItemId={activeItemId}\n linkComponent={linkComponent}\n onSelectItem={onSelectItem}\n testIdPrefix={testIdPrefix}\n fillHeight={fillHeight}\n >\n {children}\n </SettingsPanel>\n </Box>\n );\n};\n\nSettingsLayout.displayName = 'SettingsLayout';\n","import type { SettingsNavGroup, SettingsNavItem } from './SettingsLayout.types';\n\n/** Case-insensitive filter over item label + keywords; drops emptied groups. */\nexport function filterGroups(groups: SettingsNavGroup[], query: string): SettingsNavGroup[] {\n const needle = query.trim().toLowerCase();\n if (!needle) return groups;\n const matches = (item: SettingsNavItem): boolean =>\n item.label.toLowerCase().includes(needle) ||\n (item.keywords ?? []).some((keyword) => keyword.toLowerCase().includes(needle));\n return groups\n .map((group) => ({ ...group, items: group.items.filter(matches) }))\n .filter((group) => group.items.length > 0);\n}\n\n/** Label of the active item, shown in the `switcher`'s collapsed header. */\nexport function activeItemLabel(\n groups: SettingsNavGroup[],\n activeItemId?: string,\n): string | undefined {\n return groups.flatMap((group) => group.items).find((item) => item.id === activeItemId)?.label;\n}\n","'use client';\n\nimport ExpandMoreIcon from '@mui/icons-material/ExpandMore';\nimport SearchIcon from '@mui/icons-material/Search';\nimport { Box, Button, InputAdornment, TextField, Typography } from '@mui/material';\nimport React from 'react';\n\nimport { SettingsRailGroup, type RailShape } from './SettingsLayout.items';\nimport { atLeastRail, displayAcrossRail, RAIL_WIDTH, TOUCH_TARGET } from './SettingsLayout.styles';\nimport type {\n SettingsEmptySearchAction,\n SettingsLayoutProps,\n SettingsNavGroup,\n SettingsRailBreakpoint,\n} from './SettingsLayout.types';\n\ninterface RailToggleProps {\n title?: React.ReactNode;\n activeLabel?: string;\n open: boolean;\n onToggle: () => void;\n breakpoint: SettingsRailBreakpoint;\n testIdPrefix: string;\n}\n\n/**\n * `switcher`-only collapse control: shows the current section (a compact\n * switcher) so the panel content stays at the top of the screen. Hidden once the\n * rail has its own column.\n */\nfunction SettingsRailToggle({\n title,\n activeLabel,\n open,\n onToggle,\n breakpoint,\n testIdPrefix,\n}: RailToggleProps): React.JSX.Element {\n return (\n <Box\n component=\"button\"\n type=\"button\"\n onClick={onToggle}\n aria-expanded={open}\n aria-label={typeof title === 'string' ? title : 'Settings'}\n data-testid={`${testIdPrefix}-toggle`}\n sx={(theme) => ({\n ...displayAcrossRail(theme, breakpoint, 'flex', 'none'),\n alignItems: 'center',\n justifyContent: 'space-between',\n gap: 1,\n width: '100%',\n minHeight: TOUCH_TARGET,\n px: 1.5,\n py: 1,\n mb: 1,\n textAlign: 'left',\n cursor: 'pointer',\n color: 'text.primary',\n background: 'transparent',\n border: `1px solid ${theme.palette.divider}`,\n borderRadius: 1.5,\n })}\n >\n <Box sx={{ minWidth: 0 }}>\n {title ? (\n <Typography variant=\"caption\" sx={{ display: 'block', color: 'text.secondary' }}>\n {title}\n </Typography>\n ) : null}\n <Typography variant=\"body2\" noWrap sx={{ fontWeight: 600 }}>\n {activeLabel ?? ''}\n </Typography>\n </Box>\n <ExpandMoreIcon\n fontSize=\"small\"\n sx={{\n flex: '0 0 auto',\n color: 'text.secondary',\n transition: 'transform 150ms ease',\n transform: open ? 'rotate(180deg)' : 'none',\n '@media (prefers-reduced-motion: reduce)': { transition: 'none' },\n }}\n />\n </Box>\n );\n}\n\n/** The empty-search state, with the way out of it rendered inside. */\nfunction SettingsSearchEmpty({\n label,\n action,\n onClear,\n testIdPrefix,\n}: {\n label: React.ReactNode;\n action?: SettingsEmptySearchAction;\n onClear: () => void;\n testIdPrefix: string;\n}): React.JSX.Element {\n return (\n <Box data-testid={`${testIdPrefix}-empty`} sx={{ px: 1, py: 2 }}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {label}\n </Typography>\n {action ? (\n <Button\n size=\"small\"\n variant=\"outlined\"\n onClick={() => {\n onClear();\n action.onClear?.();\n }}\n data-testid={`${testIdPrefix}-empty-action`}\n sx={{ mt: 1.5, minHeight: TOUCH_TARGET }}\n >\n {action.label}\n </Button>\n ) : null}\n </Box>\n );\n}\n\nexport interface RailBodyProps extends RailShape {\n navOpen: boolean;\n query: string;\n onQueryChange: (value: string) => void;\n searchPlaceholder: string;\n emptySearchLabel: React.ReactNode;\n emptySearchAction?: SettingsEmptySearchAction;\n filteredGroups: SettingsNavGroup[];\n activeItemId?: string;\n linkComponent?: SettingsLayoutProps['linkComponent'];\n onSelectItem?: (id: string) => void;\n onNavigate?: () => void;\n testIdPrefix: string;\n}\n\n/** The searchable rail body (search + groups). */\nfunction SettingsRailBody({\n navOpen,\n query,\n onQueryChange,\n searchPlaceholder,\n emptySearchLabel,\n emptySearchAction,\n filteredGroups,\n activeItemId,\n linkComponent,\n onSelectItem,\n onNavigate,\n testIdPrefix,\n ...shape\n}: RailBodyProps): React.JSX.Element {\n // `drilldown` has no disclosure to be closed behind: below the breakpoint the\n // body is the page, above it the rail. Only `switcher` hides it.\n const hidden = navOpen ? 'block' : 'none';\n return (\n <Box\n data-testid={`${testIdPrefix}-rail-body`}\n sx={(theme) =>\n shape.variant === 'drilldown'\n ? { display: 'block' }\n : displayAcrossRail(theme, shape.breakpoint, hidden, 'block')\n }\n >\n <TextField\n value={query}\n onChange={(event) => onQueryChange(event.target.value)}\n placeholder={searchPlaceholder}\n size=\"small\"\n fullWidth\n aria-label={searchPlaceholder}\n data-testid={`${testIdPrefix}-search`}\n InputProps={{\n startAdornment: (\n <InputAdornment position=\"start\">\n <SearchIcon fontSize=\"small\" sx={{ color: 'text.secondary' }} />\n </InputAdornment>\n ),\n }}\n sx={{ mb: 2 }}\n />\n {filteredGroups.length === 0 ? (\n <SettingsSearchEmpty\n label={emptySearchLabel}\n action={emptySearchAction}\n onClear={() => onQueryChange('')}\n testIdPrefix={testIdPrefix}\n />\n ) : (\n filteredGroups.map((group) => (\n <SettingsRailGroup\n key={group.id}\n group={group}\n activeItemId={activeItemId}\n linkComponent={linkComponent}\n onSelectItem={onSelectItem}\n onNavigate={onNavigate}\n testIdPrefix={testIdPrefix}\n {...shape}\n />\n ))\n )}\n </Box>\n );\n}\n\n/** Props for {@link SettingsRail}: the rail body's inputs plus the mobile chrome. */\nexport type SettingsRailProps = RailBodyProps & {\n title?: React.ReactNode;\n activeLabel?: string;\n onToggleNav: () => void;\n /** Scroll inside the rail rather than with the page. See `fillHeight`. */\n fillHeight?: boolean;\n};\n\n/** The navigation column: title, the `switcher`'s disclosure, and the body. */\nexport function SettingsRail({\n title,\n activeLabel,\n navOpen,\n onToggleNav,\n testIdPrefix,\n fillHeight = false,\n ...bodyProps\n}: SettingsRailProps): React.JSX.Element {\n const { variant, breakpoint, atIndex } = bodyProps;\n // In `drilldown` the rail is the index's list below the breakpoint and the\n // left column above it — so inside a section it steps aside for the chip\n // strip. It is `display: none`, never unmounted: the two navigation forms\n // both stay in the DOM at every width, which is what makes \"the narrow width\n // offers what the wide one offers\" true by construction instead of by review.\n const hiddenWhileInSection = variant === 'drilldown' && !atIndex;\n\n return (\n <Box\n component=\"nav\"\n aria-label={typeof title === 'string' ? title : 'Settings'}\n data-testid={`${testIdPrefix}-rail`}\n sx={(theme) => ({\n display: hiddenWhileInSection ? 'none' : 'block',\n flex: '0 0 auto',\n width: '100%',\n [atLeastRail(theme, breakpoint)]: {\n display: 'block',\n flex: `0 0 ${RAIL_WIDTH}px`,\n width: RAIL_WIDTH,\n borderRight: `1px solid ${theme.palette.divider}`,\n pr: 2,\n // Its own scrollbar, so a settings area with twenty categories does\n // not make the thing you NAVIGATE with scroll away while you read the\n // panel. `minHeight: 0` is the load-bearing half: without it a flex\n // child refuses to shrink below its content and `overflow` never\n // engages.\n ...(fillHeight\n ? { height: '100%', minHeight: 0, overflowY: 'auto' as const, overscrollBehavior: 'contain' as const }\n : {}),\n },\n })}\n >\n {title ? (\n <Typography\n variant=\"h6\"\n data-testid={`${testIdPrefix}-title`}\n sx={(theme) => ({\n ...(variant === 'drilldown'\n ? { display: 'block' }\n : displayAcrossRail(theme, breakpoint, 'none', 'block')),\n fontWeight: 700,\n mb: 1.5,\n })}\n >\n {title}\n </Typography>\n ) : null}\n {variant === 'switcher' ? (\n <SettingsRailToggle\n title={title}\n activeLabel={activeLabel}\n open={navOpen}\n onToggle={onToggleNav}\n breakpoint={breakpoint}\n testIdPrefix={testIdPrefix}\n />\n ) : null}\n <SettingsRailBody navOpen={navOpen} testIdPrefix={testIdPrefix} {...bodyProps} />\n </Box>\n );\n}\n","'use client';\n\nimport {\n alpha,\n Box,\n List,\n ListItem,\n ListItemButton,\n ListItemIcon,\n ListItemText,\n Typography,\n useTheme,\n} from '@mui/material';\nimport React from 'react';\n\nimport { atLeastRail, TOUCH_TARGET } from './SettingsLayout.styles';\nimport { SettingsStatusMarker } from './SettingsStatusMarker';\nimport type {\n SettingsLayoutProps,\n SettingsNavGroup,\n SettingsNavItem,\n SettingsNavVariant,\n SettingsRailBreakpoint,\n} from './SettingsLayout.types';\n\n/** The shape-deciding inputs every rail part needs to know about. */\nexport interface RailShape {\n variant: SettingsNavVariant;\n breakpoint: SettingsRailBreakpoint;\n atIndex: boolean;\n}\n\ninterface RailItemProps extends RailShape {\n item: SettingsNavItem;\n active: boolean;\n linkComponent?: SettingsLayoutProps['linkComponent'];\n onSelectItem?: (id: string) => void;\n /** Called on activation so the `switcher`'s rail can collapse after a choice. */\n onNavigate?: () => void;\n testIdPrefix: string;\n}\n\n/** Label, plus the situation marker when the host resolved one. */\nfunction RailItemContent({\n item,\n active,\n testIdPrefix,\n}: {\n item: SettingsNavItem;\n active: boolean;\n testIdPrefix: string;\n}): React.JSX.Element {\n return (\n <>\n {item.icon ? (\n <ListItemIcon sx={{ minWidth: 0, color: 'inherit' }}>{item.icon}</ListItemIcon>\n ) : null}\n <ListItemText\n primary={item.label}\n primaryTypographyProps={{ variant: 'body2', noWrap: true, fontWeight: active ? 600 : 500 }}\n />\n {item.status ? (\n <SettingsStatusMarker\n status={item.status}\n label={item.statusLabel}\n testId={`${testIdPrefix}-status-${item.id}`}\n />\n ) : null}\n </>\n );\n}\n\n/** One rail entry — a link when it has `href`, inert text when it says so, else a button. */\nexport function SettingsRailItem({\n item,\n active,\n linkComponent,\n onSelectItem,\n onNavigate,\n testIdPrefix,\n variant,\n breakpoint,\n}: RailItemProps): React.JSX.Element {\n const theme = useTheme();\n // In `drilldown` the narrow rail IS the page's list, so its rows are the\n // primary touch targets rather than a secondary column — hence the 44px floor\n // below the breakpoint. Above it, the row goes back to rail density.\n const touchFloor =\n variant === 'drilldown'\n ? { minHeight: TOUCH_TARGET, [atLeastRail(theme, breakpoint)]: { minHeight: 'auto' } }\n : null;\n const shared = {\n 'data-testid': `${testIdPrefix}-item-${item.id}`,\n sx: {\n borderRadius: 1.5,\n mb: 0.25,\n gap: 1.5,\n ...touchFloor,\n color: active ? 'primary.main' : 'text.secondary',\n '&.Mui-selected': {\n bgcolor: alpha(theme.palette.primary.main, 0.12),\n '&:hover': { bgcolor: alpha(theme.palette.primary.main, 0.18) },\n },\n },\n };\n const content = <RailItemContent item={item} active={active} testIdPrefix={testIdPrefix} />;\n\n // Listed but not reachable from here. Deliberately not a disabled button: a\n // disabled control says \"you may not\", and the true statement is \"not from\n // this screen\" — the section exists and someone else's screen owns it.\n if (item.inert) {\n return (\n <ListItem {...shared} data-inert=\"true\">\n {content}\n </ListItem>\n );\n }\n\n const interactive = {\n ...shared,\n selected: active,\n 'aria-current': active ? ('page' as const) : undefined,\n };\n\n if (item.href && linkComponent) {\n const LinkComponent = linkComponent;\n return (\n <ListItemButton\n component={LinkComponent}\n href={item.href}\n onClick={onNavigate}\n {...interactive}\n >\n {content}\n </ListItemButton>\n );\n }\n return (\n <ListItemButton\n onClick={() => {\n onSelectItem?.(item.id);\n onNavigate?.();\n }}\n {...interactive}\n >\n {content}\n </ListItemButton>\n );\n}\n\ninterface RailGroupProps extends RailShape {\n group: SettingsNavGroup;\n activeItemId?: string;\n linkComponent?: SettingsLayoutProps['linkComponent'];\n onSelectItem?: (id: string) => void;\n onNavigate?: () => void;\n testIdPrefix: string;\n}\n\n/**\n * The list's own grid, in `drilldown` at the index only.\n *\n * At the index on a wide phone or a tablet the list has room for two columns;\n * the rail column never does, so the second column is undone at exactly the\n * width where this stops being the page.\n */\nfunction indexListSx(shape: RailShape) {\n if (shape.variant !== 'drilldown' || !shape.atIndex) return null;\n return (theme: import('@mui/material').Theme) => ({\n display: 'grid',\n gridTemplateColumns: '1fr',\n columnGap: 8,\n [theme.breakpoints.up('sm')]: { gridTemplateColumns: '1fr 1fr' },\n [atLeastRail(theme, shape.breakpoint)]: { gridTemplateColumns: '1fr' },\n });\n}\n\n/** A category: header + optional description + its items. */\nexport function SettingsRailGroup({\n group,\n activeItemId,\n linkComponent,\n onSelectItem,\n onNavigate,\n testIdPrefix,\n ...shape\n}: RailGroupProps): React.JSX.Element {\n const grid = indexListSx(shape);\n return (\n <Box data-testid={`${testIdPrefix}-group-${group.id}`} sx={{ mb: 2 }}>\n <Typography\n variant=\"caption\"\n sx={{\n display: 'block',\n px: 1,\n color: 'text.secondary',\n fontWeight: 600,\n textTransform: 'uppercase',\n letterSpacing: '0.06em',\n }}\n >\n {group.label}\n </Typography>\n {group.description ? (\n <Typography variant=\"caption\" component=\"p\" sx={{ px: 1, mt: 0.25, color: 'text.disabled' }}>\n {group.description}\n </Typography>\n ) : null}\n <List\n disablePadding\n sx={(theme) => ({ mt: 0.5, ...(grid ? grid(theme) : null) })}\n >\n {group.items.map((item) => (\n <SettingsRailItem\n key={item.id}\n item={item}\n active={item.id === activeItemId}\n linkComponent={linkComponent}\n onSelectItem={onSelectItem}\n onNavigate={onNavigate}\n testIdPrefix={testIdPrefix}\n {...shape}\n />\n ))}\n </List>\n </Box>\n );\n}\n","import type { CSSObject, Theme } from '@mui/material';\n\nimport type { SettingsNavStatus, SettingsRailBreakpoint } from './SettingsLayout.types';\n\n/** Fixed width of the left navigation rail once it has its own column. */\nexport const RAIL_WIDTH = 300;\n\n/** Visually hidden, still read aloud — for text a marker's colour cannot carry. */\nexport const SR_ONLY_SX: CSSObject = {\n position: 'absolute',\n width: '1px',\n height: '1px',\n padding: 0,\n margin: '-1px',\n overflow: 'hidden',\n clip: 'rect(0, 0, 0, 0)',\n whiteSpace: 'nowrap',\n border: 0,\n};\n\n/**\n * The media query for \"the rail has its own column\".\n *\n * `theme.breakpoints.up` takes a NUMBER as happily as a name, which is why\n * `railBreakpoint` accepts one: a settings area whose structural switch is at\n * 1024px has no MUI breakpoint to name (the default `lg` is 1200), and rounding\n * the design to the nearest named breakpoint moves 176px of behaviour to keep a\n * string tidy.\n */\nexport function atLeastRail(theme: Theme, breakpoint: SettingsRailBreakpoint): string {\n return theme.breakpoints.up(breakpoint);\n}\n\n/**\n * A `display` pair across that query: `narrow` below it, `wide` at and above.\n *\n * Everything that changes shape between the two navigation modes goes through\n * this, so \"which width am I?\" is answered once, in CSS, for every part at\n * once — rather than by each part asking a media query in JavaScript and\n * answering slightly differently.\n */\nexport function displayAcrossRail(\n theme: Theme,\n breakpoint: SettingsRailBreakpoint,\n narrow: string,\n wide: string,\n): CSSObject {\n return { display: narrow, [atLeastRail(theme, breakpoint)]: { display: wide } };\n}\n\n/** Minimum touch target, in px. Held down to 320px-wide phones, deliberately. */\nexport const TOUCH_TARGET = 44;\n\n/**\n * Palette role per situation.\n *\n * Returned as theme keys rather than hex so a marker follows the host's theme —\n * and so light and dark do not need two copies of this table.\n */\nexport const STATUS_COLOR: Record<Exclude<SettingsNavStatus, 'locked'>, string> = {\n ok: 'success.main',\n off: 'text.disabled',\n new: 'error.main',\n};\n","'use client';\n\nimport LockOutlinedIcon from '@mui/icons-material/LockOutlined';\nimport { Box } from '@mui/material';\nimport React from 'react';\n\nimport { SR_ONLY_SX, STATUS_COLOR } from './SettingsLayout.styles';\nimport type { SettingsNavStatus } from './SettingsLayout.types';\n\nexport interface SettingsStatusMarkerProps {\n /** Which situation to draw. */\n status: SettingsNavStatus;\n /** What it means, in the host's language — the only carrier a screen reader gets. */\n label?: string;\n /** `data-testid` for the marker element. */\n testId?: string;\n}\n\n/**\n * The situation marker beside a settings entry: a coloured dot, or a padlock\n * when the plan does not include the section.\n *\n * The label is rendered as visually-hidden text INSIDE the marker rather than as\n * an `aria-label` on it, so it lands in the accessible name of the row that\n * contains it. A row reading \"Endereço, ligado\" is one utterance; a decorative\n * dot with a title attribute beside it is two, and the second one is skipped.\n *\n * Colour alone never says which of the three states this is — that is the whole\n * reason `label` exists, and why a marker without one is a marker with no\n * meaning attached.\n */\nexport function SettingsStatusMarker({\n status,\n label,\n testId,\n}: SettingsStatusMarkerProps): React.JSX.Element {\n const hidden = label ? <Box component=\"span\" sx={SR_ONLY_SX}>{label}</Box> : null;\n\n if (status === 'locked') {\n return (\n <Box\n component=\"span\"\n data-testid={testId}\n data-status={status}\n sx={{ display: 'inline-flex', alignItems: 'center', color: 'text.disabled' }}\n >\n <LockOutlinedIcon sx={{ fontSize: 16 }} />\n {hidden}\n </Box>\n );\n }\n\n return (\n <Box\n component=\"span\"\n data-testid={testId}\n data-status={status}\n sx={{ display: 'inline-flex', alignItems: 'center' }}\n >\n <Box\n component=\"span\"\n sx={{\n width: 8,\n height: 8,\n borderRadius: '50%',\n flex: '0 0 auto',\n bgcolor: STATUS_COLOR[status],\n }}\n />\n {hidden}\n </Box>\n );\n}\n\nSettingsStatusMarker.displayName = 'SettingsStatusMarker';\n","\"use client\";\n\nimport ChevronLeftIcon from \"@mui/icons-material/ChevronLeft\";\nimport type { CSSObject, Theme } from \"@mui/material\";\nimport { Box } from \"@mui/material\";\nimport React from \"react\";\n\nimport {\n atLeastRail,\n displayAcrossRail,\n TOUCH_TARGET,\n} from \"./SettingsLayout.styles\";\nimport { SettingsSectionChips } from \"./SettingsSectionChips\";\nimport { SettingsSectionHeader } from \"./SettingsSectionHeader\";\nimport type {\n SettingsLayoutProps,\n SettingsNavItem,\n SettingsRailBreakpoint,\n} from \"./SettingsLayout.types\";\n\nexport interface SettingsPanelProps {\n /** True in `drilldown`, inside a section rather than at the index. */\n inSection: boolean;\n /** True in `drilldown`, at the area's index. */\n hideOnNarrow: boolean;\n breakpoint: SettingsRailBreakpoint;\n indexHref?: string;\n backLabel: string;\n /** The open section's own name — turns the back link into a compact header. */\n sectionTitle?: string;\n /** One line under that name. */\n sectionDescription?: string;\n ariaLabel: string;\n sectionChips?: SettingsNavItem[];\n activeItemId?: string;\n linkComponent?: SettingsLayoutProps[\"linkComponent\"];\n onSelectItem?: (id: string) => void;\n testIdPrefix: string;\n children: React.ReactNode;\n /** Scroll inside the panel rather than with the page. See `fillHeight`. */\n fillHeight?: boolean;\n}\n\n/** The way back to the area's index — narrow widths only. */\nfunction BackLink({\n href,\n label,\n breakpoint,\n linkComponent,\n testIdPrefix,\n}: {\n href: string;\n label: string;\n breakpoint: SettingsRailBreakpoint;\n linkComponent: NonNullable<SettingsLayoutProps[\"linkComponent\"]>;\n testIdPrefix: string;\n}): React.JSX.Element {\n return (\n <Box\n component={linkComponent}\n href={href}\n aria-label={label}\n data-testid={`${testIdPrefix}-back`}\n sx={(theme) => ({\n ...displayAcrossRail(theme, breakpoint, \"inline-flex\", \"none\"),\n alignItems: \"center\",\n gap: 0.5,\n minHeight: TOUCH_TARGET,\n pr: 1,\n textDecoration: \"none\",\n color: \"text.secondary\",\n font: \"inherit\",\n fontSize: \"0.875rem\",\n })}\n >\n <ChevronLeftIcon fontSize=\"small\" />\n {label}\n </Box>\n );\n}\n\n/**\n * Two shapes of the same control, chosen by whether the host named the section.\n *\n * With a `sectionTitle` the back folds into a compact header that also says\n * where you are; without one the original text link is kept, so no existing\n * consumer's header grows a title and pushes their first field down the page on\n * upgrade. Its own component so `SettingsPanel` keeps one branch rather than\n * three.\n */\nfunction PanelBack({\n href,\n backLabel,\n sectionTitle,\n sectionDescription,\n breakpoint,\n linkComponent,\n testIdPrefix,\n}: {\n href: string;\n backLabel: string;\n sectionTitle?: string;\n sectionDescription?: string;\n breakpoint: SettingsRailBreakpoint;\n linkComponent: NonNullable<SettingsLayoutProps['linkComponent']>;\n testIdPrefix: string;\n}): React.JSX.Element {\n if (sectionTitle === undefined) {\n return (\n <BackLink\n href={href}\n label={backLabel}\n breakpoint={breakpoint}\n linkComponent={linkComponent}\n testIdPrefix={testIdPrefix}\n />\n );\n }\n return (\n <SettingsSectionHeader\n href={href}\n backLabel={backLabel}\n title={sectionTitle}\n description={sectionDescription}\n breakpoint={breakpoint}\n linkComponent={linkComponent}\n testIdPrefix={testIdPrefix}\n />\n );\n}\n\n/**\n * The panel's own box, as ONE object per media query rather than two.\n *\n * Both the `display` rule and `fillHeight`'s belong at the same wide-shape\n * query, and spreading them as separate `{ [query]: … }` objects makes the\n * second REPLACE the first — so turning `fillHeight` on silently dropped the\n * `display: block` that `hideOnNarrow` puts there and left the panel\n * `display: none` at every width. That only shows at a drilldown INDEX, where\n * `hideOnNarrow` is true, which is why a section route looked fine.\n *\n * `overflowY` here is also what lets a `position: sticky` toolbar inside\n * `children` pin to the TOP OF THE PANEL — under the host's header — instead\n * of to a document that is not the scroller.\n */\nfunction panelSx(\n breakpoint: SettingsRailBreakpoint,\n hideOnNarrow: boolean,\n fillHeight: boolean,\n): (theme: Theme) => CSSObject {\n return (theme) => ({\n display: hideOnNarrow ? \"none\" : \"block\",\n flex: \"1 1 auto\",\n minWidth: 0,\n width: \"100%\",\n [atLeastRail(theme, breakpoint)]: {\n ...(hideOnNarrow ? { display: \"block\" } : {}),\n ...(fillHeight\n ? {\n height: \"100%\",\n minHeight: 0,\n overflowY: \"auto\",\n overscrollBehavior: \"contain\",\n }\n : {}),\n },\n });\n}\n\n/**\n * The central column: the open screen, and — in `drilldown`, inside a section —\n * the back link and sibling-section strip above it.\n *\n * At the index in `drilldown` this whole column is `display: none` below the\n * breakpoint and the rail becomes the page. Mounted either way: the panel and\n * the list are one tree with `display` between them, not two branches of a\n * render.\n */\nexport function SettingsPanel({\n inSection,\n hideOnNarrow,\n breakpoint,\n indexHref,\n backLabel,\n sectionTitle,\n sectionDescription,\n ariaLabel,\n sectionChips,\n activeItemId,\n linkComponent,\n onSelectItem,\n testIdPrefix,\n fillHeight = false,\n children,\n}: SettingsPanelProps): React.JSX.Element {\n const showBack =\n inSection && indexHref !== undefined && linkComponent !== undefined;\n const showChips =\n inSection && sectionChips !== undefined && sectionChips.length > 0;\n\n return (\n <Box\n data-testid={`${testIdPrefix}-panel`}\n sx={panelSx(breakpoint, hideOnNarrow, fillHeight)}\n >\n {showBack ? (\n <PanelBack\n href={indexHref}\n backLabel={backLabel}\n sectionTitle={sectionTitle}\n sectionDescription={sectionDescription}\n breakpoint={breakpoint}\n linkComponent={linkComponent}\n testIdPrefix={testIdPrefix}\n />\n ) : null}\n\n {showChips ? (\n <Box\n sx={(theme) => ({\n ...displayAcrossRail(theme, breakpoint, \"block\", \"none\"),\n mb: 1,\n minWidth: 0,\n })}\n >\n <SettingsSectionChips\n items={sectionChips}\n activeItemId={activeItemId}\n ariaLabel={ariaLabel}\n linkComponent={linkComponent}\n onSelectItem={onSelectItem}\n testIdPrefix={testIdPrefix}\n />\n </Box>\n ) : null}\n\n {children}\n </Box>\n );\n}\n","'use client';\n\nimport { alpha, Box, useTheme, type Theme } from '@mui/material';\nimport React, { useEffect, useRef, type RefObject } from 'react';\n\nimport { SettingsStatusMarker } from './SettingsStatusMarker';\nimport { TOUCH_TARGET } from './SettingsLayout.styles';\n\n/**\n * The chip's drawn height, against `TOUCH_TARGET`'s 44 for the tappable one.\n *\n * The strip sits between a section's header and its first field on the width\n * with the least room for either, so the pill is sized to its text rather than\n * to the thumb — and the thumb is served by the hit area instead.\n */\nconst CHIP_HEIGHT = 34;\nimport type { SettingsLayoutProps, SettingsNavItem } from './SettingsLayout.types';\n\nexport interface SettingsSectionChipsProps {\n /** The sibling sections to offer — usually the open item's own group. */\n items: SettingsNavItem[];\n /** Which one is open. */\n activeItemId?: string;\n /** Accessible name of the strip, in the host's language. */\n ariaLabel: string;\n linkComponent?: SettingsLayoutProps['linkComponent'];\n onSelectItem?: (id: string) => void;\n testIdPrefix: string;\n}\n\n/** True when the visitor asked the OS for less movement. Re-read per scroll. */\nfunction prefersReducedMotion(): boolean {\n if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return false;\n return window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n}\n\n/**\n * Keep the open section's chip in view, however the visitor got here.\n *\n * Keyed on `activeItemId` in an effect rather than done in a click handler:\n * arriving by the desktop rail, by a card, by a pasted link and by the browser's\n * back button all have to land the same way, and only one of those is a click on\n * a chip. Every re-render otherwise starts the strip at scroll 0, which parks\n * the active chip off the right edge on any list longer than a screen — the\n * visitor is told where they are by an element they cannot see.\n *\n * `itemsKey` rather than `items`: a host almost always rebuilds that array per\n * render (`groups.flatMap(...)`), and depending on it would re-run this on every\n * render and yank the strip back mid-drag.\n *\n * The key joins on NUL — as the ESCAPE `'\\u0000'`, which it must stay. A\n * literal NUL byte works identically at runtime and makes git classify this file\n * as BINARY: `git diff` prints \"Binary file not shown\" and the GitHub review UI\n * shows nothing at all. This file shipped that way once, and the cost was not\n * the byte — it was a whole change nobody could read.\n */\nfunction useCentreActiveChip(\n stripRef: RefObject<HTMLDivElement | null>,\n activeItemId: string | undefined,\n itemsKey: string,\n): void {\n useEffect(() => {\n const strip = stripRef.current;\n if (!strip || !activeItemId) return;\n // Walked rather than selected: an id is host data, and a section id carrying\n // a quote would turn a selector string into a thrown SyntaxError — which\n // here would take the whole strip down to centre one chip.\n const chip = Array.from(strip.children).find(\n (node): node is HTMLElement =>\n node instanceof HTMLElement && node.dataset.chipId === activeItemId,\n );\n if (!chip) return;\n // Feature-checked, not assumed. `Element.scrollTo` is absent in jsdom and in\n // a few embedded webviews, and an unguarded call there THROWS out of an\n // effect — which React escalates to the nearest error boundary. The whole\n // settings area would go down to centre one chip, and the host's own tests\n // could not render the shell at all. Same reasoning as walking the children\n // instead of building a selector: nothing in here is worth a crash.\n if (typeof strip.scrollTo !== 'function') return;\n const target = chip.offsetLeft + chip.offsetWidth / 2 - strip.clientWidth / 2;\n const left = Math.max(0, Math.min(target, strip.scrollWidth - strip.clientWidth));\n strip.scrollTo({ left, behavior: prefersReducedMotion() ? 'auto' : 'smooth' });\n }, [stripRef, activeItemId, itemsKey]);\n}\n\n/** One chip: a link when the host gave it a target, else a button. */\nfunction SectionChip({\n item,\n active,\n theme,\n linkComponent,\n onSelectItem,\n testIdPrefix,\n}: {\n item: SettingsNavItem;\n active: boolean;\n theme: Theme;\n linkComponent?: SettingsLayoutProps['linkComponent'];\n onSelectItem?: (id: string) => void;\n testIdPrefix: string;\n}): React.JSX.Element {\n const asLink = item.href !== undefined && linkComponent !== undefined;\n const behaviour = asLink\n ? { component: linkComponent, href: item.href }\n : { component: 'button' as const, type: 'button' as const, onClick: () => onSelectItem?.(item.id) };\n\n return (\n <Box\n {...behaviour}\n data-chip-id={item.id}\n data-testid={`${testIdPrefix}-chip-${item.id}`}\n aria-current={active ? 'page' : undefined}\n sx={{\n display: 'inline-flex',\n alignItems: 'center',\n gap: 0.75,\n flex: '0 0 auto',\n position: 'relative',\n // The chip LOOKS this tall; it is still tapped at `TOUCH_TARGET`.\n //\n // Shrinking `minHeight` alone would have bought a slimmer strip with a\n // smaller hit area — on the one width where the strip IS the navigation\n // and the input is a thumb. The `::after` below keeps the tappable\n // region at the full target while the pill itself takes less room, so\n // this is a visual change and not an accessibility trade.\n minHeight: CHIP_HEIGHT,\n px: 1.5,\n borderRadius: 999,\n cursor: 'pointer',\n font: 'inherit',\n fontSize: '0.8125rem',\n fontWeight: active ? 700 : 500,\n textDecoration: 'none',\n whiteSpace: 'nowrap',\n color: active ? 'primary.main' : 'text.secondary',\n bgcolor: active ? alpha(theme.palette.primary.main, 0.12) : 'transparent',\n border: `1px solid ${active ? alpha(theme.palette.primary.main, 0.4) : theme.palette.divider}`,\n // Spans the chip's width only, so neighbours never overlap: the strip\n // is a row, and the extra reach is vertical.\n '&::after': {\n content: '\"\"',\n position: 'absolute',\n insetInlineStart: 0,\n insetInlineEnd: 0,\n top: '50%',\n transform: 'translateY(-50%)',\n height: TOUCH_TARGET,\n },\n }}\n >\n {item.status ? (\n <SettingsStatusMarker\n status={item.status}\n label={item.statusLabel}\n testId={`${testIdPrefix}-chip-status-${item.id}`}\n />\n ) : null}\n {item.label}\n </Box>\n );\n}\n\n/**\n * The narrow-width section strip: horizontally scrollable chips for the sections\n * beside the open one.\n *\n * It clips its own overflow — `overflow-x: auto` on the strip, never on an\n * ancestor. A strip that pushes the page wide makes every OTHER screen at that\n * width scroll sideways too, and the cause is nowhere near the symptom.\n */\nexport function SettingsSectionChips({\n items,\n activeItemId,\n ariaLabel,\n linkComponent,\n onSelectItem,\n testIdPrefix,\n}: SettingsSectionChipsProps): React.JSX.Element {\n const theme = useTheme();\n const stripRef = useRef<HTMLDivElement | null>(null);\n useCentreActiveChip(stripRef, activeItemId, items.map((item) => item.id).join('\\u0000'));\n\n return (\n <Box\n ref={stripRef}\n component=\"nav\"\n aria-label={ariaLabel}\n data-testid={`${testIdPrefix}-chips`}\n sx={{\n display: 'flex',\n alignItems: 'center',\n gap: 0.75,\n px: 0.25,\n py: 0.25,\n overflowX: 'auto',\n overflowY: 'hidden',\n scrollbarWidth: 'none',\n '&::-webkit-scrollbar': { display: 'none' },\n }}\n >\n {items.map((item) => (\n <SectionChip\n key={item.id}\n item={item}\n active={item.id === activeItemId}\n theme={theme}\n linkComponent={linkComponent}\n onSelectItem={onSelectItem}\n testIdPrefix={testIdPrefix}\n />\n ))}\n </Box>\n );\n}\n\nSettingsSectionChips.displayName = 'SettingsSectionChips';\n","'use client';\n\nimport ChevronLeftIcon from '@mui/icons-material/ChevronLeft';\nimport { Box } from '@mui/material';\nimport React from 'react';\n\nimport { displayAcrossRail, TOUCH_TARGET } from './SettingsLayout.styles';\nimport type { SettingsLayoutProps, SettingsRailBreakpoint } from './SettingsLayout.types';\n\n/** One line that must not wrap, whatever the section is called. */\nconst CLIP = {\n overflow: 'hidden',\n textOverflow: 'ellipsis',\n whiteSpace: 'nowrap',\n} as const;\n\n/**\n * The name of where you are, and optionally what it holds.\n *\n * `minWidth: 0` on the wrapper is what lets the clipping work at all: without\n * it a flex item refuses to shrink below its content, so a long section name\n * pushes the back button off the row instead of ellipsising.\n */\nfunction HeaderText({\n title,\n description,\n}: {\n title: string;\n description?: string;\n}): React.JSX.Element {\n return (\n <Box sx={{ minWidth: 0 }}>\n <Box\n component=\"h2\"\n sx={{ m: 0, font: 'inherit', fontSize: '1rem', fontWeight: 600, lineHeight: 1.3, ...CLIP }}\n >\n {title}\n </Box>\n {description !== undefined && description !== '' && (\n <Box\n component=\"p\"\n sx={{\n m: 0,\n font: 'inherit',\n fontSize: '0.8125rem',\n color: 'text.secondary',\n lineHeight: 1.35,\n ...CLIP,\n }}\n >\n {description}\n </Box>\n )}\n </Box>\n );\n}\n\n/**\n * The narrow-width panel header: a back control, and the name of where you are.\n *\n * ## Why this replaced a \"‹ Voltar\" link\n *\n * The bare link spent a whole row saying only that a back existed. Under it came\n * the sibling chips and under those the section's own Salvar, so three stacked\n * rows went by before the first field — on the width with the least room for\n * them. Worse, none of the three said WHICH section was open: the chips mark the\n * active one, but a chip strip scrolls, so the answer could be off-screen.\n *\n * Folding the control into a square button beside the title buys back a row and\n * makes the header say where you are, which is what a header is for.\n *\n * ## Narrow only, and mounted at every width\n *\n * `displayAcrossRail` hides this above the breakpoint rather than unmounting it,\n * for the reason the whole component works that way: navigation that exists at\n * one width and not the other is navigation nothing can assert about. Above the\n * breakpoint the rail is on screen with the open row marked, so a back control\n * would point at a list already visible.\n *\n * The button is `TOUCH_TARGET` square because this is the phone shape, and it\n * keeps the accessible name — the visible text is the section, not the verb, so\n * a screen reader still hears \"Voltar\" rather than being handed an icon.\n */\nexport function SettingsSectionHeader({\n href,\n backLabel,\n title,\n description,\n breakpoint,\n linkComponent,\n testIdPrefix,\n}: {\n href: string;\n backLabel: string;\n title: string;\n description?: string;\n breakpoint: SettingsRailBreakpoint;\n linkComponent: NonNullable<SettingsLayoutProps['linkComponent']>;\n testIdPrefix: string;\n}): React.JSX.Element {\n return (\n <Box\n data-testid={`${testIdPrefix}-section-header`}\n sx={(theme) => ({\n ...displayAcrossRail(theme, breakpoint, 'flex', 'none'),\n alignItems: 'center',\n gap: 1.5,\n minWidth: 0,\n // ITS OWN SURFACE, not the panel's.\n //\n // The header, the chip strip and the first card were all the same\n // white, so nothing said where \"you are here\" ended and the screen\n // began — the eye had only the type sizes to go on. A tint plus the\n // rule below turns three stacked runs of text into two blocks.\n //\n // `paper` against a `default` panel rather than a hand-picked grey, so\n // it tracks the host's theme (and its dark mode) instead of being a\n // light-mode constant that goes wrong the moment the palette does.\n bgcolor: 'background.paper',\n px: 1.5,\n pt: 1.5,\n pb: 1.5,\n mb: 1,\n borderRadius: 1,\n // A rule under the header, because what follows it is the sibling strip\n // — a row of other sections. Without a line the section's own name and\n // the chip naming a DIFFERENT section sit in one undifferentiated\n // block, which reads as one navigation rather than as \"you are here\"\n // above \"go elsewhere\".\n borderBottom: 1,\n borderColor: 'divider',\n })}\n >\n <Box\n component={linkComponent}\n href={href}\n aria-label={backLabel}\n data-testid={`${testIdPrefix}-back`}\n sx={(theme) => ({\n flex: '0 0 auto',\n display: 'inline-flex',\n alignItems: 'center',\n justifyContent: 'center',\n width: TOUCH_TARGET,\n height: TOUCH_TARGET,\n border: 1,\n borderColor: 'divider',\n borderRadius: 2,\n color: 'text.primary',\n textDecoration: 'none',\n '&:hover': { backgroundColor: theme.palette.action.hover },\n })}\n >\n <ChevronLeftIcon fontSize=\"small\" />\n </Box>\n\n <HeaderText title={title} description={description} />\n </Box>\n );\n}\n"],"mappings":"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","names":["Box","Box","Typography","Box","jsx","jsxs","Box","jsx","jsxs","Box","Typography","ChevronLeftIcon","Box","alpha","Box","useTheme","jsx","jsxs","Box","alpha","useTheme","Box","jsx","jsxs","Box","jsx","jsxs","Box","ChevronLeftIcon","jsx","jsxs","Box"]}
@@ -1 +1 @@
1
- {"version":3,"file":"SettingsLayout.d.ts","sourceRoot":"","sources":["../../../../../src/components/layout/SettingsLayout/SettingsLayout.tsx"],"names":[],"mappings":"AAGA,OAAO,KAA4B,MAAM,OAAO,CAAC;AAOjD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAyDlE;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,eAAO,MAAM,cAAc,EAAE,KAAK,CAAC,EAAE,CAAC,mBAAmB,CA4ExD,CAAC"}
1
+ {"version":3,"file":"SettingsLayout.d.ts","sourceRoot":"","sources":["../../../../../src/components/layout/SettingsLayout/SettingsLayout.tsx"],"names":[],"mappings":"AAGA,OAAO,KAA4B,MAAM,OAAO,CAAC;AAOjD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAyDlE;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,eAAO,MAAM,cAAc,EAAE,KAAK,CAAC,EAAE,CAAC,mBAAmB,CA8ExD,CAAC"}
@@ -172,6 +172,22 @@ export interface SettingsLayoutProps {
172
172
  indexHref?: string;
173
173
  /** Label on the back control. Defaults to `"Back"`. */
174
174
  backLabel?: string;
175
+ /**
176
+ * The open section's own name.
177
+ *
178
+ * Supplying it turns the narrow-width back link into a compact header: a
179
+ * square back button beside the section's name and, optionally, one line
180
+ * about it. That buys back a row on the width with the least of them, and
181
+ * makes the header answer "where am I?" — which the chip strip cannot, since
182
+ * it scrolls and the marked chip can be off-screen.
183
+ *
184
+ * Optional so the plain text link stays the default: a header that grew a
185
+ * title and a description on upgrade would move every existing consumer's
186
+ * first field down the page without them asking.
187
+ */
188
+ sectionTitle?: string;
189
+ /** One line under {@link sectionTitle}. Ignored without it. */
190
+ sectionDescription?: string;
175
191
  /**
176
192
  * The sibling sections carried by the narrow-width chip strip — usually the
177
193
  * open item's own group.
@@ -1 +1 @@
1
- {"version":3,"file":"SettingsLayout.types.d.ts","sourceRoot":"","sources":["../../../../../src/components/layout/SettingsLayout/SettingsLayout.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEpD;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,iBAAiB,GAAG,IAAI,GAAG,KAAK,GAAG,KAAK,GAAG,QAAQ,CAAC;AAEhE;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,0FAA0F;IAC1F,EAAE,EAAE,MAAM,CAAC;IACX,0EAA0E;IAC1E,KAAK,EAAE,MAAM,CAAC;IACd,6BAA6B;IAC7B,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,uEAAuE;IACvE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,uEAAuE;IACvE,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,mEAAmE;AACnE,MAAM,WAAW,gBAAgB;IAC/B,sDAAsD;IACtD,EAAE,EAAE,MAAM,CAAC;IACX,sDAAsD;IACtD,KAAK,EAAE,MAAM,CAAC;IACd,4DAA4D;IAC5D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6CAA6C;IAC7C,KAAK,EAAE,eAAe,EAAE,CAAC;CAC1B;AAED;;;;;;;;;;GAUG;AACH,MAAM,MAAM,sBAAsB,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,MAAM,CAAC;AAExE;;;;;;;;;;GAUG;AACH,MAAM,MAAM,kBAAkB,GAAG,UAAU,GAAG,WAAW,CAAC;AAE1D,kFAAkF;AAClF,MAAM,WAAW,yBAAyB;IACxC,6CAA6C;IAC7C,KAAK,EAAE,SAAS,CAAC;IACjB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CACtB;AAED;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAClC,kEAAkE;IAClE,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,4CAA4C;IAC5C,MAAM,EAAE,gBAAgB,EAAE,CAAC;IAC3B,+DAA+D;IAC/D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,8DAA8D;IAC9D,YAAY,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,yEAAyE;IACzE,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;;;;;OAQG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,SAAS,CAAC;IAC7B;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,yBAAyB,CAAC;IAC9C;;;;OAIG;IACH,aAAa,CAAC,EAAE,WAAW,CAAC;IAC5B,qEAAqE;IACrE,QAAQ,EAAE,SAAS,CAAC;IACpB,qEAAqE;IACrE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,wEAAwE;IACxE;;;;;;;;;;;;;;;;OAgBG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB,cAAc,CAAC,EAAE,sBAAsB,CAAC;IACxC,+DAA+D;IAC/D,UAAU,CAAC,EAAE,kBAAkB,CAAC;IAChC;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,0DAA0D;IAC1D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uDAAuD;IACvD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;;;OAQG;IACH,YAAY,CAAC,EAAE,eAAe,EAAE,CAAC;CAClC"}
1
+ {"version":3,"file":"SettingsLayout.types.d.ts","sourceRoot":"","sources":["../../../../../src/components/layout/SettingsLayout/SettingsLayout.types.ts"],"names":[],"mappings":"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"}
@@ -8,6 +8,10 @@ export interface SettingsPanelProps {
8
8
  breakpoint: SettingsRailBreakpoint;
9
9
  indexHref?: string;
10
10
  backLabel: string;
11
+ /** The open section's own name — turns the back link into a compact header. */
12
+ sectionTitle?: string;
13
+ /** One line under that name. */
14
+ sectionDescription?: string;
11
15
  ariaLabel: string;
12
16
  sectionChips?: SettingsNavItem[];
13
17
  activeItemId?: string;
@@ -27,5 +31,5 @@ export interface SettingsPanelProps {
27
31
  * the list are one tree with `display` between them, not two branches of a
28
32
  * render.
29
33
  */
30
- export declare function SettingsPanel({ inSection, hideOnNarrow, breakpoint, indexHref, backLabel, ariaLabel, sectionChips, activeItemId, linkComponent, onSelectItem, testIdPrefix, fillHeight, children, }: SettingsPanelProps): React.JSX.Element;
34
+ export declare function SettingsPanel({ inSection, hideOnNarrow, breakpoint, indexHref, backLabel, sectionTitle, sectionDescription, ariaLabel, sectionChips, activeItemId, linkComponent, onSelectItem, testIdPrefix, fillHeight, children, }: SettingsPanelProps): React.JSX.Element;
31
35
  //# sourceMappingURL=SettingsPanel.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"SettingsPanel.d.ts","sourceRoot":"","sources":["../../../../../src/components/layout/SettingsLayout/SettingsPanel.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,MAAM,OAAO,CAAC;AAQ1B,OAAO,KAAK,EACV,mBAAmB,EACnB,eAAe,EACf,sBAAsB,EACvB,MAAM,wBAAwB,CAAC;AAEhC,MAAM,WAAW,kBAAkB;IACjC,sEAAsE;IACtE,SAAS,EAAE,OAAO,CAAC;IACnB,gDAAgD;IAChD,YAAY,EAAE,OAAO,CAAC;IACtB,UAAU,EAAE,sBAAsB,CAAC;IACnC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,eAAe,EAAE,CAAC;IACjC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,mBAAmB,CAAC,eAAe,CAAC,CAAC;IACrD,YAAY,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,2EAA2E;IAC3E,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AA8ED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,EAC5B,SAAS,EACT,YAAY,EACZ,UAAU,EACV,SAAS,EACT,SAAS,EACT,SAAS,EACT,YAAY,EACZ,YAAY,EACZ,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,UAAkB,EAClB,QAAQ,GACT,EAAE,kBAAkB,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CA2CxC"}
1
+ {"version":3,"file":"SettingsPanel.d.ts","sourceRoot":"","sources":["../../../../../src/components/layout/SettingsLayout/SettingsPanel.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,MAAM,OAAO,CAAC;AAS1B,OAAO,KAAK,EACV,mBAAmB,EACnB,eAAe,EACf,sBAAsB,EACvB,MAAM,wBAAwB,CAAC;AAEhC,MAAM,WAAW,kBAAkB;IACjC,sEAAsE;IACtE,SAAS,EAAE,OAAO,CAAC;IACnB,gDAAgD;IAChD,YAAY,EAAE,OAAO,CAAC;IACtB,UAAU,EAAE,sBAAsB,CAAC;IACnC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,+EAA+E;IAC/E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gCAAgC;IAChC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,eAAe,EAAE,CAAC;IACjC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,mBAAmB,CAAC,eAAe,CAAC,CAAC;IACrD,YAAY,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,2EAA2E;IAC3E,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAgID;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,EAC5B,SAAS,EACT,YAAY,EACZ,UAAU,EACV,SAAS,EACT,SAAS,EACT,YAAY,EACZ,kBAAkB,EAClB,SAAS,EACT,YAAY,EACZ,YAAY,EACZ,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,UAAkB,EAClB,QAAQ,GACT,EAAE,kBAAkB,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CA6CxC"}
@@ -1 +1 @@
1
- {"version":3,"file":"SettingsSectionChips.d.ts","sourceRoot":"","sources":["../../../../../src/components/layout/SettingsLayout/SettingsSectionChips.tsx"],"names":[],"mappings":"AAGA,OAAO,KAA4C,MAAM,OAAO,CAAC;AAIjE,OAAO,KAAK,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAEnF,MAAM,WAAW,yBAAyB;IACxC,yEAAyE;IACzE,KAAK,EAAE,eAAe,EAAE,CAAC;IACzB,yBAAyB;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,4DAA4D;IAC5D,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,mBAAmB,CAAC,eAAe,CAAC,CAAC;IACrD,YAAY,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,YAAY,EAAE,MAAM,CAAC;CACtB;AAmHD;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,EACnC,KAAK,EACL,YAAY,EACZ,SAAS,EACT,aAAa,EACb,YAAY,EACZ,YAAY,GACb,EAAE,yBAAyB,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CAoC/C;yBA3Ce,oBAAoB"}
1
+ {"version":3,"file":"SettingsSectionChips.d.ts","sourceRoot":"","sources":["../../../../../src/components/layout/SettingsLayout/SettingsSectionChips.tsx"],"names":[],"mappings":"AAGA,OAAO,KAA4C,MAAM,OAAO,CAAC;AAajE,OAAO,KAAK,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAEnF,MAAM,WAAW,yBAAyB;IACxC,yEAAyE;IACzE,KAAK,EAAE,eAAe,EAAE,CAAC;IACzB,yBAAyB;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,4DAA4D;IAC5D,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,mBAAmB,CAAC,eAAe,CAAC,CAAC;IACrD,YAAY,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,YAAY,EAAE,MAAM,CAAC;CACtB;AAsID;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,EACnC,KAAK,EACL,YAAY,EACZ,SAAS,EACT,aAAa,EACb,YAAY,EACZ,YAAY,GACb,EAAE,yBAAyB,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CAoC/C;yBA3Ce,oBAAoB"}
@@ -0,0 +1,38 @@
1
+ import React from 'react';
2
+ import type { SettingsLayoutProps, SettingsRailBreakpoint } from './SettingsLayout.types';
3
+ /**
4
+ * The narrow-width panel header: a back control, and the name of where you are.
5
+ *
6
+ * ## Why this replaced a "‹ Voltar" link
7
+ *
8
+ * The bare link spent a whole row saying only that a back existed. Under it came
9
+ * the sibling chips and under those the section's own Salvar, so three stacked
10
+ * rows went by before the first field — on the width with the least room for
11
+ * them. Worse, none of the three said WHICH section was open: the chips mark the
12
+ * active one, but a chip strip scrolls, so the answer could be off-screen.
13
+ *
14
+ * Folding the control into a square button beside the title buys back a row and
15
+ * makes the header say where you are, which is what a header is for.
16
+ *
17
+ * ## Narrow only, and mounted at every width
18
+ *
19
+ * `displayAcrossRail` hides this above the breakpoint rather than unmounting it,
20
+ * for the reason the whole component works that way: navigation that exists at
21
+ * one width and not the other is navigation nothing can assert about. Above the
22
+ * breakpoint the rail is on screen with the open row marked, so a back control
23
+ * would point at a list already visible.
24
+ *
25
+ * The button is `TOUCH_TARGET` square because this is the phone shape, and it
26
+ * keeps the accessible name — the visible text is the section, not the verb, so
27
+ * a screen reader still hears "Voltar" rather than being handed an icon.
28
+ */
29
+ export declare function SettingsSectionHeader({ href, backLabel, title, description, breakpoint, linkComponent, testIdPrefix, }: {
30
+ href: string;
31
+ backLabel: string;
32
+ title: string;
33
+ description?: string;
34
+ breakpoint: SettingsRailBreakpoint;
35
+ linkComponent: NonNullable<SettingsLayoutProps['linkComponent']>;
36
+ testIdPrefix: string;
37
+ }): React.JSX.Element;
38
+ //# sourceMappingURL=SettingsSectionHeader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SettingsSectionHeader.d.ts","sourceRoot":"","sources":["../../../../../src/components/layout/SettingsLayout/SettingsSectionHeader.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B,OAAO,KAAK,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AAkD1F;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,qBAAqB,CAAC,EACpC,IAAI,EACJ,SAAS,EACT,KAAK,EACL,WAAW,EACX,UAAU,EACV,aAAa,EACb,YAAY,GACb,EAAE;IACD,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,sBAAsB,CAAC;IACnC,aAAa,EAAE,WAAW,CAAC,mBAAmB,CAAC,eAAe,CAAC,CAAC,CAAC;IACjE,YAAY,EAAE,MAAM,CAAC;CACtB,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CA4DpB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@12-apps/ui",
3
- "version": "5.3.1",
3
+ "version": "5.5.0",
4
4
  "license": "MIT",
5
5
  "packageManager": "pnpm@9.0.0",
6
6
  "type": "module",
@@ -154,6 +154,8 @@ export const SettingsLayout: React.FC<SettingsLayoutProps> = (props) => {
154
154
  hideOnNarrow={drilldown && atIndex}
155
155
  breakpoint={railBreakpoint}
156
156
  indexHref={indexHref}
157
+ sectionTitle={props.sectionTitle}
158
+ sectionDescription={props.sectionDescription}
157
159
  backLabel={backLabel}
158
160
  ariaLabel={ariaLabel}
159
161
  sectionChips={sectionChips}
@@ -180,6 +180,22 @@ export interface SettingsLayoutProps {
180
180
  indexHref?: string;
181
181
  /** Label on the back control. Defaults to `"Back"`. */
182
182
  backLabel?: string;
183
+ /**
184
+ * The open section's own name.
185
+ *
186
+ * Supplying it turns the narrow-width back link into a compact header: a
187
+ * square back button beside the section's name and, optionally, one line
188
+ * about it. That buys back a row on the width with the least of them, and
189
+ * makes the header answer "where am I?" — which the chip strip cannot, since
190
+ * it scrolls and the marked chip can be off-screen.
191
+ *
192
+ * Optional so the plain text link stays the default: a header that grew a
193
+ * title and a description on upgrade would move every existing consumer's
194
+ * first field down the page without them asking.
195
+ */
196
+ sectionTitle?: string;
197
+ /** One line under {@link sectionTitle}. Ignored without it. */
198
+ sectionDescription?: string;
183
199
  /**
184
200
  * The sibling sections carried by the narrow-width chip strip — usually the
185
201
  * open item's own group.
@@ -11,6 +11,7 @@ import {
11
11
  TOUCH_TARGET,
12
12
  } from "./SettingsLayout.styles";
13
13
  import { SettingsSectionChips } from "./SettingsSectionChips";
14
+ import { SettingsSectionHeader } from "./SettingsSectionHeader";
14
15
  import type {
15
16
  SettingsLayoutProps,
16
17
  SettingsNavItem,
@@ -25,6 +26,10 @@ export interface SettingsPanelProps {
25
26
  breakpoint: SettingsRailBreakpoint;
26
27
  indexHref?: string;
27
28
  backLabel: string;
29
+ /** The open section's own name — turns the back link into a compact header. */
30
+ sectionTitle?: string;
31
+ /** One line under that name. */
32
+ sectionDescription?: string;
28
33
  ariaLabel: string;
29
34
  sectionChips?: SettingsNavItem[];
30
35
  activeItemId?: string;
@@ -74,6 +79,56 @@ function BackLink({
74
79
  );
75
80
  }
76
81
 
82
+ /**
83
+ * Two shapes of the same control, chosen by whether the host named the section.
84
+ *
85
+ * With a `sectionTitle` the back folds into a compact header that also says
86
+ * where you are; without one the original text link is kept, so no existing
87
+ * consumer's header grows a title and pushes their first field down the page on
88
+ * upgrade. Its own component so `SettingsPanel` keeps one branch rather than
89
+ * three.
90
+ */
91
+ function PanelBack({
92
+ href,
93
+ backLabel,
94
+ sectionTitle,
95
+ sectionDescription,
96
+ breakpoint,
97
+ linkComponent,
98
+ testIdPrefix,
99
+ }: {
100
+ href: string;
101
+ backLabel: string;
102
+ sectionTitle?: string;
103
+ sectionDescription?: string;
104
+ breakpoint: SettingsRailBreakpoint;
105
+ linkComponent: NonNullable<SettingsLayoutProps['linkComponent']>;
106
+ testIdPrefix: string;
107
+ }): React.JSX.Element {
108
+ if (sectionTitle === undefined) {
109
+ return (
110
+ <BackLink
111
+ href={href}
112
+ label={backLabel}
113
+ breakpoint={breakpoint}
114
+ linkComponent={linkComponent}
115
+ testIdPrefix={testIdPrefix}
116
+ />
117
+ );
118
+ }
119
+ return (
120
+ <SettingsSectionHeader
121
+ href={href}
122
+ backLabel={backLabel}
123
+ title={sectionTitle}
124
+ description={sectionDescription}
125
+ breakpoint={breakpoint}
126
+ linkComponent={linkComponent}
127
+ testIdPrefix={testIdPrefix}
128
+ />
129
+ );
130
+ }
131
+
77
132
  /**
78
133
  * The panel's own box, as ONE object per media query rather than two.
79
134
  *
@@ -127,6 +182,8 @@ export function SettingsPanel({
127
182
  breakpoint,
128
183
  indexHref,
129
184
  backLabel,
185
+ sectionTitle,
186
+ sectionDescription,
130
187
  ariaLabel,
131
188
  sectionChips,
132
189
  activeItemId,
@@ -147,9 +204,11 @@ export function SettingsPanel({
147
204
  sx={panelSx(breakpoint, hideOnNarrow, fillHeight)}
148
205
  >
149
206
  {showBack ? (
150
- <BackLink
207
+ <PanelBack
151
208
  href={indexHref}
152
- label={backLabel}
209
+ backLabel={backLabel}
210
+ sectionTitle={sectionTitle}
211
+ sectionDescription={sectionDescription}
153
212
  breakpoint={breakpoint}
154
213
  linkComponent={linkComponent}
155
214
  testIdPrefix={testIdPrefix}
@@ -5,6 +5,15 @@ import React, { useEffect, useRef, type RefObject } from 'react';
5
5
 
6
6
  import { SettingsStatusMarker } from './SettingsStatusMarker';
7
7
  import { TOUCH_TARGET } from './SettingsLayout.styles';
8
+
9
+ /**
10
+ * The chip's drawn height, against `TOUCH_TARGET`'s 44 for the tappable one.
11
+ *
12
+ * The strip sits between a section's header and its first field on the width
13
+ * with the least room for either, so the pill is sized to its text rather than
14
+ * to the thumb — and the thumb is served by the hit area instead.
15
+ */
16
+ const CHIP_HEIGHT = 34;
8
17
  import type { SettingsLayoutProps, SettingsNavItem } from './SettingsLayout.types';
9
18
 
10
19
  export interface SettingsSectionChipsProps {
@@ -106,7 +115,15 @@ function SectionChip({
106
115
  alignItems: 'center',
107
116
  gap: 0.75,
108
117
  flex: '0 0 auto',
109
- minHeight: TOUCH_TARGET,
118
+ position: 'relative',
119
+ // The chip LOOKS this tall; it is still tapped at `TOUCH_TARGET`.
120
+ //
121
+ // Shrinking `minHeight` alone would have bought a slimmer strip with a
122
+ // smaller hit area — on the one width where the strip IS the navigation
123
+ // and the input is a thumb. The `::after` below keeps the tappable
124
+ // region at the full target while the pill itself takes less room, so
125
+ // this is a visual change and not an accessibility trade.
126
+ minHeight: CHIP_HEIGHT,
110
127
  px: 1.5,
111
128
  borderRadius: 999,
112
129
  cursor: 'pointer',
@@ -118,6 +135,17 @@ function SectionChip({
118
135
  color: active ? 'primary.main' : 'text.secondary',
119
136
  bgcolor: active ? alpha(theme.palette.primary.main, 0.12) : 'transparent',
120
137
  border: `1px solid ${active ? alpha(theme.palette.primary.main, 0.4) : theme.palette.divider}`,
138
+ // Spans the chip's width only, so neighbours never overlap: the strip
139
+ // is a row, and the extra reach is vertical.
140
+ '&::after': {
141
+ content: '""',
142
+ position: 'absolute',
143
+ insetInlineStart: 0,
144
+ insetInlineEnd: 0,
145
+ top: '50%',
146
+ transform: 'translateY(-50%)',
147
+ height: TOUCH_TARGET,
148
+ },
121
149
  }}
122
150
  >
123
151
  {item.status ? (
@@ -163,7 +191,7 @@ export function SettingsSectionChips({
163
191
  alignItems: 'center',
164
192
  gap: 0.75,
165
193
  px: 0.25,
166
- py: 0.75,
194
+ py: 0.25,
167
195
  overflowX: 'auto',
168
196
  overflowY: 'hidden',
169
197
  scrollbarWidth: 'none',
@@ -0,0 +1,160 @@
1
+ 'use client';
2
+
3
+ import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
4
+ import { Box } from '@mui/material';
5
+ import React from 'react';
6
+
7
+ import { displayAcrossRail, TOUCH_TARGET } from './SettingsLayout.styles';
8
+ import type { SettingsLayoutProps, SettingsRailBreakpoint } from './SettingsLayout.types';
9
+
10
+ /** One line that must not wrap, whatever the section is called. */
11
+ const CLIP = {
12
+ overflow: 'hidden',
13
+ textOverflow: 'ellipsis',
14
+ whiteSpace: 'nowrap',
15
+ } as const;
16
+
17
+ /**
18
+ * The name of where you are, and optionally what it holds.
19
+ *
20
+ * `minWidth: 0` on the wrapper is what lets the clipping work at all: without
21
+ * it a flex item refuses to shrink below its content, so a long section name
22
+ * pushes the back button off the row instead of ellipsising.
23
+ */
24
+ function HeaderText({
25
+ title,
26
+ description,
27
+ }: {
28
+ title: string;
29
+ description?: string;
30
+ }): React.JSX.Element {
31
+ return (
32
+ <Box sx={{ minWidth: 0 }}>
33
+ <Box
34
+ component="h2"
35
+ sx={{ m: 0, font: 'inherit', fontSize: '1rem', fontWeight: 600, lineHeight: 1.3, ...CLIP }}
36
+ >
37
+ {title}
38
+ </Box>
39
+ {description !== undefined && description !== '' && (
40
+ <Box
41
+ component="p"
42
+ sx={{
43
+ m: 0,
44
+ font: 'inherit',
45
+ fontSize: '0.8125rem',
46
+ color: 'text.secondary',
47
+ lineHeight: 1.35,
48
+ ...CLIP,
49
+ }}
50
+ >
51
+ {description}
52
+ </Box>
53
+ )}
54
+ </Box>
55
+ );
56
+ }
57
+
58
+ /**
59
+ * The narrow-width panel header: a back control, and the name of where you are.
60
+ *
61
+ * ## Why this replaced a "‹ Voltar" link
62
+ *
63
+ * The bare link spent a whole row saying only that a back existed. Under it came
64
+ * the sibling chips and under those the section's own Salvar, so three stacked
65
+ * rows went by before the first field — on the width with the least room for
66
+ * them. Worse, none of the three said WHICH section was open: the chips mark the
67
+ * active one, but a chip strip scrolls, so the answer could be off-screen.
68
+ *
69
+ * Folding the control into a square button beside the title buys back a row and
70
+ * makes the header say where you are, which is what a header is for.
71
+ *
72
+ * ## Narrow only, and mounted at every width
73
+ *
74
+ * `displayAcrossRail` hides this above the breakpoint rather than unmounting it,
75
+ * for the reason the whole component works that way: navigation that exists at
76
+ * one width and not the other is navigation nothing can assert about. Above the
77
+ * breakpoint the rail is on screen with the open row marked, so a back control
78
+ * would point at a list already visible.
79
+ *
80
+ * The button is `TOUCH_TARGET` square because this is the phone shape, and it
81
+ * keeps the accessible name — the visible text is the section, not the verb, so
82
+ * a screen reader still hears "Voltar" rather than being handed an icon.
83
+ */
84
+ export function SettingsSectionHeader({
85
+ href,
86
+ backLabel,
87
+ title,
88
+ description,
89
+ breakpoint,
90
+ linkComponent,
91
+ testIdPrefix,
92
+ }: {
93
+ href: string;
94
+ backLabel: string;
95
+ title: string;
96
+ description?: string;
97
+ breakpoint: SettingsRailBreakpoint;
98
+ linkComponent: NonNullable<SettingsLayoutProps['linkComponent']>;
99
+ testIdPrefix: string;
100
+ }): React.JSX.Element {
101
+ return (
102
+ <Box
103
+ data-testid={`${testIdPrefix}-section-header`}
104
+ sx={(theme) => ({
105
+ ...displayAcrossRail(theme, breakpoint, 'flex', 'none'),
106
+ alignItems: 'center',
107
+ gap: 1.5,
108
+ minWidth: 0,
109
+ // ITS OWN SURFACE, not the panel's.
110
+ //
111
+ // The header, the chip strip and the first card were all the same
112
+ // white, so nothing said where "you are here" ended and the screen
113
+ // began — the eye had only the type sizes to go on. A tint plus the
114
+ // rule below turns three stacked runs of text into two blocks.
115
+ //
116
+ // `paper` against a `default` panel rather than a hand-picked grey, so
117
+ // it tracks the host's theme (and its dark mode) instead of being a
118
+ // light-mode constant that goes wrong the moment the palette does.
119
+ bgcolor: 'background.paper',
120
+ px: 1.5,
121
+ pt: 1.5,
122
+ pb: 1.5,
123
+ mb: 1,
124
+ borderRadius: 1,
125
+ // A rule under the header, because what follows it is the sibling strip
126
+ // — a row of other sections. Without a line the section's own name and
127
+ // the chip naming a DIFFERENT section sit in one undifferentiated
128
+ // block, which reads as one navigation rather than as "you are here"
129
+ // above "go elsewhere".
130
+ borderBottom: 1,
131
+ borderColor: 'divider',
132
+ })}
133
+ >
134
+ <Box
135
+ component={linkComponent}
136
+ href={href}
137
+ aria-label={backLabel}
138
+ data-testid={`${testIdPrefix}-back`}
139
+ sx={(theme) => ({
140
+ flex: '0 0 auto',
141
+ display: 'inline-flex',
142
+ alignItems: 'center',
143
+ justifyContent: 'center',
144
+ width: TOUCH_TARGET,
145
+ height: TOUCH_TARGET,
146
+ border: 1,
147
+ borderColor: 'divider',
148
+ borderRadius: 2,
149
+ color: 'text.primary',
150
+ textDecoration: 'none',
151
+ '&:hover': { backgroundColor: theme.palette.action.hover },
152
+ })}
153
+ >
154
+ <ChevronLeftIcon fontSize="small" />
155
+ </Box>
156
+
157
+ <HeaderText title={title} description={description} />
158
+ </Box>
159
+ );
160
+ }