@umodeapporg/ui 0.8.1 → 0.9.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.
@@ -12,7 +12,7 @@ export interface IDatePickerProps {
12
12
  startValue?: Date;
13
13
  onFocus?: () => void;
14
14
  onBlur?: () => void;
15
- /** Extra classes for the inner input. The default keeps the historical compact height (!py-0). */
15
+ /** Extra classes for the inner input, which is a `ControlledInput` like any other field. */
16
16
  inputClassName?: string;
17
17
  }
18
18
  declare const DatePicker: React.FC<IDatePickerProps>;
@@ -79,13 +79,13 @@ const DatePicker = ({
79
79
  {
80
80
  readOnly: true,
81
81
  placeholder: placeholder ?? label,
82
- className: twMerge("rounded-md !py-0 focus:border-border-strong", inputClassName),
82
+ className: twMerge("focus:border-border-strong", inputClassName),
83
83
  disabled,
84
84
  value: date ? formatDate(date) : "",
85
85
  onClick: togglePopover
86
86
  }
87
87
  ),
88
- date && /* @__PURE__ */ jsx(ControlledTooltip, { content: t("dates.clearSelection"), children: /* @__PURE__ */ jsx("div", { onClick: clear, className: "absolute inset-y-0 right-4 top-0.5 flex cursor-pointer items-center", children: /* @__PURE__ */ jsx(MaterialSymbol, { icon: "close_small", size: 20, color: "rgb(var(--color-foreground-subtle))" }) }) })
88
+ date && /* @__PURE__ */ jsx(ControlledTooltip, { content: t("dates.clearSelection"), children: /* @__PURE__ */ jsx("div", { onClick: clear, className: "absolute inset-y-0 right-4 flex cursor-pointer items-center", children: /* @__PURE__ */ jsx(MaterialSymbol, { icon: "close_small", size: 20, color: "rgb(var(--color-foreground-subtle))" }) }) })
89
89
  ] })
90
90
  ] }) }),
91
91
  /* @__PURE__ */ jsx(ControlledPopoverContent, { className: "zIndexMax m-0 p-0", children: /* @__PURE__ */ jsx(
@@ -1 +1 @@
1
- {"version":3,"file":"DatePicker.js","sources":["../../../src/components/DateTime/DatePicker.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from 'react';\nimport * as React from 'react';\nimport { ptBR } from 'date-fns/locale';\nimport { DateAfter, DateBefore, DayPicker } from 'react-day-picker';\nimport { useTranslation } from 'react-i18next';\nimport { twMerge } from 'tailwind-merge';\n\nimport {\n ControlledPopoverContent,\n ControlledPopoverHandler,\n ControlledPopoverRoot,\n} from '@/components/UI/ControlledPopover';\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { formatDate } from '@/helpers/dateHelpers';\n\nimport ControlledInput from '../Form/ControlledInput';\nimport { MaterialSymbol } from '../UI/MaterialSymbol';\n\nimport 'react-day-picker/style.css';\n\nconst DATE_PICKER_TIMEZONE = 'UTC';\n\nexport interface IDatePickerProps {\n label?: string;\n /** Text shown inside the empty input. Falls back to the label for the historical behavior. */\n placeholder?: string;\n onUpdate: (value: string | undefined) => void;\n disabled?: boolean;\n minDate?: string;\n maxDate?: string;\n useIcon?: boolean;\n valueRender?: () => React.ReactNode;\n startValue?: Date;\n onFocus?: () => void;\n onBlur?: () => void;\n /** Extra classes for the inner input. The default keeps the historical compact height (!py-0). */\n inputClassName?: string;\n}\n\nfunction formatDateToYYYYMMDD(date: Date) {\n const month = String(date.getUTCMonth() + 1).padStart(2, '0');\n const day = String(date.getUTCDate()).padStart(2, '0');\n const year = date.getUTCFullYear();\n return `${year}-${month}-${day}`;\n}\n\nfunction parseDate(dateString: string): Date {\n const dateParts = dateString.split('T')[0].split('-');\n const year = parseInt(dateParts[0], 10);\n const month = parseInt(dateParts[1], 10) - 1;\n const day = parseInt(dateParts[2], 10);\n\n const formattedDate = new Date(year, month, day);\n return formattedDate;\n}\n\nconst DatePicker: React.FC<IDatePickerProps> = ({\n label,\n placeholder,\n disabled,\n onUpdate,\n minDate,\n maxDate,\n useIcon,\n valueRender,\n startValue,\n onFocus,\n inputClassName,\n}) => {\n const { t } = useTranslation('ui');\n const [date, setDate] = useState<Date | undefined>(startValue || undefined);\n const [popoverOpen, setPopoverOpen] = useState(false);\n\n // Hosts pass a freshly parsed Date on every render, so the sync compares\n // timestamps. Without this the picker ignored external changes (clearing\n // filters, applying a preset) and callers had to force a remount via `key`.\n const startTime = startValue ? startValue.getTime() : null;\n const syncedStartTimeRef = useRef(startTime);\n\n useEffect(() => {\n if (syncedStartTimeRef.current === startTime) return;\n syncedStartTimeRef.current = startTime;\n setDate(startTime === null ? undefined : new Date(startTime));\n }, [startTime]);\n\n const handleSelectedDate = (date: Date | undefined) => {\n setDate(date);\n if (date) {\n const formattedDate = formatDateToYYYYMMDD(date);\n onUpdate(formattedDate);\n } else {\n onUpdate(undefined);\n }\n setPopoverOpen(false);\n };\n\n const togglePopover = () => {\n setPopoverOpen(!popoverOpen);\n };\n\n const clear = () => {\n handleSelectedDate(undefined);\n };\n\n const beforeMatcher: DateBefore = {\n before: minDate ? parseDate(String(minDate)) : new Date(2024, 1, 1),\n };\n const afterMatcher: DateAfter = {\n after: maxDate ? parseDate(String(maxDate)) : new Date(2030, 1, 1),\n };\n\n return (\n <ControlledPopoverRoot placement=\"bottom\" open={popoverOpen} handler={setPopoverOpen}>\n <ControlledPopoverHandler>\n <div className=\"zIndexMax flex w-full flex-row items-center gap-x-1\" onFocus={onFocus}>\n {useIcon && <MaterialSymbol icon=\"calendar_month\" size={20} />}\n {valueRender ? (\n valueRender()\n ) : (\n <div className=\"relative w-full\">\n <ControlledInput\n readOnly\n placeholder={placeholder ?? label}\n className={twMerge('rounded-md !py-0 focus:border-border-strong', inputClassName)}\n disabled={disabled}\n value={date ? formatDate(date) : ''}\n onClick={togglePopover}\n />\n\n {date && (\n <ControlledTooltip content={t('dates.clearSelection')}>\n <div onClick={clear} className=\"absolute inset-y-0 right-4 top-0.5 flex cursor-pointer items-center\">\n <MaterialSymbol icon=\"close_small\" size={20} color=\"rgb(var(--color-foreground-subtle))\" />\n </div>\n </ControlledTooltip>\n )}\n </div>\n )}\n </div>\n </ControlledPopoverHandler>\n <ControlledPopoverContent className=\"zIndexMax m-0 p-0\">\n <DayPicker\n ISOWeek\n showOutsideDays\n captionLayout=\"label\"\n dir=\"ltr\"\n firstWeekContainsDate={1}\n mode=\"single\"\n numberOfMonths={1}\n timeZone={DATE_PICKER_TIMEZONE}\n weekStartsOn={1}\n locale={ptBR}\n selected={date}\n defaultMonth={date}\n disabled={[beforeMatcher, afterMatcher]}\n onSelect={handleSelectedDate}\n className=\"p-3\"\n classNames={{\n today: `rounded-md bg-muted-active text-foreground`,\n selected: `rounded-md bg-ink text-white hover:bg-ink hover:text-white focus:bg-ink focus:text-white`,\n chevron: `fill-foreground`,\n }}\n />\n </ControlledPopoverContent>\n </ControlledPopoverRoot>\n );\n};\n\nexport default DatePicker;\n"],"names":["date"],"mappings":"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"}
1
+ {"version":3,"file":"DatePicker.js","sources":["../../../src/components/DateTime/DatePicker.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from 'react';\nimport * as React from 'react';\nimport { ptBR } from 'date-fns/locale';\nimport { DateAfter, DateBefore, DayPicker } from 'react-day-picker';\nimport { useTranslation } from 'react-i18next';\nimport { twMerge } from 'tailwind-merge';\n\nimport {\n ControlledPopoverContent,\n ControlledPopoverHandler,\n ControlledPopoverRoot,\n} from '@/components/UI/ControlledPopover';\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { formatDate } from '@/helpers/dateHelpers';\n\nimport ControlledInput from '../Form/ControlledInput';\nimport { MaterialSymbol } from '../UI/MaterialSymbol';\n\nimport 'react-day-picker/style.css';\n\nconst DATE_PICKER_TIMEZONE = 'UTC';\n\nexport interface IDatePickerProps {\n label?: string;\n /** Text shown inside the empty input. Falls back to the label for the historical behavior. */\n placeholder?: string;\n onUpdate: (value: string | undefined) => void;\n disabled?: boolean;\n minDate?: string;\n maxDate?: string;\n useIcon?: boolean;\n valueRender?: () => React.ReactNode;\n startValue?: Date;\n onFocus?: () => void;\n onBlur?: () => void;\n /** Extra classes for the inner input, which is a `ControlledInput` like any other field. */\n inputClassName?: string;\n}\n\nfunction formatDateToYYYYMMDD(date: Date) {\n const month = String(date.getUTCMonth() + 1).padStart(2, '0');\n const day = String(date.getUTCDate()).padStart(2, '0');\n const year = date.getUTCFullYear();\n return `${year}-${month}-${day}`;\n}\n\nfunction parseDate(dateString: string): Date {\n const dateParts = dateString.split('T')[0].split('-');\n const year = parseInt(dateParts[0], 10);\n const month = parseInt(dateParts[1], 10) - 1;\n const day = parseInt(dateParts[2], 10);\n\n const formattedDate = new Date(year, month, day);\n return formattedDate;\n}\n\nconst DatePicker: React.FC<IDatePickerProps> = ({\n label,\n placeholder,\n disabled,\n onUpdate,\n minDate,\n maxDate,\n useIcon,\n valueRender,\n startValue,\n onFocus,\n inputClassName,\n}) => {\n const { t } = useTranslation('ui');\n const [date, setDate] = useState<Date | undefined>(startValue || undefined);\n const [popoverOpen, setPopoverOpen] = useState(false);\n\n // Hosts pass a freshly parsed Date on every render, so the sync compares\n // timestamps. Without this the picker ignored external changes (clearing\n // filters, applying a preset) and callers had to force a remount via `key`.\n const startTime = startValue ? startValue.getTime() : null;\n const syncedStartTimeRef = useRef(startTime);\n\n useEffect(() => {\n if (syncedStartTimeRef.current === startTime) return;\n syncedStartTimeRef.current = startTime;\n setDate(startTime === null ? undefined : new Date(startTime));\n }, [startTime]);\n\n const handleSelectedDate = (date: Date | undefined) => {\n setDate(date);\n if (date) {\n const formattedDate = formatDateToYYYYMMDD(date);\n onUpdate(formattedDate);\n } else {\n onUpdate(undefined);\n }\n setPopoverOpen(false);\n };\n\n const togglePopover = () => {\n setPopoverOpen(!popoverOpen);\n };\n\n const clear = () => {\n handleSelectedDate(undefined);\n };\n\n const beforeMatcher: DateBefore = {\n before: minDate ? parseDate(String(minDate)) : new Date(2024, 1, 1),\n };\n const afterMatcher: DateAfter = {\n after: maxDate ? parseDate(String(maxDate)) : new Date(2030, 1, 1),\n };\n\n return (\n <ControlledPopoverRoot placement=\"bottom\" open={popoverOpen} handler={setPopoverOpen}>\n <ControlledPopoverHandler>\n <div className=\"zIndexMax flex w-full flex-row items-center gap-x-1\" onFocus={onFocus}>\n {useIcon && <MaterialSymbol icon=\"calendar_month\" size={20} />}\n {valueRender ? (\n valueRender()\n ) : (\n <div className=\"relative w-full\">\n <ControlledInput\n readOnly\n placeholder={placeholder ?? label}\n className={twMerge('focus:border-border-strong', inputClassName)}\n disabled={disabled}\n value={date ? formatDate(date) : ''}\n onClick={togglePopover}\n />\n\n {date && (\n <ControlledTooltip content={t('dates.clearSelection')}>\n <div onClick={clear} className=\"absolute inset-y-0 right-4 flex cursor-pointer items-center\">\n <MaterialSymbol icon=\"close_small\" size={20} color=\"rgb(var(--color-foreground-subtle))\" />\n </div>\n </ControlledTooltip>\n )}\n </div>\n )}\n </div>\n </ControlledPopoverHandler>\n <ControlledPopoverContent className=\"zIndexMax m-0 p-0\">\n <DayPicker\n ISOWeek\n showOutsideDays\n captionLayout=\"label\"\n dir=\"ltr\"\n firstWeekContainsDate={1}\n mode=\"single\"\n numberOfMonths={1}\n timeZone={DATE_PICKER_TIMEZONE}\n weekStartsOn={1}\n locale={ptBR}\n selected={date}\n defaultMonth={date}\n disabled={[beforeMatcher, afterMatcher]}\n onSelect={handleSelectedDate}\n className=\"p-3\"\n classNames={{\n today: `rounded-md bg-muted-active text-foreground`,\n selected: `rounded-md bg-ink text-white hover:bg-ink hover:text-white focus:bg-ink focus:text-white`,\n chevron: `fill-foreground`,\n }}\n />\n </ControlledPopoverContent>\n </ControlledPopoverRoot>\n );\n};\n\nexport default DatePicker;\n"],"names":["date"],"mappings":"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"}
@@ -1,10 +1,10 @@
1
- import { ReactNode } from 'react';
1
+ import { ReactNode, RefObject } from 'react';
2
2
  import { SymbolCodepoints } from '../UI/MaterialSymbol';
3
3
  export interface IPageTitleImage {
4
4
  src: string;
5
5
  /** The image REPLACES the icon chip, so it carries the accessible name of what it depicts. */
6
6
  alt: string;
7
- /** Escape hatch for the shape, e.g. `rounded-full` for an avatar. */
7
+ /** Escape hatch for shape and size, e.g. `rounded-full` for an avatar. Merged last, so it wins. */
8
8
  className?: string;
9
9
  }
10
10
  interface IPageTitleProps {
@@ -16,6 +16,12 @@ interface IPageTitleProps {
16
16
  /** Conteúdo abaixo da linha do título, dentro da coluna de texto (tags, steppers). */
17
17
  subtitleSlot?: ReactNode;
18
18
  slot?: ReactNode;
19
+ /**
20
+ * Where the `slot` sits against the text column. `center` (default) is right for a one-line title with
21
+ * a couple of buttons; `start` pins it to the TOP of the header, which is what a tall identification
22
+ * block needs so its actions line up with the eyebrow instead of floating at the middle.
23
+ */
24
+ slotAlign?: 'center' | 'start';
19
25
  /** Conteúdo renderizado à ESQUERDA, imediatamente ao lado do título (o `slot` fica à direita). */
20
26
  titleSlot?: ReactNode;
21
27
  /** Conteúdo renderizado ANTES do ícone/título (ex.: botão voltar customizado, alvo de drop). */
@@ -33,6 +39,29 @@ interface IPageTitleProps {
33
39
  * image in the header.
34
40
  */
35
41
  titleImage?: IPageTitleImage;
42
+ /**
43
+ * Size of the leading media (icon chip or `titleImage`). `md` is the 2.5rem chip every page uses;
44
+ * `lg` is the 5rem cover of a RECORD header, where the image identifies the record itself (a product
45
+ * datasheet). `compact` already implies `sm`, so pass this only to go bigger.
46
+ */
47
+ titleMediaSize?: 'sm' | 'md' | 'lg';
48
+ /**
49
+ * The header shrinks once the page under it has been scrolled: the breadcrumb, the eyebrow, the
50
+ * headline and the `subtitleSlot` collapse away, the media drops one size and the title takes its
51
+ * compact type, while the back button and the `slot` stay exactly where they were. The header also
52
+ * pins itself to the top of its scroll container, which is what makes shrinking it worth anything:
53
+ * a header that scrolls away does not need to be smaller.
54
+ *
55
+ * On by default. Turn it off for a header that must keep its full identity on screen, and for one
56
+ * that is not at the top of a scrolling page (inside a card, a dialog or a print sheet).
57
+ */
58
+ collapseOnScroll?: boolean;
59
+ /**
60
+ * The element whose scrolling drives the collapse. Only needed when the page's content scrolls in a
61
+ * container that is NOT an ancestor of the header, which is what a split layout does (a fixed header
62
+ * over an independently scrolling body). Left out, the header finds its own scrollable ancestor.
63
+ */
64
+ collapseScrollRef?: RefObject<HTMLElement | null>;
36
65
  /** Deriva o ícone automaticamente do menu quando `titleIcon` não é passado. */
37
66
  autoIcon?: boolean;
38
67
  /** Variante compacta: título menor, para headers de navegação interna (ex.: pastas). */
@@ -1,14 +1,35 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
- import { useMemo, useState } from "react";
2
+ import { useMemo, useState, useRef, useEffect } from "react";
3
3
  import classNames from "classnames";
4
4
  import { useTranslation } from "react-i18next";
5
+ import { twMerge } from "tailwind-merge";
5
6
  import ControlledButton from "../Form/ControlledButton.js";
7
+ import ControlledCollapse from "../UI/ControlledCollapse.js";
6
8
  import ControlledTooltip from "../UI/ControlledTooltip.js";
7
9
  import { capitalizeAllLetters } from "../../helpers/stringHelpers.js";
8
10
  import { MaterialSymbol } from "../UI/MaterialSymbol/index.js";
9
11
  import ControlledBreadcrumb from "./ControlledBreadcrumb.js";
12
+ import { PageTitleCollapseProvider } from "./PageTitleCollapseContext.js";
10
13
  import { usePageTitleNavigation } from "./PageTitleNavigationContext.js";
11
14
  import { useUiNavigation } from "./UiNavigationContext.js";
15
+ const MEDIA_BOX_CLASSNAME = {
16
+ sm: "size-8 rounded-lg",
17
+ md: "size-10 rounded-xl",
18
+ lg: "size-20 rounded-xl"
19
+ };
20
+ const MEDIA_ICON_SIZE = { sm: 18, md: 22, lg: 34 };
21
+ const COLLAPSED_MEDIA_SIZE = { sm: "sm", md: "sm", lg: "md" };
22
+ const COLLAPSE_AT_SCROLL = 48;
23
+ const EXPAND_AT_SCROLL = 16;
24
+ const findScrollParent = (node) => {
25
+ let current = node?.parentElement ?? null;
26
+ while (current) {
27
+ const { overflowY } = getComputedStyle(current);
28
+ if (overflowY === "auto" || overflowY === "scroll") return current;
29
+ current = current.parentElement;
30
+ }
31
+ return null;
32
+ };
12
33
  const PageTitle = ({
13
34
  canBack = false,
14
35
  canFloat = false,
@@ -19,6 +40,7 @@ const PageTitle = ({
19
40
  subtitleSlot,
20
41
  headline,
21
42
  slot,
43
+ slotAlign = "center",
22
44
  titleSlot,
23
45
  leadingSlot,
24
46
  breadcrumb,
@@ -26,6 +48,9 @@ const PageTitle = ({
26
48
  titleIcon,
27
49
  titleIconClassName,
28
50
  titleImage,
51
+ titleMediaSize,
52
+ collapseOnScroll = true,
53
+ collapseScrollRef,
29
54
  autoIcon = true,
30
55
  compact = false,
31
56
  capitalizeTitle = true,
@@ -67,91 +92,159 @@ const PageTitle = ({
67
92
  const resolvedTitleIcon = titleIcon ?? (autoIcon ? menuTrail[menuTrail.length - 1]?.icon : void 0);
68
93
  const [failedTitleImageSrc, setFailedTitleImageSrc] = useState(null);
69
94
  const rendersTitleImage = Boolean(titleImage?.src) && titleImage?.src !== failedTitleImageSrc;
70
- return /* @__PURE__ */ jsxs(
71
- "div",
72
- {
73
- className: classNames("flex flex-col", {
74
- "zIndexMax fixed top-16 mr-7": canFloat,
75
- className
76
- }),
77
- style: {
78
- width: canFloat ? isFirefox() ? "-moz-available" : "-webkit-fill-available" : "100%"
79
- },
80
- children: [
81
- resolvedBreadcrumb && /* @__PURE__ */ jsx("div", { className: "mb-2 shrink-0", children: resolvedBreadcrumb }),
82
- /* @__PURE__ */ jsxs(
83
- "div",
95
+ const sentinelRef = useRef(null);
96
+ const [isCollapsed, setIsCollapsed] = useState(false);
97
+ const collapses = collapseOnScroll && !canFloat;
98
+ useEffect(() => {
99
+ if (!collapses) {
100
+ setIsCollapsed(false);
101
+ return;
102
+ }
103
+ const siblingContainer = collapseScrollRef?.current;
104
+ if (siblingContainer) {
105
+ const handleScroll = () => {
106
+ const offset = siblingContainer.scrollTop;
107
+ setIsCollapsed((previous) => previous ? offset > EXPAND_AT_SCROLL : offset >= COLLAPSE_AT_SCROLL);
108
+ };
109
+ handleScroll();
110
+ siblingContainer.addEventListener("scroll", handleScroll, { passive: true });
111
+ return () => siblingContainer.removeEventListener("scroll", handleScroll);
112
+ }
113
+ const sentinel = sentinelRef.current;
114
+ if (!sentinel || typeof IntersectionObserver === "undefined") return;
115
+ const observer = new IntersectionObserver(([entry]) => setIsCollapsed(!entry.isIntersecting), {
116
+ root: findScrollParent(sentinel),
117
+ threshold: 0
118
+ });
119
+ observer.observe(sentinel);
120
+ return () => observer.disconnect();
121
+ }, [collapses, collapseScrollRef]);
122
+ const isCompactLayout = compact || isCollapsed;
123
+ const resolvedMediaSize = titleMediaSize ?? (compact ? "sm" : "md");
124
+ const mediaSize = isCollapsed ? COLLAPSED_MEDIA_SIZE[resolvedMediaSize] : resolvedMediaSize;
125
+ const mediaBoxClassName = MEDIA_BOX_CLASSNAME[mediaSize];
126
+ const mediaIconSize = MEDIA_ICON_SIZE[mediaSize];
127
+ const mediaTransitionClassName = "transition-[width,height] duration-base ease-enter";
128
+ const collapseValue = useMemo(() => ({ isCollapsed }), [isCollapsed]);
129
+ return /* @__PURE__ */ jsxs(PageTitleCollapseProvider, { value: collapseValue, children: [
130
+ collapses && /* @__PURE__ */ jsx("div", { ref: sentinelRef, "aria-hidden": true, className: "-mt-px h-px shrink-0" }),
131
+ /* @__PURE__ */ jsxs(
132
+ "div",
133
+ {
134
+ className: classNames(
135
+ "flex flex-col",
84
136
  {
85
- className: classNames("flex w-full flex-wrap items-center justify-between gap-x-4 gap-y-2", {
86
- "bg-surface py-5": canFloat
87
- }),
88
- children: [
89
- /* @__PURE__ */ jsxs("div", { className: classNames("flex min-w-0 flex-1 basis-60 items-center", compact ? "gap-2" : "gap-3"), children: [
90
- leadingSlot,
91
- canBack && /* @__PURE__ */ jsx(ControlledTooltip, { content: t("actions.back"), children: /* @__PURE__ */ jsx(
92
- ControlledButton,
93
- {
94
- defaultHtmlButton: true,
95
- onClick: handleBack,
96
- disabled: disableBack,
97
- className: classNames(
98
- "flex shrink-0 items-center justify-center rounded-full text-foreground-subtle transition-interactive duration-base ease-enter hover:bg-muted-hover hover:text-foreground-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-border active:scale-90 disabled:pointer-events-none disabled:text-foreground-faint",
99
- compact ? "size-8" : "size-9"
100
- ),
101
- children: /* @__PURE__ */ jsx(MaterialSymbol, { icon: "arrow_back", size: compact ? 18 : 20 })
102
- }
103
- ) }),
104
- rendersTitleImage && titleImage ? /* @__PURE__ */ jsx(
105
- "img",
106
- {
107
- src: titleImage.src,
108
- alt: titleImage.alt,
109
- onError: () => setFailedTitleImageSrc(titleImage.src),
110
- className: classNames(
111
- "shrink-0 border border-border object-cover",
112
- compact ? "size-8 rounded-lg" : "size-10 rounded-xl",
113
- titleImage.className
114
- )
115
- }
116
- ) : resolvedTitleIcon && /* @__PURE__ */ jsx(
117
- "span",
137
+ "zIndexMax fixed top-16 mr-7": canFloat,
138
+ // Pins under the app header, which publishes its own height; hosts without the shell
139
+ // stick at the top of their scroll container.
140
+ "sticky top-[var(--page-header-sticky-top,0px)] z-30": collapses,
141
+ "shadow-elevation1 glass-subtle": collapses && isCollapsed
142
+ },
143
+ className
144
+ ),
145
+ style: {
146
+ width: canFloat ? isFirefox() ? "-moz-available" : "-webkit-fill-available" : "100%"
147
+ },
148
+ children: [
149
+ resolvedBreadcrumb && /* @__PURE__ */ jsx(ControlledCollapse, { open: !isCollapsed, className: "shrink-0", children: /* @__PURE__ */ jsx("div", { className: "mb-2", children: resolvedBreadcrumb }) }),
150
+ /* @__PURE__ */ jsxs(
151
+ "div",
152
+ {
153
+ className: classNames("flex w-full flex-wrap justify-between gap-x-4 gap-y-2", {
154
+ "items-start": slotAlign === "start",
155
+ "items-center": slotAlign !== "start",
156
+ "bg-surface py-5": canFloat
157
+ }),
158
+ children: [
159
+ /* @__PURE__ */ jsxs("div", { className: classNames("flex min-w-0 flex-1 basis-60 items-center", isCompactLayout ? "gap-2" : "gap-3"), children: [
160
+ leadingSlot,
161
+ canBack && /* @__PURE__ */ jsx(ControlledTooltip, { content: t("actions.back"), children: /* @__PURE__ */ jsx(
162
+ ControlledButton,
163
+ {
164
+ defaultHtmlButton: true,
165
+ onClick: handleBack,
166
+ disabled: disableBack,
167
+ className: classNames(
168
+ "flex shrink-0 items-center justify-center rounded-full text-foreground-subtle transition-interactive duration-base ease-enter hover:bg-muted-hover hover:text-foreground-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-border active:scale-90 disabled:pointer-events-none disabled:text-foreground-faint",
169
+ isCompactLayout ? "size-8" : "size-9"
170
+ ),
171
+ children: /* @__PURE__ */ jsx(MaterialSymbol, { icon: "arrow_back", size: isCompactLayout ? 18 : 20 })
172
+ }
173
+ ) }),
174
+ rendersTitleImage && titleImage ? /* @__PURE__ */ jsx(
175
+ "img",
176
+ {
177
+ src: titleImage.src,
178
+ alt: titleImage.alt,
179
+ onError: () => setFailedTitleImageSrc(titleImage.src),
180
+ className: twMerge(
181
+ "shrink-0 border border-border object-cover",
182
+ mediaTransitionClassName,
183
+ mediaBoxClassName,
184
+ titleImage.className
185
+ )
186
+ }
187
+ ) : resolvedTitleIcon && /* @__PURE__ */ jsx(
188
+ "span",
189
+ {
190
+ className: twMerge(
191
+ "flex shrink-0 items-center justify-center",
192
+ mediaTransitionClassName,
193
+ mediaBoxClassName,
194
+ titleIconClassName ?? "bg-brand-soft text-brand-emphasis"
195
+ ),
196
+ children: /* @__PURE__ */ jsx(MaterialSymbol, { icon: resolvedTitleIcon, size: mediaIconSize, fill: true, className: "text-inherit" })
197
+ }
198
+ ),
199
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col", children: [
200
+ eyebrow && /* @__PURE__ */ jsx(ControlledCollapse, { open: !isCollapsed, children: /* @__PURE__ */ jsx("span", { className: "mb-0.5 block text-xs font-semibold uppercase tracking-widest text-foreground-faint", children: eyebrow }) }),
201
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [
202
+ titleContent ?? /* @__PURE__ */ jsx(
203
+ "span",
204
+ {
205
+ className: classNames(
206
+ "min-w-0 truncate",
207
+ isCompactLayout ? "text-sm font-semibold text-foreground" : "text-lg font-bold tracking-tight text-foreground laptop:text-xl",
208
+ titleClassName
209
+ ),
210
+ children: capitalizeTitle ? capitalizeAllLetters(title) : title
211
+ }
212
+ ),
213
+ titleSlot
214
+ ] }),
215
+ headline && /* @__PURE__ */ jsx(ControlledCollapse, { open: !isCollapsed, children: /* @__PURE__ */ jsx("span", { className: "mt-0.5 block text-xs text-foreground-subtle", children: headline }) }),
216
+ subtitleSlot && /* @__PURE__ */ jsx(ControlledCollapse, { open: !isCollapsed, children: /* @__PURE__ */ jsx("div", { className: "mt-1 flex min-w-0 flex-col gap-1", children: subtitleSlot }) })
217
+ ] })
218
+ ] }),
219
+ slot && /* @__PURE__ */ jsx(
220
+ "div",
118
221
  {
119
222
  className: classNames(
120
- "flex shrink-0 items-center justify-center",
121
- compact ? "size-8 rounded-lg" : "size-10 rounded-xl",
122
- titleIconClassName ?? "bg-brand-soft text-brand-emphasis"
223
+ "ml-auto flex shrink-0 flex-wrap justify-end gap-2",
224
+ slotAlign === "start" ? "items-start" : "items-center"
123
225
  ),
124
- children: /* @__PURE__ */ jsx(MaterialSymbol, { icon: resolvedTitleIcon, size: compact ? 18 : 22, fill: true, className: "text-inherit" })
226
+ children: slot
125
227
  }
126
- ),
127
- /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col", children: [
128
- eyebrow && /* @__PURE__ */ jsx("span", { className: "mb-0.5 text-xs font-semibold uppercase tracking-widest text-foreground-faint", children: eyebrow }),
129
- /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [
130
- titleContent ?? /* @__PURE__ */ jsx(
131
- "span",
132
- {
133
- className: classNames(
134
- "min-w-0 truncate",
135
- compact ? "text-sm font-semibold text-foreground" : "text-lg font-bold tracking-tight text-foreground laptop:text-xl",
136
- titleClassName
137
- ),
138
- children: capitalizeTitle ? capitalizeAllLetters(title) : title
139
- }
140
- ),
141
- titleSlot
142
- ] }),
143
- headline && /* @__PURE__ */ jsx("span", { className: "mt-0.5 text-xs text-foreground-subtle", children: headline }),
144
- subtitleSlot && /* @__PURE__ */ jsx("div", { className: "mt-1 flex min-w-0 flex-col gap-1", children: subtitleSlot })
145
- ] })
146
- ] }),
147
- slot && /* @__PURE__ */ jsx("div", { className: "ml-auto flex shrink-0 flex-wrap items-center justify-end gap-2", children: slot })
148
- ]
149
- }
150
- ),
151
- showDivider && !canFloat && (title.length > 0 || Boolean(titleContent)) && /* @__PURE__ */ jsx("hr", { className: classNames("border-border-subtle", compact ? "mb-2 mt-2" : "mb-2 mt-3") })
152
- ]
153
- }
154
- );
228
+ )
229
+ ]
230
+ }
231
+ ),
232
+ showDivider && !canFloat && (title.length > 0 || Boolean(titleContent)) && // Pinned, the shadow already separates the header from what slides under it, and a rule on
233
+ // top of it reads as a seam.
234
+ /* @__PURE__ */ jsx(
235
+ "hr",
236
+ {
237
+ className: classNames(
238
+ "border-border-subtle transition-opacity duration-base",
239
+ isCompactLayout ? "mb-2 mt-2" : "mb-2 mt-3",
240
+ isCollapsed && "opacity-0"
241
+ )
242
+ }
243
+ )
244
+ ]
245
+ }
246
+ )
247
+ ] });
155
248
  };
156
249
  export {
157
250
  PageTitle as default
@@ -1 +1 @@
1
- {"version":3,"file":"PageTitle.js","sources":["../../../src/components/Navigation/PageTitle.tsx"],"sourcesContent":["import { ReactNode, useMemo, useState } from 'react';\nimport classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\n\nimport ControlledButton from '@/components/Form/ControlledButton';\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { capitalizeAllLetters } from '@/helpers/stringHelpers';\n\nimport { MaterialSymbol, SymbolCodepoints } from '../UI/MaterialSymbol';\n\nimport ControlledBreadcrumb from './ControlledBreadcrumb';\nimport { usePageTitleNavigation } from './PageTitleNavigationContext';\nimport { useUiNavigation } from './UiNavigationContext';\n\nexport interface IPageTitleImage {\n src: string;\n /** The image REPLACES the icon chip, so it carries the accessible name of what it depicts. */\n alt: string;\n /** Escape hatch for the shape, e.g. `rounded-full` for an avatar. */\n className?: string;\n}\n\ninterface IPageTitleProps {\n title?: string;\n /** Substitui o span do título por conteúdo rico (ex.: nome editável inline da ficha técnica). */\n titleContent?: ReactNode;\n /** Rótulo pequeno em caixa alta acima do título (ex.: \"FICHA TÉCNICA\"). */\n eyebrow?: string;\n /** Conteúdo abaixo da linha do título, dentro da coluna de texto (tags, steppers). */\n subtitleSlot?: ReactNode;\n slot?: ReactNode;\n /** Conteúdo renderizado à ESQUERDA, imediatamente ao lado do título (o `slot` fica à direita). */\n titleSlot?: ReactNode;\n /** Conteúdo renderizado ANTES do ícone/título (ex.: botão voltar customizado, alvo de drop). */\n leadingSlot?: ReactNode;\n /** Trilha custom acima do título (use `ControlledBreadcrumb`). Sem ela, a trilha é derivada do menu. */\n breadcrumb?: ReactNode;\n /** Deriva a trilha automaticamente da árvore de navegação quando `breadcrumb` não é passado. */\n autoBreadcrumb?: boolean;\n /** Ícone em chip à esquerda do título. Sem ele, herda o ícone do item de menu da rota atual. */\n titleIcon?: SymbolCodepoints;\n titleIconClassName?: string;\n /**\n * An image in the icon chip's place, at the same size and radius (e.g. the product cover on a\n * datasheet header). A src that fails to load falls back to the icon instead of leaving a broken\n * image in the header.\n */\n titleImage?: IPageTitleImage;\n /** Deriva o ícone automaticamente do menu quando `titleIcon` não é passado. */\n autoIcon?: boolean;\n /** Variante compacta: título menor, para headers de navegação interna (ex.: pastas). */\n compact?: boolean;\n /** Desligue para títulos digitados pelo usuário (nomes de pastas etc.), que não podem ter a caixa alterada. */\n capitalizeTitle?: boolean;\n /** Desligue quando o header é seguido imediatamente por tabs ou outra divisão própria. */\n showDivider?: boolean;\n canBack?: boolean;\n canBackTo?: string;\n canFloat?: boolean;\n headline?: string;\n className?: string;\n titleClassName?: string;\n onBackClick?: () => void;\n disableBack?: boolean;\n}\n\nconst PageTitle: React.FC<IPageTitleProps> = ({\n canBack = false,\n canFloat = false,\n canBackTo,\n title = '',\n titleContent,\n eyebrow,\n subtitleSlot,\n headline,\n slot,\n titleSlot,\n leadingSlot,\n breadcrumb,\n autoBreadcrumb = true,\n titleIcon,\n titleIconClassName,\n titleImage,\n autoIcon = true,\n compact = false,\n capitalizeTitle = true,\n showDivider = true,\n className,\n titleClassName,\n onBackClick,\n disableBack = false,\n}) => {\n const { t } = useTranslation('ui');\n const { navigate, goBack, location } = useUiNavigation();\n\n const isFirefox = () => typeof navigator !== 'undefined' && navigator.userAgent.toLowerCase().includes('firefox');\n\n const handleBack = () => {\n if (disableBack) return;\n\n if (onBackClick) {\n onBackClick();\n } else if (canBackTo) {\n navigate(canBackTo);\n } else {\n goBack();\n }\n };\n\n const { resolveTrail } = usePageTitleNavigation();\n\n const menuTrail = useMemo(() => {\n const needsTrail = (autoBreadcrumb && !breadcrumb && !canFloat) || (autoIcon && !titleIcon && !titleImage);\n return needsTrail ? resolveTrail(location.pathname) : [];\n }, [autoBreadcrumb, breadcrumb, canFloat, autoIcon, titleIcon, titleImage, location.pathname, resolveTrail]);\n\n const resolvedBreadcrumb =\n breadcrumb ??\n (autoBreadcrumb && !canFloat && menuTrail.length >= 2 ? (\n <ControlledBreadcrumb\n ariaLabel={t('pageTitle.breadcrumbLabel')}\n items={menuTrail.map((item, index) => ({\n key: item.key,\n label: item.label,\n to: index < menuTrail.length - 1 ? item.to : undefined,\n }))}\n />\n ) : undefined);\n\n const resolvedTitleIcon = titleIcon ?? (autoIcon ? menuTrail[menuTrail.length - 1]?.icon : undefined);\n\n // Holds the src that failed rather than a boolean, so pointing at another image retries on its own.\n const [failedTitleImageSrc, setFailedTitleImageSrc] = useState<string | null>(null);\n const rendersTitleImage = Boolean(titleImage?.src) && titleImage?.src !== failedTitleImageSrc;\n\n return (\n <div\n className={classNames('flex flex-col', {\n 'zIndexMax fixed top-16 mr-7': canFloat,\n className,\n })}\n style={{\n width: canFloat ? (isFirefox() ? '-moz-available' : '-webkit-fill-available') : '100%',\n }}\n >\n {resolvedBreadcrumb && <div className=\"mb-2 shrink-0\">{resolvedBreadcrumb}</div>}\n\n <div\n className={classNames('flex w-full flex-wrap items-center justify-between gap-x-4 gap-y-2', {\n 'bg-surface py-5': canFloat,\n })}\n >\n <div className={classNames('flex min-w-0 flex-1 basis-60 items-center', compact ? 'gap-2' : 'gap-3')}>\n {leadingSlot}\n\n {canBack && (\n <ControlledTooltip content={t('actions.back')}>\n <ControlledButton\n defaultHtmlButton\n onClick={handleBack}\n disabled={disableBack}\n className={classNames(\n 'flex shrink-0 items-center justify-center rounded-full text-foreground-subtle transition-interactive duration-base ease-enter hover:bg-muted-hover hover:text-foreground-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-border active:scale-90 disabled:pointer-events-none disabled:text-foreground-faint',\n compact ? 'size-8' : 'size-9',\n )}\n >\n <MaterialSymbol icon=\"arrow_back\" size={compact ? 18 : 20} />\n </ControlledButton>\n </ControlledTooltip>\n )}\n\n {rendersTitleImage && titleImage ? (\n <img\n src={titleImage.src}\n alt={titleImage.alt}\n onError={() => setFailedTitleImageSrc(titleImage.src)}\n className={classNames(\n 'shrink-0 border border-border object-cover',\n compact ? 'size-8 rounded-lg' : 'size-10 rounded-xl',\n titleImage.className,\n )}\n />\n ) : (\n resolvedTitleIcon && (\n <span\n className={classNames(\n 'flex shrink-0 items-center justify-center',\n compact ? 'size-8 rounded-lg' : 'size-10 rounded-xl',\n titleIconClassName ?? 'bg-brand-soft text-brand-emphasis',\n )}\n >\n <MaterialSymbol icon={resolvedTitleIcon} size={compact ? 18 : 22} fill className=\"text-inherit\" />\n </span>\n )\n )}\n\n <div className=\"flex min-w-0 flex-1 flex-col\">\n {eyebrow && (\n <span className=\"mb-0.5 text-xs font-semibold uppercase tracking-widest text-foreground-faint\">\n {eyebrow}\n </span>\n )}\n\n <div className=\"flex min-w-0 items-center gap-2\">\n {titleContent ?? (\n <span\n className={classNames(\n 'min-w-0 truncate',\n compact\n ? 'text-sm font-semibold text-foreground'\n : 'text-lg font-bold tracking-tight text-foreground laptop:text-xl',\n titleClassName,\n )}\n >\n {capitalizeTitle ? capitalizeAllLetters(title) : title}\n </span>\n )}\n\n {titleSlot}\n </div>\n\n {headline && <span className=\"mt-0.5 text-xs text-foreground-subtle\">{headline}</span>}\n\n {subtitleSlot && <div className=\"mt-1 flex min-w-0 flex-col gap-1\">{subtitleSlot}</div>}\n </div>\n </div>\n\n {slot && <div className=\"ml-auto flex shrink-0 flex-wrap items-center justify-end gap-2\">{slot}</div>}\n </div>\n {showDivider && !canFloat && (title.length > 0 || Boolean(titleContent)) && (\n <hr className={classNames('border-border-subtle', compact ? 'mb-2 mt-2' : 'mb-2 mt-3')} />\n )}\n </div>\n );\n};\n\nexport default PageTitle;\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"PageTitle.js","sources":["../../../src/components/Navigation/PageTitle.tsx"],"sourcesContent":["import { ReactNode, RefObject, useEffect, useMemo, useRef, useState } from 'react';\nimport classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\nimport { twMerge } from 'tailwind-merge';\n\nimport ControlledButton from '@/components/Form/ControlledButton';\nimport ControlledCollapse from '@/components/UI/ControlledCollapse';\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { capitalizeAllLetters } from '@/helpers/stringHelpers';\n\nimport { MaterialSymbol, SymbolCodepoints } from '../UI/MaterialSymbol';\n\nimport ControlledBreadcrumb from './ControlledBreadcrumb';\nimport { PageTitleCollapseProvider } from './PageTitleCollapseContext';\nimport { usePageTitleNavigation } from './PageTitleNavigationContext';\nimport { useUiNavigation } from './UiNavigationContext';\n\nexport interface IPageTitleImage {\n src: string;\n /** The image REPLACES the icon chip, so it carries the accessible name of what it depicts. */\n alt: string;\n /** Escape hatch for shape and size, e.g. `rounded-full` for an avatar. Merged last, so it wins. */\n className?: string;\n}\n\nconst MEDIA_BOX_CLASSNAME: Record<'sm' | 'md' | 'lg', string> = {\n sm: 'size-8 rounded-lg',\n md: 'size-10 rounded-xl',\n lg: 'size-20 rounded-xl',\n};\n\nconst MEDIA_ICON_SIZE: Record<'sm' | 'md' | 'lg', number> = { sm: 18, md: 22, lg: 34 };\n\n/** One step down, so a record cover stays recognisable in the collapsed header instead of vanishing. */\nconst COLLAPSED_MEDIA_SIZE: Record<'sm' | 'md' | 'lg', 'sm' | 'md' | 'lg'> = { sm: 'sm', md: 'sm', lg: 'md' };\n\n/**\n * Thresholds for the sibling-container case, in px. They differ on purpose: collapsing and expanding\n * at the same position makes the header flicker for a whole gesture when a scroll lands on it.\n */\nconst COLLAPSE_AT_SCROLL = 48;\nconst EXPAND_AT_SCROLL = 16;\n\nconst findScrollParent = (node: HTMLElement | null): HTMLElement | null => {\n let current = node?.parentElement ?? null;\n while (current) {\n const { overflowY } = getComputedStyle(current);\n if (overflowY === 'auto' || overflowY === 'scroll') return current;\n current = current.parentElement;\n }\n return null;\n};\n\ninterface IPageTitleProps {\n title?: string;\n /** Substitui o span do título por conteúdo rico (ex.: nome editável inline da ficha técnica). */\n titleContent?: ReactNode;\n /** Rótulo pequeno em caixa alta acima do título (ex.: \"FICHA TÉCNICA\"). */\n eyebrow?: string;\n /** Conteúdo abaixo da linha do título, dentro da coluna de texto (tags, steppers). */\n subtitleSlot?: ReactNode;\n slot?: ReactNode;\n /**\n * Where the `slot` sits against the text column. `center` (default) is right for a one-line title with\n * a couple of buttons; `start` pins it to the TOP of the header, which is what a tall identification\n * block needs so its actions line up with the eyebrow instead of floating at the middle.\n */\n slotAlign?: 'center' | 'start';\n /** Conteúdo renderizado à ESQUERDA, imediatamente ao lado do título (o `slot` fica à direita). */\n titleSlot?: ReactNode;\n /** Conteúdo renderizado ANTES do ícone/título (ex.: botão voltar customizado, alvo de drop). */\n leadingSlot?: ReactNode;\n /** Trilha custom acima do título (use `ControlledBreadcrumb`). Sem ela, a trilha é derivada do menu. */\n breadcrumb?: ReactNode;\n /** Deriva a trilha automaticamente da árvore de navegação quando `breadcrumb` não é passado. */\n autoBreadcrumb?: boolean;\n /** Ícone em chip à esquerda do título. Sem ele, herda o ícone do item de menu da rota atual. */\n titleIcon?: SymbolCodepoints;\n titleIconClassName?: string;\n /**\n * An image in the icon chip's place, at the same size and radius (e.g. the product cover on a\n * datasheet header). A src that fails to load falls back to the icon instead of leaving a broken\n * image in the header.\n */\n titleImage?: IPageTitleImage;\n /**\n * Size of the leading media (icon chip or `titleImage`). `md` is the 2.5rem chip every page uses;\n * `lg` is the 5rem cover of a RECORD header, where the image identifies the record itself (a product\n * datasheet). `compact` already implies `sm`, so pass this only to go bigger.\n */\n titleMediaSize?: 'sm' | 'md' | 'lg';\n /**\n * The header shrinks once the page under it has been scrolled: the breadcrumb, the eyebrow, the\n * headline and the `subtitleSlot` collapse away, the media drops one size and the title takes its\n * compact type, while the back button and the `slot` stay exactly where they were. The header also\n * pins itself to the top of its scroll container, which is what makes shrinking it worth anything:\n * a header that scrolls away does not need to be smaller.\n *\n * On by default. Turn it off for a header that must keep its full identity on screen, and for one\n * that is not at the top of a scrolling page (inside a card, a dialog or a print sheet).\n */\n collapseOnScroll?: boolean;\n /**\n * The element whose scrolling drives the collapse. Only needed when the page's content scrolls in a\n * container that is NOT an ancestor of the header, which is what a split layout does (a fixed header\n * over an independently scrolling body). Left out, the header finds its own scrollable ancestor.\n */\n collapseScrollRef?: RefObject<HTMLElement | null>;\n /** Deriva o ícone automaticamente do menu quando `titleIcon` não é passado. */\n autoIcon?: boolean;\n /** Variante compacta: título menor, para headers de navegação interna (ex.: pastas). */\n compact?: boolean;\n /** Desligue para títulos digitados pelo usuário (nomes de pastas etc.), que não podem ter a caixa alterada. */\n capitalizeTitle?: boolean;\n /** Desligue quando o header é seguido imediatamente por tabs ou outra divisão própria. */\n showDivider?: boolean;\n canBack?: boolean;\n canBackTo?: string;\n canFloat?: boolean;\n headline?: string;\n className?: string;\n titleClassName?: string;\n onBackClick?: () => void;\n disableBack?: boolean;\n}\n\nconst PageTitle: React.FC<IPageTitleProps> = ({\n canBack = false,\n canFloat = false,\n canBackTo,\n title = '',\n titleContent,\n eyebrow,\n subtitleSlot,\n headline,\n slot,\n slotAlign = 'center',\n titleSlot,\n leadingSlot,\n breadcrumb,\n autoBreadcrumb = true,\n titleIcon,\n titleIconClassName,\n titleImage,\n titleMediaSize,\n collapseOnScroll = true,\n collapseScrollRef,\n autoIcon = true,\n compact = false,\n capitalizeTitle = true,\n showDivider = true,\n className,\n titleClassName,\n onBackClick,\n disableBack = false,\n}) => {\n const { t } = useTranslation('ui');\n const { navigate, goBack, location } = useUiNavigation();\n\n const isFirefox = () => typeof navigator !== 'undefined' && navigator.userAgent.toLowerCase().includes('firefox');\n\n const handleBack = () => {\n if (disableBack) return;\n\n if (onBackClick) {\n onBackClick();\n } else if (canBackTo) {\n navigate(canBackTo);\n } else {\n goBack();\n }\n };\n\n const { resolveTrail } = usePageTitleNavigation();\n\n const menuTrail = useMemo(() => {\n const needsTrail = (autoBreadcrumb && !breadcrumb && !canFloat) || (autoIcon && !titleIcon && !titleImage);\n return needsTrail ? resolveTrail(location.pathname) : [];\n }, [autoBreadcrumb, breadcrumb, canFloat, autoIcon, titleIcon, titleImage, location.pathname, resolveTrail]);\n\n const resolvedBreadcrumb =\n breadcrumb ??\n (autoBreadcrumb && !canFloat && menuTrail.length >= 2 ? (\n <ControlledBreadcrumb\n ariaLabel={t('pageTitle.breadcrumbLabel')}\n items={menuTrail.map((item, index) => ({\n key: item.key,\n label: item.label,\n to: index < menuTrail.length - 1 ? item.to : undefined,\n }))}\n />\n ) : undefined);\n\n const resolvedTitleIcon = titleIcon ?? (autoIcon ? menuTrail[menuTrail.length - 1]?.icon : undefined);\n\n // Holds the src that failed rather than a boolean, so pointing at another image retries on its own.\n const [failedTitleImageSrc, setFailedTitleImageSrc] = useState<string | null>(null);\n const rendersTitleImage = Boolean(titleImage?.src) && titleImage?.src !== failedTitleImageSrc;\n\n const sentinelRef = useRef<HTMLDivElement>(null);\n const [isCollapsed, setIsCollapsed] = useState(false);\n // A floating header is already pinned by its own mechanism, and collapsing it would fight it.\n const collapses = collapseOnScroll && !canFloat;\n\n useEffect(() => {\n if (!collapses) {\n setIsCollapsed(false);\n return;\n }\n\n // Sibling container: the header does not move with the content, so its own position says\n // nothing and the scroll offset is the only signal.\n const siblingContainer = collapseScrollRef?.current;\n if (siblingContainer) {\n const handleScroll = () => {\n const offset = siblingContainer.scrollTop;\n setIsCollapsed((previous) => (previous ? offset > EXPAND_AT_SCROLL : offset >= COLLAPSE_AT_SCROLL));\n };\n handleScroll();\n siblingContainer.addEventListener('scroll', handleScroll, { passive: true });\n return () => siblingContainer.removeEventListener('scroll', handleScroll);\n }\n\n // Ancestor container: the sentinel sits ABOVE the sticky header, in the normal flow, so it\n // leaves the scrollport exactly when the header pins itself. Watching the header instead would\n // never fire, since a pinned element stays in view by definition.\n const sentinel = sentinelRef.current;\n if (!sentinel || typeof IntersectionObserver === 'undefined') return;\n\n const observer = new IntersectionObserver(([entry]) => setIsCollapsed(!entry.isIntersecting), {\n root: findScrollParent(sentinel),\n threshold: 0,\n });\n observer.observe(sentinel);\n return () => observer.disconnect();\n }, [collapses, collapseScrollRef]);\n\n const isCompactLayout = compact || isCollapsed;\n const resolvedMediaSize = titleMediaSize ?? (compact ? 'sm' : 'md');\n const mediaSize = isCollapsed ? COLLAPSED_MEDIA_SIZE[resolvedMediaSize] : resolvedMediaSize;\n const mediaBoxClassName = MEDIA_BOX_CLASSNAME[mediaSize];\n const mediaIconSize = MEDIA_ICON_SIZE[mediaSize];\n const mediaTransitionClassName = 'transition-[width,height] duration-base ease-enter';\n const collapseValue = useMemo(() => ({ isCollapsed }), [isCollapsed]);\n\n return (\n <PageTitleCollapseProvider value={collapseValue}>\n {/* Sits in the normal flow, right above the pinned header, so it is the thing that scrolls\n out of view when the header pins itself. Zero height: it must not add rhythm. */}\n {collapses && <div ref={sentinelRef} aria-hidden className=\"-mt-px h-px shrink-0\" />}\n\n <div\n className={classNames(\n 'flex flex-col',\n {\n 'zIndexMax fixed top-16 mr-7': canFloat,\n // Pins under the app header, which publishes its own height; hosts without the shell\n // stick at the top of their scroll container.\n 'sticky top-[var(--page-header-sticky-top,0px)] z-30': collapses,\n 'shadow-elevation1 glass-subtle': collapses && isCollapsed,\n },\n className,\n )}\n style={{\n width: canFloat ? (isFirefox() ? '-moz-available' : '-webkit-fill-available') : '100%',\n }}\n >\n {resolvedBreadcrumb && (\n <ControlledCollapse open={!isCollapsed} className=\"shrink-0\">\n <div className=\"mb-2\">{resolvedBreadcrumb}</div>\n </ControlledCollapse>\n )}\n\n <div\n className={classNames('flex w-full flex-wrap justify-between gap-x-4 gap-y-2', {\n 'items-start': slotAlign === 'start',\n 'items-center': slotAlign !== 'start',\n 'bg-surface py-5': canFloat,\n })}\n >\n <div className={classNames('flex min-w-0 flex-1 basis-60 items-center', isCompactLayout ? 'gap-2' : 'gap-3')}>\n {leadingSlot}\n\n {canBack && (\n <ControlledTooltip content={t('actions.back')}>\n <ControlledButton\n defaultHtmlButton\n onClick={handleBack}\n disabled={disableBack}\n className={classNames(\n 'flex shrink-0 items-center justify-center rounded-full text-foreground-subtle transition-interactive duration-base ease-enter hover:bg-muted-hover hover:text-foreground-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-border active:scale-90 disabled:pointer-events-none disabled:text-foreground-faint',\n isCompactLayout ? 'size-8' : 'size-9',\n )}\n >\n <MaterialSymbol icon=\"arrow_back\" size={isCompactLayout ? 18 : 20} />\n </ControlledButton>\n </ControlledTooltip>\n )}\n\n {rendersTitleImage && titleImage ? (\n <img\n src={titleImage.src}\n alt={titleImage.alt}\n onError={() => setFailedTitleImageSrc(titleImage.src)}\n className={twMerge(\n 'shrink-0 border border-border object-cover',\n mediaTransitionClassName,\n mediaBoxClassName,\n titleImage.className,\n )}\n />\n ) : (\n resolvedTitleIcon && (\n <span\n className={twMerge(\n 'flex shrink-0 items-center justify-center',\n mediaTransitionClassName,\n mediaBoxClassName,\n titleIconClassName ?? 'bg-brand-soft text-brand-emphasis',\n )}\n >\n <MaterialSymbol icon={resolvedTitleIcon} size={mediaIconSize} fill className=\"text-inherit\" />\n </span>\n )\n )}\n\n <div className=\"flex min-w-0 flex-1 flex-col\">\n {eyebrow && (\n <ControlledCollapse open={!isCollapsed}>\n <span className=\"mb-0.5 block text-xs font-semibold uppercase tracking-widest text-foreground-faint\">\n {eyebrow}\n </span>\n </ControlledCollapse>\n )}\n\n <div className=\"flex min-w-0 items-center gap-2\">\n {titleContent ?? (\n <span\n className={classNames(\n 'min-w-0 truncate',\n isCompactLayout\n ? 'text-sm font-semibold text-foreground'\n : 'text-lg font-bold tracking-tight text-foreground laptop:text-xl',\n titleClassName,\n )}\n >\n {capitalizeTitle ? capitalizeAllLetters(title) : title}\n </span>\n )}\n\n {titleSlot}\n </div>\n\n {headline && (\n <ControlledCollapse open={!isCollapsed}>\n <span className=\"mt-0.5 block text-xs text-foreground-subtle\">{headline}</span>\n </ControlledCollapse>\n )}\n\n {subtitleSlot && (\n <ControlledCollapse open={!isCollapsed}>\n <div className=\"mt-1 flex min-w-0 flex-col gap-1\">{subtitleSlot}</div>\n </ControlledCollapse>\n )}\n </div>\n </div>\n\n {slot && (\n <div\n className={classNames(\n 'ml-auto flex shrink-0 flex-wrap justify-end gap-2',\n slotAlign === 'start' ? 'items-start' : 'items-center',\n )}\n >\n {slot}\n </div>\n )}\n </div>\n {showDivider && !canFloat && (title.length > 0 || Boolean(titleContent)) && (\n // Pinned, the shadow already separates the header from what slides under it, and a rule on\n // top of it reads as a seam.\n <hr\n className={classNames(\n 'border-border-subtle transition-opacity duration-base',\n isCompactLayout ? 'mb-2 mt-2' : 'mb-2 mt-3',\n isCollapsed && 'opacity-0',\n )}\n />\n )}\n </div>\n </PageTitleCollapseProvider>\n );\n};\n\nexport default PageTitle;\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ interface IPageTitleCollapseValue {
2
+ /** `true` once the page under the header has been scrolled and the header shrank. */
3
+ isCollapsed: boolean;
4
+ }
5
+ export declare const PageTitleCollapseProvider: import('react').Provider<IPageTitleCollapseValue>;
6
+ /**
7
+ * Reads the collapsed state of the PageTitle a node is rendered inside. It exists for the rich
8
+ * `titleContent` of a record header, which PageTitle cannot restyle on its own: the header owns
9
+ * that markup, so it is the one that decides how its title behaves when the header shrinks.
10
+ * Outside a PageTitle it answers `false`, so a component can be shared by both surfaces.
11
+ */
12
+ export declare const usePageTitleCollapsed: () => boolean;
13
+ export {};
@@ -0,0 +1,9 @@
1
+ import { createContext, useContext } from "react";
2
+ const PageTitleCollapseContext = createContext({ isCollapsed: false });
3
+ const PageTitleCollapseProvider = PageTitleCollapseContext.Provider;
4
+ const usePageTitleCollapsed = () => useContext(PageTitleCollapseContext).isCollapsed;
5
+ export {
6
+ PageTitleCollapseProvider,
7
+ usePageTitleCollapsed
8
+ };
9
+ //# sourceMappingURL=PageTitleCollapseContext.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PageTitleCollapseContext.js","sources":["../../../src/components/Navigation/PageTitleCollapseContext.tsx"],"sourcesContent":["import { createContext, useContext } from 'react';\n\ninterface IPageTitleCollapseValue {\n /** `true` once the page under the header has been scrolled and the header shrank. */\n isCollapsed: boolean;\n}\n\nconst PageTitleCollapseContext = createContext<IPageTitleCollapseValue>({ isCollapsed: false });\n\nexport const PageTitleCollapseProvider = PageTitleCollapseContext.Provider;\n\n/**\n * Reads the collapsed state of the PageTitle a node is rendered inside. It exists for the rich\n * `titleContent` of a record header, which PageTitle cannot restyle on its own: the header owns\n * that markup, so it is the one that decides how its title behaves when the header shrinks.\n * Outside a PageTitle it answers `false`, so a component can be shared by both surfaces.\n */\nexport const usePageTitleCollapsed = (): boolean => useContext(PageTitleCollapseContext).isCollapsed;\n"],"names":[],"mappings":";AAOA,MAAM,2BAA2B,cAAuC,EAAE,aAAa,OAAO;AAEvF,MAAM,4BAA4B,yBAAyB;AAQ3D,MAAM,wBAAwB,MAAe,WAAW,wBAAwB,EAAE;"}
@@ -85,7 +85,7 @@ const AppShell = ({
85
85
  id: "scrollableDiv",
86
86
  ref: scrollRef,
87
87
  className: "z-30 h-screen w-full px-4 pt-24 tablet:px-6",
88
- style: { overflow: "auto" },
88
+ style: { overflow: "auto", ["--page-header-sticky-top"]: "4rem" },
89
89
  children
90
90
  }
91
91
  )
@@ -1 +1 @@
1
- {"version":3,"file":"AppShell.js","sources":["../../../src/components/Shell/AppShell.tsx"],"sourcesContent":["import { ReactNode, useEffect, useRef } from 'react';\nimport classNames from 'classnames';\n\nimport { useUiNavigation } from '@/components/Navigation/UiNavigationContext';\n\nimport AppHeader from './AppHeader';\nimport AppSidebar from './AppSidebar';\nimport { ISidebarMenuItem } from './SidebarMenu';\nimport { useSidebarState } from './useSidebarState';\n\nimport '../../styles/sidebar.css';\n\ninterface IAppShellProps {\n items: Array<ISidebarMenuItem>;\n homePath: string;\n logo?: ReactNode;\n toolsSlot?: ReactNode;\n controlsSlot?: ReactNode;\n userMenuSlot?: ReactNode;\n /** Sidebar footer sign-out row; hidden when absent. */\n onLogout?: () => void;\n children: ReactNode;\n}\n\n/**\n * The uMode application chrome: fixed glass header, rail/dock/hover sidebar (pin persisted\n * through uiPreferences, Ctrl+\\ shortcut) and the scroll container every page renders into.\n * Everything app-shaped (menu items, tools, account surface, logout) arrives as props/slots.\n */\nconst AppShell: React.FC<IAppShellProps> = ({\n items,\n homePath,\n logo,\n toolsSlot,\n controlsSlot,\n userMenuSlot,\n onLogout,\n children,\n}) => {\n const { location } = useUiNavigation();\n const scrollRef = useRef<HTMLDivElement>(null);\n\n const {\n isMobile,\n isVisible,\n isDocked,\n isOverlayOnMobile,\n showRail,\n toggleLabelKey,\n togglePrimary,\n onHoverEnter,\n onHoverLeave,\n closeMobileDrawer,\n } = useSidebarState();\n\n useEffect(() => {\n scrollRef.current?.scrollTo({ top: 0, behavior: 'smooth' });\n }, [location.pathname]);\n\n return (\n <div className=\"h-screen w-full bg-surface\">\n <AppHeader\n toggleSidebar={togglePrimary}\n isSidebarOpen={isDocked}\n toggleLabelKey={toggleLabelKey}\n homePath={homePath}\n logo={logo}\n toolsSlot={toolsSlot}\n controlsSlot={controlsSlot}\n userMenuSlot={userMenuSlot}\n onHoverEnter={onHoverEnter}\n onHoverLeave={onHoverLeave}\n />\n\n <main\n className={classNames('sidebar-content flex h-screen w-full', {\n 'is-rail': !isMobile && !isDocked,\n 'is-mobile': isMobile,\n })}\n >\n {isOverlayOnMobile && (\n <div\n aria-hidden\n className=\"fixed inset-0 z-40 bg-scrim motion-reduce:transition-none\"\n onClick={closeMobileDrawer}\n />\n )}\n\n <AppSidebar\n items={items}\n isMobile={isMobile}\n isVisible={isVisible}\n collapsed={showRail}\n isFloating={!isMobile && !showRail && !isDocked}\n onHoverEnter={onHoverEnter}\n onHoverLeave={onHoverLeave}\n onNavigate={isMobile ? closeMobileDrawer : undefined}\n onLogout={onLogout}\n />\n\n <div\n id=\"scrollableDiv\"\n ref={scrollRef}\n className=\"z-30 h-screen w-full px-4 pt-24 tablet:px-6\"\n style={{ overflow: 'auto' }}\n >\n {children}\n </div>\n </main>\n </div>\n );\n};\n\nexport default AppShell;\n"],"names":[],"mappings":";;;;;;;;AA6BA,MAAM,WAAqC,CAAC;AAAA,EAC1C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MAAM;AACJ,QAAM,EAAE,SAAA,IAAa,gBAAA;AACrB,QAAM,YAAY,OAAuB,IAAI;AAE7C,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA,IACE,gBAAA;AAEJ,YAAU,MAAM;AACd,cAAU,SAAS,SAAS,EAAE,KAAK,GAAG,UAAU,UAAU;AAAA,EAC5D,GAAG,CAAC,SAAS,QAAQ,CAAC;AAEtB,SACE,qBAAC,OAAA,EAAI,WAAU,8BACb,UAAA;AAAA,IAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,eAAe;AAAA,QACf,eAAe;AAAA,QACf;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MAAA;AAAA,IAAA;AAAA,IAGF;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,WAAW,WAAW,wCAAwC;AAAA,UAC5D,WAAW,CAAC,YAAY,CAAC;AAAA,UACzB,aAAa;AAAA,QAAA,CACd;AAAA,QAEA,UAAA;AAAA,UAAA,qBACC;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,eAAW;AAAA,cACX,WAAU;AAAA,cACV,SAAS;AAAA,YAAA;AAAA,UAAA;AAAA,UAIb;AAAA,YAAC;AAAA,YAAA;AAAA,cACC;AAAA,cACA;AAAA,cACA;AAAA,cACA,WAAW;AAAA,cACX,YAAY,CAAC,YAAY,CAAC,YAAY,CAAC;AAAA,cACvC;AAAA,cACA;AAAA,cACA,YAAY,WAAW,oBAAoB;AAAA,cAC3C;AAAA,YAAA;AAAA,UAAA;AAAA,UAGF;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,IAAG;AAAA,cACH,KAAK;AAAA,cACL,WAAU;AAAA,cACV,OAAO,EAAE,UAAU,OAAA;AAAA,cAElB;AAAA,YAAA;AAAA,UAAA;AAAA,QACH;AAAA,MAAA;AAAA,IAAA;AAAA,EACF,GACF;AAEJ;"}
1
+ {"version":3,"file":"AppShell.js","sources":["../../../src/components/Shell/AppShell.tsx"],"sourcesContent":["import { ReactNode, useEffect, useRef } from 'react';\nimport classNames from 'classnames';\n\nimport { useUiNavigation } from '@/components/Navigation/UiNavigationContext';\n\nimport AppHeader from './AppHeader';\nimport AppSidebar from './AppSidebar';\nimport { ISidebarMenuItem } from './SidebarMenu';\nimport { useSidebarState } from './useSidebarState';\n\nimport '../../styles/sidebar.css';\n\ninterface IAppShellProps {\n items: Array<ISidebarMenuItem>;\n homePath: string;\n logo?: ReactNode;\n toolsSlot?: ReactNode;\n controlsSlot?: ReactNode;\n userMenuSlot?: ReactNode;\n /** Sidebar footer sign-out row; hidden when absent. */\n onLogout?: () => void;\n children: ReactNode;\n}\n\n/**\n * The uMode application chrome: fixed glass header, rail/dock/hover sidebar (pin persisted\n * through uiPreferences, Ctrl+\\ shortcut) and the scroll container every page renders into.\n * Everything app-shaped (menu items, tools, account surface, logout) arrives as props/slots.\n */\nconst AppShell: React.FC<IAppShellProps> = ({\n items,\n homePath,\n logo,\n toolsSlot,\n controlsSlot,\n userMenuSlot,\n onLogout,\n children,\n}) => {\n const { location } = useUiNavigation();\n const scrollRef = useRef<HTMLDivElement>(null);\n\n const {\n isMobile,\n isVisible,\n isDocked,\n isOverlayOnMobile,\n showRail,\n toggleLabelKey,\n togglePrimary,\n onHoverEnter,\n onHoverLeave,\n closeMobileDrawer,\n } = useSidebarState();\n\n useEffect(() => {\n scrollRef.current?.scrollTo({ top: 0, behavior: 'smooth' });\n }, [location.pathname]);\n\n return (\n <div className=\"h-screen w-full bg-surface\">\n <AppHeader\n toggleSidebar={togglePrimary}\n isSidebarOpen={isDocked}\n toggleLabelKey={toggleLabelKey}\n homePath={homePath}\n logo={logo}\n toolsSlot={toolsSlot}\n controlsSlot={controlsSlot}\n userMenuSlot={userMenuSlot}\n onHoverEnter={onHoverEnter}\n onHoverLeave={onHoverLeave}\n />\n\n <main\n className={classNames('sidebar-content flex h-screen w-full', {\n 'is-rail': !isMobile && !isDocked,\n 'is-mobile': isMobile,\n })}\n >\n {isOverlayOnMobile && (\n <div\n aria-hidden\n className=\"fixed inset-0 z-40 bg-scrim motion-reduce:transition-none\"\n onClick={closeMobileDrawer}\n />\n )}\n\n <AppSidebar\n items={items}\n isMobile={isMobile}\n isVisible={isVisible}\n collapsed={showRail}\n isFloating={!isMobile && !showRail && !isDocked}\n onHoverEnter={onHoverEnter}\n onHoverLeave={onHoverLeave}\n onNavigate={isMobile ? closeMobileDrawer : undefined}\n onLogout={onLogout}\n />\n\n <div\n id=\"scrollableDiv\"\n ref={scrollRef}\n className=\"z-30 h-screen w-full px-4 pt-24 tablet:px-6\"\n // `--page-header-sticky-top` is where a PageTitle pins itself when the page scrolls: the\n // height of the fixed app header, so the two never overlap. Declared here because the shell\n // owns that height; a host without the shell gets the fallback of 0.\n style={{ overflow: 'auto', ['--page-header-sticky-top' as string]: '4rem' }}\n >\n {children}\n </div>\n </main>\n </div>\n );\n};\n\nexport default AppShell;\n"],"names":[],"mappings":";;;;;;;;AA6BA,MAAM,WAAqC,CAAC;AAAA,EAC1C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MAAM;AACJ,QAAM,EAAE,SAAA,IAAa,gBAAA;AACrB,QAAM,YAAY,OAAuB,IAAI;AAE7C,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA,IACE,gBAAA;AAEJ,YAAU,MAAM;AACd,cAAU,SAAS,SAAS,EAAE,KAAK,GAAG,UAAU,UAAU;AAAA,EAC5D,GAAG,CAAC,SAAS,QAAQ,CAAC;AAEtB,SACE,qBAAC,OAAA,EAAI,WAAU,8BACb,UAAA;AAAA,IAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,eAAe;AAAA,QACf,eAAe;AAAA,QACf;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MAAA;AAAA,IAAA;AAAA,IAGF;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,WAAW,WAAW,wCAAwC;AAAA,UAC5D,WAAW,CAAC,YAAY,CAAC;AAAA,UACzB,aAAa;AAAA,QAAA,CACd;AAAA,QAEA,UAAA;AAAA,UAAA,qBACC;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,eAAW;AAAA,cACX,WAAU;AAAA,cACV,SAAS;AAAA,YAAA;AAAA,UAAA;AAAA,UAIb;AAAA,YAAC;AAAA,YAAA;AAAA,cACC;AAAA,cACA;AAAA,cACA;AAAA,cACA,WAAW;AAAA,cACX,YAAY,CAAC,YAAY,CAAC,YAAY,CAAC;AAAA,cACvC;AAAA,cACA;AAAA,cACA,YAAY,WAAW,oBAAoB;AAAA,cAC3C;AAAA,YAAA;AAAA,UAAA;AAAA,UAGF;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,IAAG;AAAA,cACH,KAAK;AAAA,cACL,WAAU;AAAA,cAIV,OAAO,EAAE,UAAU,QAAQ,CAAC,0BAAoC,GAAG,OAAA;AAAA,cAElE;AAAA,YAAA;AAAA,UAAA;AAAA,QACH;AAAA,MAAA;AAAA,IAAA;AAAA,EACF,GACF;AAEJ;"}
@@ -8,6 +8,11 @@ interface IControlledCollapseProps {
8
8
  * Height is measured from the content node and written to the wrapper, so the transition
9
9
  * always has a numeric start and end. The observer watches the CONTENT, whose height is
10
10
  * unconstrained, so updating the wrapper cannot feed back into it.
11
+ *
12
+ * A closed collapse is `inert`: `height: 0` with `overflow: hidden` still leaves whatever is
13
+ * inside reachable by Tab, so without it a keyboard lands on controls nobody can see, and
14
+ * `aria-hidden` around focusable content is itself an ARIA violation. Set through the DOM
15
+ * because React 18 does not type the attribute.
11
16
  */
12
17
  declare const ControlledCollapse: React.FC<IControlledCollapseProps>;
13
18
  export default ControlledCollapse;
@@ -2,8 +2,15 @@ import { jsx } from "react/jsx-runtime";
2
2
  import { useRef, useState, useEffect } from "react";
3
3
  import { twMerge } from "tailwind-merge";
4
4
  const ControlledCollapse = ({ open, children, className }) => {
5
+ const wrapperRef = useRef(null);
5
6
  const contentRef = useRef(null);
6
7
  const [height, setHeight] = useState(0);
8
+ useEffect(() => {
9
+ const wrapper = wrapperRef.current;
10
+ if (!wrapper) return;
11
+ if (open) wrapper.removeAttribute("inert");
12
+ else wrapper.setAttribute("inert", "");
13
+ }, [open]);
7
14
  useEffect(() => {
8
15
  const content = contentRef.current;
9
16
  if (!content) return void 0;
@@ -19,6 +26,7 @@ const ControlledCollapse = ({ open, children, className }) => {
19
26
  return /* @__PURE__ */ jsx(
20
27
  "div",
21
28
  {
29
+ ref: wrapperRef,
22
30
  style: { height },
23
31
  "aria-hidden": !open,
24
32
  className: twMerge("overflow-hidden transition-[height] duration-slow ease-enter", className),
@@ -1 +1 @@
1
- {"version":3,"file":"ControlledCollapse.js","sources":["../../../src/components/UI/ControlledCollapse.tsx"],"sourcesContent":["import React, { useEffect, useRef, useState } from 'react';\nimport { twMerge } from 'tailwind-merge';\n\ninterface IControlledCollapseProps {\n open: boolean;\n children: React.ReactNode;\n className?: string;\n}\n\n/**\n * Height is measured from the content node and written to the wrapper, so the transition\n * always has a numeric start and end. The observer watches the CONTENT, whose height is\n * unconstrained, so updating the wrapper cannot feed back into it.\n */\nconst ControlledCollapse: React.FC<IControlledCollapseProps> = ({ open, children, className }) => {\n const contentRef = useRef<HTMLDivElement>(null);\n const [height, setHeight] = useState(0);\n\n useEffect(() => {\n const content = contentRef.current;\n if (!content) return undefined;\n\n if (!open) {\n setHeight(0);\n return undefined;\n }\n\n setHeight(content.scrollHeight);\n\n const observer = new ResizeObserver(() => setHeight(content.scrollHeight));\n observer.observe(content);\n\n return () => observer.disconnect();\n }, [open]);\n\n return (\n <div\n style={{ height }}\n aria-hidden={!open}\n className={twMerge('overflow-hidden transition-[height] duration-slow ease-enter', className)}\n >\n <div ref={contentRef}>{children}</div>\n </div>\n );\n};\n\nexport default ControlledCollapse;\n"],"names":[],"mappings":";;;AAcA,MAAM,qBAAyD,CAAC,EAAE,MAAM,UAAU,gBAAgB;AAChG,QAAM,aAAa,OAAuB,IAAI;AAC9C,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,CAAC;AAEtC,YAAU,MAAM;AACd,UAAM,UAAU,WAAW;AAC3B,QAAI,CAAC,QAAS,QAAO;AAErB,QAAI,CAAC,MAAM;AACT,gBAAU,CAAC;AACX,aAAO;AAAA,IACT;AAEA,cAAU,QAAQ,YAAY;AAE9B,UAAM,WAAW,IAAI,eAAe,MAAM,UAAU,QAAQ,YAAY,CAAC;AACzE,aAAS,QAAQ,OAAO;AAExB,WAAO,MAAM,SAAS,WAAA;AAAA,EACxB,GAAG,CAAC,IAAI,CAAC;AAET,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,OAAO,EAAE,OAAA;AAAA,MACT,eAAa,CAAC;AAAA,MACd,WAAW,QAAQ,gEAAgE,SAAS;AAAA,MAE5F,UAAA,oBAAC,OAAA,EAAI,KAAK,YAAa,SAAA,CAAS;AAAA,IAAA;AAAA,EAAA;AAGtC;"}
1
+ {"version":3,"file":"ControlledCollapse.js","sources":["../../../src/components/UI/ControlledCollapse.tsx"],"sourcesContent":["import React, { useEffect, useRef, useState } from 'react';\nimport { twMerge } from 'tailwind-merge';\n\ninterface IControlledCollapseProps {\n open: boolean;\n children: React.ReactNode;\n className?: string;\n}\n\n/**\n * Height is measured from the content node and written to the wrapper, so the transition\n * always has a numeric start and end. The observer watches the CONTENT, whose height is\n * unconstrained, so updating the wrapper cannot feed back into it.\n *\n * A closed collapse is `inert`: `height: 0` with `overflow: hidden` still leaves whatever is\n * inside reachable by Tab, so without it a keyboard lands on controls nobody can see, and\n * `aria-hidden` around focusable content is itself an ARIA violation. Set through the DOM\n * because React 18 does not type the attribute.\n */\nconst ControlledCollapse: React.FC<IControlledCollapseProps> = ({ open, children, className }) => {\n const wrapperRef = useRef<HTMLDivElement>(null);\n const contentRef = useRef<HTMLDivElement>(null);\n const [height, setHeight] = useState(0);\n\n useEffect(() => {\n const wrapper = wrapperRef.current;\n if (!wrapper) return;\n if (open) wrapper.removeAttribute('inert');\n else wrapper.setAttribute('inert', '');\n }, [open]);\n\n useEffect(() => {\n const content = contentRef.current;\n if (!content) return undefined;\n\n if (!open) {\n setHeight(0);\n return undefined;\n }\n\n setHeight(content.scrollHeight);\n\n const observer = new ResizeObserver(() => setHeight(content.scrollHeight));\n observer.observe(content);\n\n return () => observer.disconnect();\n }, [open]);\n\n return (\n <div\n ref={wrapperRef}\n style={{ height }}\n aria-hidden={!open}\n className={twMerge('overflow-hidden transition-[height] duration-slow ease-enter', className)}\n >\n <div ref={contentRef}>{children}</div>\n </div>\n );\n};\n\nexport default ControlledCollapse;\n"],"names":[],"mappings":";;;AAmBA,MAAM,qBAAyD,CAAC,EAAE,MAAM,UAAU,gBAAgB;AAChG,QAAM,aAAa,OAAuB,IAAI;AAC9C,QAAM,aAAa,OAAuB,IAAI;AAC9C,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,CAAC;AAEtC,YAAU,MAAM;AACd,UAAM,UAAU,WAAW;AAC3B,QAAI,CAAC,QAAS;AACd,QAAI,KAAM,SAAQ,gBAAgB,OAAO;AAAA,QACpC,SAAQ,aAAa,SAAS,EAAE;AAAA,EACvC,GAAG,CAAC,IAAI,CAAC;AAET,YAAU,MAAM;AACd,UAAM,UAAU,WAAW;AAC3B,QAAI,CAAC,QAAS,QAAO;AAErB,QAAI,CAAC,MAAM;AACT,gBAAU,CAAC;AACX,aAAO;AAAA,IACT;AAEA,cAAU,QAAQ,YAAY;AAE9B,UAAM,WAAW,IAAI,eAAe,MAAM,UAAU,QAAQ,YAAY,CAAC;AACzE,aAAS,QAAQ,OAAO;AAExB,WAAO,MAAM,SAAS,WAAA;AAAA,EACxB,GAAG,CAAC,IAAI,CAAC;AAET,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,KAAK;AAAA,MACL,OAAO,EAAE,OAAA;AAAA,MACT,eAAa,CAAC;AAAA,MACd,WAAW,QAAQ,gEAAgE,SAAS;AAAA,MAE5F,UAAA,oBAAC,OAAA,EAAI,KAAK,YAAa,SAAA,CAAS;AAAA,IAAA;AAAA,EAAA;AAGtC;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umodeapporg/ui",
3
- "version": "0.8.1",
3
+ "version": "0.9.0",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "sideEffects": [