@umodeapporg/ui 0.9.2 → 0.10.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.
@@ -13,6 +13,11 @@ export interface ITabInfo {
13
13
  isVisible?: boolean;
14
14
  /** When false, the tab keeps a plain label in the bar: no inline rename pencil and no visibility eye (e.g. aggregate tabs). */
15
15
  editable?: boolean;
16
+ /**
17
+ * Content rendered after the label, inside the tab (a per-tab actions menu, a status icon). Presses
18
+ * on it never select or drag the tab, so a menu trigger placed here opens without switching tabs.
19
+ */
20
+ trailingSlot?: ReactNode;
16
21
  }
17
22
  export interface IDataTab {
18
23
  key: string;
@@ -1,4 +1,4 @@
1
- import { jsx, jsxs, Fragment } from "react/jsx-runtime";
1
+ import { jsxs, jsx, Fragment } from "react/jsx-runtime";
2
2
  import { forwardRef, useMemo, useRef, useState, useImperativeHandle, useEffect, useCallback } from "react";
3
3
  import classNames from "classnames";
4
4
  import { useTranslation } from "react-i18next";
@@ -333,7 +333,7 @@ const ControlledTabs = forwardRef(
333
333
  const tabDragProps = onReorderTab ? {
334
334
  draggable: true,
335
335
  onDragStart: (event) => {
336
- if (event.target.closest("input")) {
336
+ if (event.target.closest("input, [data-tab-trailing-slot]")) {
337
337
  event.preventDefault();
338
338
  return;
339
339
  }
@@ -348,7 +348,7 @@ const ControlledTabs = forwardRef(
348
348
  onDragOver: (event) => event.preventDefault(),
349
349
  onDragEnd: handleTabDragEnd
350
350
  } : {};
351
- return /* @__PURE__ */ jsx(
351
+ return /* @__PURE__ */ jsxs(
352
352
  Tab,
353
353
  {
354
354
  value: tab.value,
@@ -372,18 +372,32 @@ const ControlledTabs = forwardRef(
372
372
  ["invisible"]: clippedTabValues.includes(tab.value)
373
373
  }
374
374
  ),
375
- children: onRenameTab && tab.editable !== false ? /* @__PURE__ */ jsx(
376
- EditableTabLabel,
377
- {
378
- label: tab.label,
379
- count: tab.count,
380
- loading,
381
- isVisible: tab.isVisible !== false,
382
- canToggleVisibility: tab.isVisible === false || tabs.filter((t2) => t2.isVisible !== false).length > 1,
383
- onRename: (newLabel) => onRenameTab(tab.value, newLabel),
384
- onToggleVisibility: onToggleTabVisibility ? () => handleToggleTabVisibility(tab.value) : void 0
385
- }
386
- ) : tab.tooltip ? /* @__PURE__ */ jsx(ControlledTooltip, { content: tab.tooltip, className: "max-w-60", children: tabLabel(tab.label, tab.count, tab.alertCount) }) : tabLabel(tab.label, tab.count, tab.alertCount)
375
+ children: [
376
+ onRenameTab && tab.editable !== false ? /* @__PURE__ */ jsx(
377
+ EditableTabLabel,
378
+ {
379
+ label: tab.label,
380
+ count: tab.count,
381
+ loading,
382
+ isVisible: tab.isVisible !== false,
383
+ canToggleVisibility: tab.isVisible === false || tabs.filter((t2) => t2.isVisible !== false).length > 1,
384
+ onRename: (newLabel) => onRenameTab(tab.value, newLabel),
385
+ onToggleVisibility: onToggleTabVisibility ? () => handleToggleTabVisibility(tab.value) : void 0
386
+ }
387
+ ) : tab.tooltip ? /* @__PURE__ */ jsx(ControlledTooltip, { content: tab.tooltip, className: "max-w-60", children: tabLabel(tab.label, tab.count, tab.alertCount) }) : tabLabel(tab.label, tab.count, tab.alertCount),
388
+ tab.trailingSlot && /* @__PURE__ */ jsx(
389
+ "span",
390
+ {
391
+ "data-tab-trailing-slot": true,
392
+ className: "ml-1 flex items-center",
393
+ onClick: (event) => event.stopPropagation(),
394
+ onMouseDown: (event) => event.stopPropagation(),
395
+ onPointerDown: (event) => event.stopPropagation(),
396
+ onKeyDown: (event) => event.stopPropagation(),
397
+ children: tab.trailingSlot
398
+ }
399
+ )
400
+ ]
387
401
  },
388
402
  `controlled-tab-${index}-${tab.value}`
389
403
  );
@@ -1 +1 @@
1
- {"version":3,"file":"ControlledTabs.js","sources":["../../../src/components/Form/ControlledTabs.tsx"],"sourcesContent":["import { forwardRef, ReactNode, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';\nimport classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\n\nimport { useUiNavigation } from '@/components/Navigation/UiNavigationContext';\nimport { TUiCapability, useUiAccess } from '@/components/UI/access/UiAccessContext';\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { formatNumber } from '@/helpers/formatHelpers';\nimport {\n claimUrlParam,\n queueUrlParamUpdate,\n releaseUrlParam,\n TABS_URL_PARAM,\n URL_PARAM_PRIORITY,\n} from '@/helpers/urlParamSync';\n\nimport LoadingSpinner from '../UI/LoadingSpinner';\nimport { MaterialSymbol } from '../UI/MaterialSymbol';\n\nimport ControlledButton from './ControlledButton';\nimport { Tab, TabPanel, TabsBody, TabsHeader, TabsRoot } from './ControlledTabsPrimitives';\n\ninterface IEditableTabLabelProps {\n label: string;\n count?: number;\n loading?: boolean;\n isVisible?: boolean;\n canToggleVisibility?: boolean;\n onRename: (newLabel: string) => void;\n onToggleVisibility?: () => void;\n}\n\nconst EditableTabLabel = ({\n label,\n count,\n loading,\n isVisible = true,\n canToggleVisibility = true,\n onRename,\n onToggleVisibility,\n}: IEditableTabLabelProps) => {\n const { t } = useTranslation('ui');\n const [editing, setEditing] = useState(false);\n const [draft, setDraft] = useState(label);\n const inputRef = useRef<HTMLInputElement>(null);\n\n useEffect(() => {\n if (editing) {\n inputRef.current?.focus();\n inputRef.current?.select();\n }\n }, [editing]);\n\n const handleEditClick = (e: React.MouseEvent) => {\n e.stopPropagation();\n setDraft(label);\n setEditing(true);\n };\n\n const commit = () => {\n const trimmed = draft.trim();\n if (trimmed && trimmed !== label) onRename(trimmed);\n setEditing(false);\n };\n\n const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {\n if (e.key === 'Enter') {\n e.preventDefault();\n commit();\n }\n if (e.key === 'Escape') {\n setEditing(false);\n setDraft(label);\n }\n e.stopPropagation();\n };\n\n if (editing) {\n return (\n <div className=\"flex items-center\" onClick={(e) => e.stopPropagation()}>\n <input\n ref={inputRef}\n value={draft}\n onChange={(e) => setDraft(e.target.value)}\n onBlur={commit}\n onKeyDown={handleKeyDown}\n style={{ width: `${Math.max(draft.length + 2, 6)}ch` }}\n className=\"border-b border-brand-border bg-transparent px-0 py-0.5 text-sm font-medium leading-none focus:outline-none\"\n />\n </div>\n );\n }\n\n return (\n <div className=\"group/tab flex items-center gap-x-1.5 align-bottom\">\n <span>{label}</span>\n {count !== undefined &&\n (loading ? (\n <LoadingSpinner size={3} />\n ) : (\n <span className=\"inline-flex min-w-[18px] items-center justify-center rounded-full bg-muted-hover px-1.5 py-px text-[11px] font-medium leading-none text-foreground-subtle\">\n {formatNumber(count)}\n </span>\n ))}\n <ControlledTooltip content={t('controlledTabs.renameTab')}>\n <ControlledButton\n defaultHtmlButton\n onClick={handleEditClick}\n className=\"ml-0.5 flex items-center text-foreground-faint transition-colors hover:text-brand-emphasis\"\n tabIndex={-1}\n >\n <MaterialSymbol icon=\"edit\" size={14} />\n </ControlledButton>\n </ControlledTooltip>\n {onToggleVisibility && (\n <ControlledTooltip\n content={\n !canToggleVisibility\n ? t('controlledTabs.mustKeepOneEnabled')\n : isVisible\n ? t('controlledTabs.disableTab')\n : t('controlledTabs.enableTabShort')\n }\n >\n <ControlledButton\n defaultHtmlButton\n onClick={(e) => {\n e.stopPropagation();\n if (canToggleVisibility) onToggleVisibility();\n }}\n className={classNames('ml-0.5 flex items-center transition-colors', {\n ['text-foreground-faint hover:text-warning-emphasis']: isVisible && canToggleVisibility,\n ['text-warning-emphasis hover:text-success-emphasis']: !isVisible,\n ['cursor-not-allowed text-foreground-faint']: isVisible && !canToggleVisibility,\n })}\n tabIndex={-1}\n >\n <MaterialSymbol icon={isVisible ? 'visibility' : 'visibility_off'} size={14} />\n </ControlledButton>\n </ControlledTooltip>\n )}\n </div>\n );\n};\n\nexport interface ITabInfo {\n label: string;\n value: string;\n /** Missing capability removes the tab and never mounts its panel, so no protected request runs. */\n capability?: TUiCapability | Array<TUiCapability>;\n count?: number;\n /** Pendências/alertas da aba — badge âmbar ao lado do nome (oculto quando 0/ausente). */\n alertCount?: number;\n tooltip?: string;\n status?: number | string;\n isVisible?: boolean;\n /** When false, the tab keeps a plain label in the bar: no inline rename pencil and no visibility eye (e.g. aggregate tabs). */\n editable?: boolean;\n}\n\nexport interface IDataTab {\n key: string;\n element: ReactNode;\n}\n\ninterface IControlledTabsProps {\n initialActiveTab: string;\n activeTab?: string;\n tabs: Array<ITabInfo>;\n dataTabs: Array<IDataTab>;\n loading?: boolean;\n /** Blocks tab switching and dims the header, e.g. while a blocking operation runs in the active tab. */\n disabled?: boolean;\n handleActiveTabChildren?: () => void;\n useRenderValidation?: boolean;\n onTabChange?: (tab: string) => void;\n beforeTabChange?: (from: string, to: string) => boolean;\n tabsBodyClassName?: string;\n tabPanelClassName?: string;\n\n rootClassName?: string;\n /** Classes extras aplicadas a cada aba (ex.: reduzir o padding vertical em telas específicas). */\n tabClassName?: string;\n headerSlot?: ReactNode;\n /** Conteúdo fixo alinhado à direita do cabeçalho das abas (ex.: botão de ação principal). */\n actionsSlot?: ReactNode;\n onRenameTab?: (value: string, newLabel: string) => void;\n onToggleTabVisibility?: (value: string) => void;\n /**\n * Habilita arrastar-e-soltar das abas no cabeçalho (mesmo molde do drag nativo dos\n * campos do construtor de fichas). Recebe os values da aba arrastada e da aba alvo;\n * o consumidor decide como persistir a nova ordem (ex.: tabOrderPriority).\n */\n onReorderTab?: (fromValue: string, toValue: string) => void;\n /**\n * Renders the tab header as a single horizontally-scrollable line (browser-tab style)\n * with navigation arrows and a \"+N\" overflow menu, instead of wrapping onto several lines.\n */\n scrollableHeader?: boolean;\n /**\n * Faz o cabeçalho das abas grudar no topo do container rolável (`position: sticky`), com\n * fundo sólido e uma sombra sutil que aparece só ao rolar, além de um acabamento mais\n * delicado nas abas (pill no hover, sublinhado fino e transição suave). Use quando a barra\n * precisa ficar visível durante todo o scroll (ex.: ficha técnica do produto).\n */\n stickyHeader?: boolean;\n /**\n * Sincroniza a aba ativa com o query param `activeTab` da URL (ler no mount e\n * escrever a cada troca). Default `true`. Use `false` em abas dentro de modais/\n * dialogs, cujo estado é efêmero e não deve poluir a URL da página nem competir\n * com abas de nível de página que já usam `activeTab`.\n */\n syncWithUrl?: boolean;\n}\n\nconst ControlledTabs = forwardRef<unknown, IControlledTabsProps>(\n (\n {\n initialActiveTab,\n activeTab: controlledActiveTab,\n tabs: tabsProp,\n dataTabs: dataTabsProp,\n loading = false,\n disabled = false,\n handleActiveTabChildren,\n useRenderValidation = false,\n onTabChange,\n beforeTabChange,\n tabsBodyClassName,\n tabPanelClassName,\n rootClassName,\n tabClassName,\n headerSlot,\n actionsSlot,\n onRenameTab,\n onToggleTabVisibility,\n onReorderTab,\n scrollableHeader = false,\n stickyHeader = false,\n syncWithUrl = true,\n },\n ref,\n ) => {\n const { t } = useTranslation('ui');\n const { check } = useUiAccess();\n\n const deniedTabValues = useMemo(\n () => new Set(tabsProp.filter((tab) => tab.capability && !check(tab.capability)).map((tab) => tab.value)),\n [tabsProp, check],\n );\n\n const tabs = useMemo(() => tabsProp.filter((tab) => !deniedTabValues.has(tab.value)), [tabsProp, deniedTabValues]);\n\n const dataTabs = useMemo(\n () => dataTabsProp.filter((tabPanel) => !deniedTabValues.has(tabPanel.key)),\n [dataTabsProp, deniedTabValues],\n );\n\n const { location, searchParams, setSearchParams } = useUiNavigation();\n const urlParamOwnerIdRef = useRef<object>({});\n const scrollRef = useRef<HTMLDivElement>(null);\n const stickySentinelRef = useRef<HTMLDivElement>(null);\n const stickyBarRef = useRef<HTMLDivElement>(null);\n const [canScrollLeft, setCanScrollLeft] = useState(false);\n const [canScrollRight, setCanScrollRight] = useState(false);\n // Abas que cruzam uma borda da área rolável (visíveis só pela metade). Ficam invisíveis para\n // que na borda só apareça aba inteira ou espaço vazio, nunca um pedaço de texto cortado.\n const [clippedTabValues, setClippedTabValues] = useState<Array<string>>([]);\n // Barra sticky: detecta quando a barra \"grudou\" (usuário já rolou) para só então mostrar a sombra.\n const [isStuck, setIsStuck] = useState(false);\n // Drag nativo das abas (opt-in via onReorderTab), mesmo molde dos campos do construtor.\n const dragTabValueRef = useRef<string | null>(null);\n const [draggingTabValue, setDraggingTabValue] = useState<string | null>(null);\n const [dragOverTabValue, setDragOverTabValue] = useState<string | null>(null);\n\n const resolveInitialActiveTab = () => {\n const fallbackTab = tabs.some((tab) => tab.value === initialActiveTab)\n ? initialActiveTab\n : (tabs[0]?.value ?? initialActiveTab);\n\n if (!syncWithUrl) return fallbackTab;\n if ((location.state as { ignoreActiveTab?: boolean } | null | undefined)?.ignoreActiveTab === true)\n return fallbackTab;\n const urlActiveTab = searchParams.get(TABS_URL_PARAM);\n const isValidUrlTab = urlActiveTab != null && tabs.some((tab) => tab.value === urlActiveTab);\n return isValidUrlTab ? urlActiveTab : fallbackTab;\n };\n\n const [internalActiveTab, setInternalActiveTab] = useState(resolveInitialActiveTab);\n const isControlled = controlledActiveTab !== undefined;\n const activeTab = isControlled ? controlledActiveTab : internalActiveTab;\n\n const handleActiveTab = (tab: string) => {\n if (disabled) return;\n if (beforeTabChange && !beforeTabChange(activeTab, tab)) {\n if (!isControlled) {\n setInternalActiveTab(activeTab);\n }\n return;\n }\n if (handleActiveTabChildren) {\n handleActiveTabChildren();\n }\n if (!isControlled) {\n setInternalActiveTab(tab);\n }\n if (onTabChange) {\n onTabChange(tab);\n }\n\n if (syncWithUrl) {\n queueUrlParamUpdate(TABS_URL_PARAM, tab, setSearchParams);\n }\n };\n\n useImperativeHandle(ref, () => ({\n activeTab,\n setActiveTab: handleActiveTab,\n }));\n\n const handleToggleTabVisibility = (value: string) => {\n const visibleCount = tabs.filter((t) => t.isVisible !== false).length;\n const isCurrentlyVisible = tabs.find((t) => t.value === value)?.isVisible !== false;\n if (isCurrentlyVisible && visibleCount <= 1) return;\n onToggleTabVisibility?.(value);\n };\n\n // As abas são o topo da hierarquia de parâmetros: assumem `activeTab` mesmo montando depois de\n // um controle que já o tivesse reivindicado, e o liberam ao desmontar.\n useEffect(() => {\n if (!syncWithUrl) return;\n\n const ownerId = urlParamOwnerIdRef.current;\n claimUrlParam(TABS_URL_PARAM, ownerId, URL_PARAM_PRIORITY.tabs);\n\n return () => releaseUrlParam(TABS_URL_PARAM, ownerId);\n }, [syncWithUrl]);\n\n useEffect(() => {\n if (syncWithUrl && searchParams.get(TABS_URL_PARAM) !== activeTab) {\n queueUrlParamUpdate(TABS_URL_PARAM, activeTab, setSearchParams);\n }\n const tabButton = document.querySelector(`li[data-value=${activeTab}]`) as HTMLElement | null;\n if (tabButton) {\n tabButton.click();\n }\n }, [activeTab]);\n\n const updateOverflow = useCallback(() => {\n const container = scrollRef.current;\n if (!container) return;\n\n setCanScrollLeft(container.scrollLeft > 4);\n setCanScrollRight(container.scrollLeft + container.clientWidth < container.scrollWidth - 4);\n\n const EDGE_TOLERANCE = 1;\n const containerRect = container.getBoundingClientRect();\n const clipped: Array<string> = [];\n container.querySelectorAll('[data-value]').forEach((node) => {\n const element = node as HTMLElement;\n const value = element.getAttribute('data-value');\n if (!value || value === activeTab) return;\n const rect = element.getBoundingClientRect();\n const crossesLeftEdge =\n rect.left < containerRect.left - EDGE_TOLERANCE && rect.right > containerRect.left + EDGE_TOLERANCE;\n const crossesRightEdge =\n rect.right > containerRect.right + EDGE_TOLERANCE && rect.left < containerRect.right - EDGE_TOLERANCE;\n if (crossesLeftEdge || crossesRightEdge) clipped.push(value);\n });\n setClippedTabValues((prev) =>\n prev.length === clipped.length && prev.every((value, index) => value === clipped[index]) ? prev : clipped,\n );\n }, [activeTab]);\n\n useEffect(() => {\n if (!stickyHeader) return;\n const sentinel = stickySentinelRef.current;\n if (!sentinel || typeof IntersectionObserver === 'undefined') return;\n // Descobre o ancestral rolável para observar o sentinela em relação a ele. Quando o sentinela\n // (logo acima da barra) sai do topo do container, a barra grudou e ligamos a sombra.\n let scrollParent = sentinel.parentElement;\n while (scrollParent) {\n const overflowY = getComputedStyle(scrollParent).overflowY;\n if (overflowY === 'auto' || overflowY === 'scroll') break;\n scrollParent = scrollParent.parentElement;\n }\n const observer = new IntersectionObserver(([entry]) => setIsStuck(!entry.isIntersecting), {\n root: scrollParent ?? null,\n threshold: 0,\n });\n observer.observe(sentinel);\n return () => observer.disconnect();\n }, [stickyHeader]);\n\n // Publica a altura da barra fixa como `--sticky-tabs-height` no container rolável, para\n // que conteúdo sticky mais abaixo (ex.: cabeçalho de material da ficha) possa grudar logo\n // abaixo da barra em vez de ficar escondido atrás dela.\n useEffect(() => {\n if (!stickyHeader) return;\n const bar = stickyBarRef.current;\n if (!bar) return;\n let scrollParent = bar.parentElement;\n while (scrollParent) {\n const overflowY = getComputedStyle(scrollParent).overflowY;\n if (overflowY === 'auto' || overflowY === 'scroll') break;\n scrollParent = scrollParent.parentElement;\n }\n if (!scrollParent) return;\n const target = scrollParent;\n let rafId = 0;\n let lastHeight = -1;\n // Escreve a variável só quando a altura muda de fato, e fora do ciclo de entrega do\n // ResizeObserver (via rAF): escrever um valor que afeta layout dentro do callback dispara\n // \"ResizeObserver loop completed with undelivered notifications\".\n const publishHeight = () => {\n const height = bar.offsetHeight;\n if (height === lastHeight) return;\n lastHeight = height;\n target.style.setProperty('--sticky-tabs-height', `${height}px`);\n };\n publishHeight();\n if (typeof ResizeObserver === 'undefined') return;\n const observer = new ResizeObserver(() => {\n cancelAnimationFrame(rafId);\n rafId = requestAnimationFrame(publishHeight);\n });\n observer.observe(bar);\n return () => {\n cancelAnimationFrame(rafId);\n observer.disconnect();\n target.style.removeProperty('--sticky-tabs-height');\n };\n }, [stickyHeader]);\n\n useEffect(() => {\n if (!scrollableHeader) return;\n updateOverflow();\n const onResize = () => updateOverflow();\n window.addEventListener('resize', onResize);\n return () => window.removeEventListener('resize', onResize);\n }, [scrollableHeader, tabs, updateOverflow]);\n\n const scrollByStep = (direction: number) => {\n const container = scrollRef.current;\n if (!container) return;\n const EDGE_TOLERANCE = 1;\n const containerRect = container.getBoundingClientRect();\n const nodes = Array.from(container.querySelectorAll('[data-value]')) as Array<HTMLElement>;\n\n if (direction > 0) {\n const nextHidden = nodes.find(\n (node) => node.getBoundingClientRect().right > containerRect.right + EDGE_TOLERANCE,\n );\n if (!nextHidden) return;\n container.scrollBy({ left: nextHidden.getBoundingClientRect().left - containerRect.left, behavior: 'smooth' });\n return;\n }\n\n const previousHidden = [...nodes]\n .reverse()\n .find((node) => node.getBoundingClientRect().left < containerRect.left - EDGE_TOLERANCE);\n if (!previousHidden) return;\n container.scrollBy({\n left: previousHidden.getBoundingClientRect().right - containerRect.right,\n behavior: 'smooth',\n });\n };\n\n const tabLabel = (label: string, count?: number, alertCount?: number) => {\n const showCount = count !== undefined;\n return (\n <div className=\"flex items-center gap-x-1.5 align-bottom\">\n {label}\n {loading && showCount ? (\n <LoadingSpinner size={3} />\n ) : showCount ? (\n <span className=\"inline-flex min-w-[18px] items-center justify-center rounded-full bg-muted-hover px-1.5 py-px text-[11px] font-medium leading-none text-foreground-subtle\">\n {formatNumber(count)}\n </span>\n ) : null}\n {Boolean(alertCount) && (\n <span className=\"inline-flex min-w-[18px] items-center justify-center rounded-full bg-warning-soft px-1.5 py-px text-[11px] font-semibold leading-none text-warning-emphasis\">\n {formatNumber(alertCount as number)}\n </span>\n )}\n </div>\n );\n };\n\n const handleTabDragEnd = () => {\n const from = dragTabValueRef.current;\n const to = dragOverTabValue;\n\n if (from && to && from !== to) {\n onReorderTab?.(from, to);\n }\n\n dragTabValueRef.current = null;\n setDraggingTabValue(null);\n setDragOverTabValue(null);\n };\n\n const tabElements = tabs.map((tab, index) => {\n const isActive = activeTab === tab.value && tab.isVisible !== false;\n const isInactive = activeTab !== tab.value && tab.isVisible !== false;\n const isTabDropTarget =\n Boolean(draggingTabValue) && dragOverTabValue === tab.value && draggingTabValue !== tab.value;\n const tabDragProps = onReorderTab\n ? {\n draggable: true,\n onDragStart: (event: React.DragEvent) => {\n // Arrastar a partir do input de renomear deve selecionar texto, não mover a aba.\n if ((event.target as HTMLElement).closest('input')) {\n event.preventDefault();\n return;\n }\n event.dataTransfer.effectAllowed = 'move';\n dragTabValueRef.current = tab.value;\n setDraggingTabValue(tab.value);\n },\n onDragEnter: (event: React.DragEvent) => {\n event.preventDefault();\n if (dragTabValueRef.current) setDragOverTabValue(tab.value);\n },\n onDragOver: (event: React.DragEvent) => event.preventDefault(),\n onDragEnd: handleTabDragEnd,\n }\n : {};\n return (\n <Tab\n key={`controlled-tab-${index}-${tab.value}`}\n value={tab.value}\n style={{ zIndex: 2 }}\n disabled={loading}\n onClick={() => handleActiveTab(tab.value)}\n {...tabDragProps}\n className={classNames(\n 'max-w-fit px-4',\n tabClassName,\n // Acabamento mais delicado quando a barra é sticky: pill sutil no hover das abas\n // inativas e peso de fonte mais leve ao trocar de aba.\n stickyHeader && 'rounded-md transition-interactive duration-fast',\n {\n [stickyHeader ? 'font-medium text-brand-emphasis' : 'text-brand-emphasis']: isActive,\n [stickyHeader ? 'font-normal opacity-70 hover:bg-muted-hover hover:opacity-100' : 'opacity-70']:\n isInactive,\n ['!text-foreground-faint line-through opacity-60 hover:!text-foreground-faint']: tab.isVisible === false,\n ['cursor-grab active:cursor-grabbing']: Boolean(onReorderTab),\n ['rounded-md bg-brand-soft !opacity-100 ring-1 ring-brand-border']: isTabDropTarget,\n ['opacity-40']: draggingTabValue === tab.value,\n ['invisible']: clippedTabValues.includes(tab.value),\n },\n )}\n >\n {onRenameTab && tab.editable !== false ? (\n <EditableTabLabel\n label={tab.label}\n count={tab.count}\n loading={loading}\n isVisible={tab.isVisible !== false}\n canToggleVisibility={tab.isVisible === false || tabs.filter((t) => t.isVisible !== false).length > 1}\n onRename={(newLabel) => onRenameTab(tab.value, newLabel)}\n onToggleVisibility={onToggleTabVisibility ? () => handleToggleTabVisibility(tab.value) : undefined}\n />\n ) : tab.tooltip ? (\n <ControlledTooltip content={tab.tooltip} className=\"max-w-60\">\n {tabLabel(tab.label, tab.count, tab.alertCount)}\n </ControlledTooltip>\n ) : (\n tabLabel(tab.label, tab.count, tab.alertCount)\n )}\n </Tab>\n );\n });\n\n // Sublinhado fino (2px). Na barra sticky mantemos a animação de deslize nativa (transform) com\n // transição suave de ~120ms; nas demais telas o indicador segue estático como antes.\n\n const scrollArrowClass =\n 'flex size-8 shrink-0 items-center justify-center rounded-full text-foreground-subtle transition-colors hover:bg-brand-soft hover:text-brand-emphasis';\n\n return (\n <TabsRoot value={activeTab} className={classNames('!overflow-visible', rootClassName)}>\n {/* Sentinela de 1px: quando sai do topo do container rolável a barra já grudou e acende a sombra. */}\n {stickyHeader && <div ref={stickySentinelRef} className=\"h-px\" aria-hidden />}\n <div\n ref={stickyBarRef}\n className={classNames(\n 'flex shrink-0 items-center',\n // Com header rolável a barra é sempre UMA linha (nowrap): as abas cedem e viram scroll\n // horizontal, enquanto as ações (shrink-0 + ml-auto) permanecem pinadas à direita e nunca\n // caem para baixo. Sem header rolável, mantém o comportamento antigo de wrap.\n scrollableHeader ? 'flex-nowrap gap-x-3' : actionsSlot ? 'flex-wrap gap-x-3 gap-y-2' : 'gap-1',\n // Gruda logo abaixo do header do produto: fundo sólido para tapar o conteúdo que\n // rola por baixo e sombra sutil só depois que a barra encosta no topo.\n stickyHeader && 'sticky top-0 z-20 bg-surface transition-shadow duration-fast',\n stickyHeader && isStuck && 'shadow-elevation1',\n disabled && 'pointer-events-none select-none opacity-50',\n )}\n >\n {headerSlot && <>{headerSlot}</>}\n {scrollableHeader ? (\n <div className=\"flex min-w-0 flex-1 items-center gap-1 border-b border-border-subtle\">\n <ControlledButton\n defaultHtmlButton\n onClick={() => scrollByStep(-1)}\n disabled={!canScrollLeft}\n className={classNames(scrollArrowClass, { ['pointer-events-none opacity-0']: !canScrollLeft })}\n tabIndex={-1}\n >\n <MaterialSymbol icon=\"chevron_left\" size={20} />\n </ControlledButton>\n\n <div ref={scrollRef} onScroll={updateOverflow} className=\"scrollbar-hide min-w-0 flex-1 overflow-x-auto\">\n <TabsHeader className=\"flex w-max flex-nowrap items-center gap-1 rounded-none border-b-0 border-border-subtle bg-transparent p-0\">\n {tabElements}\n </TabsHeader>\n </div>\n\n <ControlledButton\n defaultHtmlButton\n onClick={() => scrollByStep(1)}\n disabled={!canScrollRight}\n className={classNames(scrollArrowClass, { ['pointer-events-none opacity-0']: !canScrollRight })}\n tabIndex={-1}\n >\n <MaterialSymbol icon=\"chevron_right\" size={20} />\n </ControlledButton>\n </div>\n ) : (\n <TabsHeader className=\"flex flex-1 flex-wrap items-center gap-1 rounded-none border-b-0 border-border-subtle bg-transparent p-0\">\n {tabElements}\n </TabsHeader>\n )}\n {actionsSlot && <div className=\"ml-auto flex shrink-0 items-center\">{actionsSlot}</div>}\n </div>\n\n <TabsBody className={tabsBodyClassName}>\n {dataTabs.map((tabPanel) => {\n const isTabVisible = tabs.find((t) => t.value === tabPanel.key)?.isVisible !== false;\n return (\n <TabPanel key={tabPanel.key} value={tabPanel.key} className={classNames('size-full', tabPanelClassName)}>\n <div className={classNames('relative size-full', { ['pointer-events-none']: !isTabVisible })}>\n {useRenderValidation\n ? activeTab === tabPanel.key && (\n <div className=\"size-full animate-fade-in transition-interactive duration-slow ease-standard\">\n {tabPanel.element}\n </div>\n )\n : tabPanel.element}\n {!isTabVisible && (\n <div className=\"absolute inset-0 z-10 flex flex-col items-center justify-center gap-2 rounded-lg bg-muted-hover/70 glass-veil\">\n <MaterialSymbol icon=\"visibility_off\" size={32} className=\"text-foreground-faint\" />\n <span className=\"text-sm font-medium text-foreground-subtle\">\n {t('controlledTabs.tabDisabled')}\n </span>\n {onToggleTabVisibility && (\n <ControlledButton\n size=\"sm\"\n variant=\"outlined\"\n className=\"pointer-events-auto mt-1 flex items-center gap-x-1\"\n onClick={() => onToggleTabVisibility(tabPanel.key)}\n >\n <MaterialSymbol icon=\"visibility\" size={16} />\n {t('controlledTabs.enableTab')}\n </ControlledButton>\n )}\n </div>\n )}\n </div>\n </TabPanel>\n );\n })}\n </TabsBody>\n </TabsRoot>\n );\n },\n);\n\nexport default ControlledTabs;\n"],"names":["t"],"mappings":"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"}
1
+ {"version":3,"file":"ControlledTabs.js","sources":["../../../src/components/Form/ControlledTabs.tsx"],"sourcesContent":["import { forwardRef, ReactNode, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';\nimport classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\n\nimport { useUiNavigation } from '@/components/Navigation/UiNavigationContext';\nimport { TUiCapability, useUiAccess } from '@/components/UI/access/UiAccessContext';\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { formatNumber } from '@/helpers/formatHelpers';\nimport {\n claimUrlParam,\n queueUrlParamUpdate,\n releaseUrlParam,\n TABS_URL_PARAM,\n URL_PARAM_PRIORITY,\n} from '@/helpers/urlParamSync';\n\nimport LoadingSpinner from '../UI/LoadingSpinner';\nimport { MaterialSymbol } from '../UI/MaterialSymbol';\n\nimport ControlledButton from './ControlledButton';\nimport { Tab, TabPanel, TabsBody, TabsHeader, TabsRoot } from './ControlledTabsPrimitives';\n\ninterface IEditableTabLabelProps {\n label: string;\n count?: number;\n loading?: boolean;\n isVisible?: boolean;\n canToggleVisibility?: boolean;\n onRename: (newLabel: string) => void;\n onToggleVisibility?: () => void;\n}\n\nconst EditableTabLabel = ({\n label,\n count,\n loading,\n isVisible = true,\n canToggleVisibility = true,\n onRename,\n onToggleVisibility,\n}: IEditableTabLabelProps) => {\n const { t } = useTranslation('ui');\n const [editing, setEditing] = useState(false);\n const [draft, setDraft] = useState(label);\n const inputRef = useRef<HTMLInputElement>(null);\n\n useEffect(() => {\n if (editing) {\n inputRef.current?.focus();\n inputRef.current?.select();\n }\n }, [editing]);\n\n const handleEditClick = (e: React.MouseEvent) => {\n e.stopPropagation();\n setDraft(label);\n setEditing(true);\n };\n\n const commit = () => {\n const trimmed = draft.trim();\n if (trimmed && trimmed !== label) onRename(trimmed);\n setEditing(false);\n };\n\n const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {\n if (e.key === 'Enter') {\n e.preventDefault();\n commit();\n }\n if (e.key === 'Escape') {\n setEditing(false);\n setDraft(label);\n }\n e.stopPropagation();\n };\n\n if (editing) {\n return (\n <div className=\"flex items-center\" onClick={(e) => e.stopPropagation()}>\n <input\n ref={inputRef}\n value={draft}\n onChange={(e) => setDraft(e.target.value)}\n onBlur={commit}\n onKeyDown={handleKeyDown}\n style={{ width: `${Math.max(draft.length + 2, 6)}ch` }}\n className=\"border-b border-brand-border bg-transparent px-0 py-0.5 text-sm font-medium leading-none focus:outline-none\"\n />\n </div>\n );\n }\n\n return (\n <div className=\"group/tab flex items-center gap-x-1.5 align-bottom\">\n <span>{label}</span>\n {count !== undefined &&\n (loading ? (\n <LoadingSpinner size={3} />\n ) : (\n <span className=\"inline-flex min-w-[18px] items-center justify-center rounded-full bg-muted-hover px-1.5 py-px text-[11px] font-medium leading-none text-foreground-subtle\">\n {formatNumber(count)}\n </span>\n ))}\n <ControlledTooltip content={t('controlledTabs.renameTab')}>\n <ControlledButton\n defaultHtmlButton\n onClick={handleEditClick}\n className=\"ml-0.5 flex items-center text-foreground-faint transition-colors hover:text-brand-emphasis\"\n tabIndex={-1}\n >\n <MaterialSymbol icon=\"edit\" size={14} />\n </ControlledButton>\n </ControlledTooltip>\n {onToggleVisibility && (\n <ControlledTooltip\n content={\n !canToggleVisibility\n ? t('controlledTabs.mustKeepOneEnabled')\n : isVisible\n ? t('controlledTabs.disableTab')\n : t('controlledTabs.enableTabShort')\n }\n >\n <ControlledButton\n defaultHtmlButton\n onClick={(e) => {\n e.stopPropagation();\n if (canToggleVisibility) onToggleVisibility();\n }}\n className={classNames('ml-0.5 flex items-center transition-colors', {\n ['text-foreground-faint hover:text-warning-emphasis']: isVisible && canToggleVisibility,\n ['text-warning-emphasis hover:text-success-emphasis']: !isVisible,\n ['cursor-not-allowed text-foreground-faint']: isVisible && !canToggleVisibility,\n })}\n tabIndex={-1}\n >\n <MaterialSymbol icon={isVisible ? 'visibility' : 'visibility_off'} size={14} />\n </ControlledButton>\n </ControlledTooltip>\n )}\n </div>\n );\n};\n\nexport interface ITabInfo {\n label: string;\n value: string;\n /** Missing capability removes the tab and never mounts its panel, so no protected request runs. */\n capability?: TUiCapability | Array<TUiCapability>;\n count?: number;\n /** Pendências/alertas da aba — badge âmbar ao lado do nome (oculto quando 0/ausente). */\n alertCount?: number;\n tooltip?: string;\n status?: number | string;\n isVisible?: boolean;\n /** When false, the tab keeps a plain label in the bar: no inline rename pencil and no visibility eye (e.g. aggregate tabs). */\n editable?: boolean;\n /**\n * Content rendered after the label, inside the tab (a per-tab actions menu, a status icon). Presses\n * on it never select or drag the tab, so a menu trigger placed here opens without switching tabs.\n */\n trailingSlot?: ReactNode;\n}\n\nexport interface IDataTab {\n key: string;\n element: ReactNode;\n}\n\ninterface IControlledTabsProps {\n initialActiveTab: string;\n activeTab?: string;\n tabs: Array<ITabInfo>;\n dataTabs: Array<IDataTab>;\n loading?: boolean;\n /** Blocks tab switching and dims the header, e.g. while a blocking operation runs in the active tab. */\n disabled?: boolean;\n handleActiveTabChildren?: () => void;\n useRenderValidation?: boolean;\n onTabChange?: (tab: string) => void;\n beforeTabChange?: (from: string, to: string) => boolean;\n tabsBodyClassName?: string;\n tabPanelClassName?: string;\n\n rootClassName?: string;\n /** Classes extras aplicadas a cada aba (ex.: reduzir o padding vertical em telas específicas). */\n tabClassName?: string;\n headerSlot?: ReactNode;\n /** Conteúdo fixo alinhado à direita do cabeçalho das abas (ex.: botão de ação principal). */\n actionsSlot?: ReactNode;\n onRenameTab?: (value: string, newLabel: string) => void;\n onToggleTabVisibility?: (value: string) => void;\n /**\n * Habilita arrastar-e-soltar das abas no cabeçalho (mesmo molde do drag nativo dos\n * campos do construtor de fichas). Recebe os values da aba arrastada e da aba alvo;\n * o consumidor decide como persistir a nova ordem (ex.: tabOrderPriority).\n */\n onReorderTab?: (fromValue: string, toValue: string) => void;\n /**\n * Renders the tab header as a single horizontally-scrollable line (browser-tab style)\n * with navigation arrows and a \"+N\" overflow menu, instead of wrapping onto several lines.\n */\n scrollableHeader?: boolean;\n /**\n * Faz o cabeçalho das abas grudar no topo do container rolável (`position: sticky`), com\n * fundo sólido e uma sombra sutil que aparece só ao rolar, além de um acabamento mais\n * delicado nas abas (pill no hover, sublinhado fino e transição suave). Use quando a barra\n * precisa ficar visível durante todo o scroll (ex.: ficha técnica do produto).\n */\n stickyHeader?: boolean;\n /**\n * Sincroniza a aba ativa com o query param `activeTab` da URL (ler no mount e\n * escrever a cada troca). Default `true`. Use `false` em abas dentro de modais/\n * dialogs, cujo estado é efêmero e não deve poluir a URL da página nem competir\n * com abas de nível de página que já usam `activeTab`.\n */\n syncWithUrl?: boolean;\n}\n\nconst ControlledTabs = forwardRef<unknown, IControlledTabsProps>(\n (\n {\n initialActiveTab,\n activeTab: controlledActiveTab,\n tabs: tabsProp,\n dataTabs: dataTabsProp,\n loading = false,\n disabled = false,\n handleActiveTabChildren,\n useRenderValidation = false,\n onTabChange,\n beforeTabChange,\n tabsBodyClassName,\n tabPanelClassName,\n rootClassName,\n tabClassName,\n headerSlot,\n actionsSlot,\n onRenameTab,\n onToggleTabVisibility,\n onReorderTab,\n scrollableHeader = false,\n stickyHeader = false,\n syncWithUrl = true,\n },\n ref,\n ) => {\n const { t } = useTranslation('ui');\n const { check } = useUiAccess();\n\n const deniedTabValues = useMemo(\n () => new Set(tabsProp.filter((tab) => tab.capability && !check(tab.capability)).map((tab) => tab.value)),\n [tabsProp, check],\n );\n\n const tabs = useMemo(() => tabsProp.filter((tab) => !deniedTabValues.has(tab.value)), [tabsProp, deniedTabValues]);\n\n const dataTabs = useMemo(\n () => dataTabsProp.filter((tabPanel) => !deniedTabValues.has(tabPanel.key)),\n [dataTabsProp, deniedTabValues],\n );\n\n const { location, searchParams, setSearchParams } = useUiNavigation();\n const urlParamOwnerIdRef = useRef<object>({});\n const scrollRef = useRef<HTMLDivElement>(null);\n const stickySentinelRef = useRef<HTMLDivElement>(null);\n const stickyBarRef = useRef<HTMLDivElement>(null);\n const [canScrollLeft, setCanScrollLeft] = useState(false);\n const [canScrollRight, setCanScrollRight] = useState(false);\n // Abas que cruzam uma borda da área rolável (visíveis só pela metade). Ficam invisíveis para\n // que na borda só apareça aba inteira ou espaço vazio, nunca um pedaço de texto cortado.\n const [clippedTabValues, setClippedTabValues] = useState<Array<string>>([]);\n // Barra sticky: detecta quando a barra \"grudou\" (usuário já rolou) para só então mostrar a sombra.\n const [isStuck, setIsStuck] = useState(false);\n // Drag nativo das abas (opt-in via onReorderTab), mesmo molde dos campos do construtor.\n const dragTabValueRef = useRef<string | null>(null);\n const [draggingTabValue, setDraggingTabValue] = useState<string | null>(null);\n const [dragOverTabValue, setDragOverTabValue] = useState<string | null>(null);\n\n const resolveInitialActiveTab = () => {\n const fallbackTab = tabs.some((tab) => tab.value === initialActiveTab)\n ? initialActiveTab\n : (tabs[0]?.value ?? initialActiveTab);\n\n if (!syncWithUrl) return fallbackTab;\n if ((location.state as { ignoreActiveTab?: boolean } | null | undefined)?.ignoreActiveTab === true)\n return fallbackTab;\n const urlActiveTab = searchParams.get(TABS_URL_PARAM);\n const isValidUrlTab = urlActiveTab != null && tabs.some((tab) => tab.value === urlActiveTab);\n return isValidUrlTab ? urlActiveTab : fallbackTab;\n };\n\n const [internalActiveTab, setInternalActiveTab] = useState(resolveInitialActiveTab);\n const isControlled = controlledActiveTab !== undefined;\n const activeTab = isControlled ? controlledActiveTab : internalActiveTab;\n\n const handleActiveTab = (tab: string) => {\n if (disabled) return;\n if (beforeTabChange && !beforeTabChange(activeTab, tab)) {\n if (!isControlled) {\n setInternalActiveTab(activeTab);\n }\n return;\n }\n if (handleActiveTabChildren) {\n handleActiveTabChildren();\n }\n if (!isControlled) {\n setInternalActiveTab(tab);\n }\n if (onTabChange) {\n onTabChange(tab);\n }\n\n if (syncWithUrl) {\n queueUrlParamUpdate(TABS_URL_PARAM, tab, setSearchParams);\n }\n };\n\n useImperativeHandle(ref, () => ({\n activeTab,\n setActiveTab: handleActiveTab,\n }));\n\n const handleToggleTabVisibility = (value: string) => {\n const visibleCount = tabs.filter((t) => t.isVisible !== false).length;\n const isCurrentlyVisible = tabs.find((t) => t.value === value)?.isVisible !== false;\n if (isCurrentlyVisible && visibleCount <= 1) return;\n onToggleTabVisibility?.(value);\n };\n\n // As abas são o topo da hierarquia de parâmetros: assumem `activeTab` mesmo montando depois de\n // um controle que já o tivesse reivindicado, e o liberam ao desmontar.\n useEffect(() => {\n if (!syncWithUrl) return;\n\n const ownerId = urlParamOwnerIdRef.current;\n claimUrlParam(TABS_URL_PARAM, ownerId, URL_PARAM_PRIORITY.tabs);\n\n return () => releaseUrlParam(TABS_URL_PARAM, ownerId);\n }, [syncWithUrl]);\n\n useEffect(() => {\n if (syncWithUrl && searchParams.get(TABS_URL_PARAM) !== activeTab) {\n queueUrlParamUpdate(TABS_URL_PARAM, activeTab, setSearchParams);\n }\n const tabButton = document.querySelector(`li[data-value=${activeTab}]`) as HTMLElement | null;\n if (tabButton) {\n tabButton.click();\n }\n }, [activeTab]);\n\n const updateOverflow = useCallback(() => {\n const container = scrollRef.current;\n if (!container) return;\n\n setCanScrollLeft(container.scrollLeft > 4);\n setCanScrollRight(container.scrollLeft + container.clientWidth < container.scrollWidth - 4);\n\n const EDGE_TOLERANCE = 1;\n const containerRect = container.getBoundingClientRect();\n const clipped: Array<string> = [];\n container.querySelectorAll('[data-value]').forEach((node) => {\n const element = node as HTMLElement;\n const value = element.getAttribute('data-value');\n if (!value || value === activeTab) return;\n const rect = element.getBoundingClientRect();\n const crossesLeftEdge =\n rect.left < containerRect.left - EDGE_TOLERANCE && rect.right > containerRect.left + EDGE_TOLERANCE;\n const crossesRightEdge =\n rect.right > containerRect.right + EDGE_TOLERANCE && rect.left < containerRect.right - EDGE_TOLERANCE;\n if (crossesLeftEdge || crossesRightEdge) clipped.push(value);\n });\n setClippedTabValues((prev) =>\n prev.length === clipped.length && prev.every((value, index) => value === clipped[index]) ? prev : clipped,\n );\n }, [activeTab]);\n\n useEffect(() => {\n if (!stickyHeader) return;\n const sentinel = stickySentinelRef.current;\n if (!sentinel || typeof IntersectionObserver === 'undefined') return;\n // Descobre o ancestral rolável para observar o sentinela em relação a ele. Quando o sentinela\n // (logo acima da barra) sai do topo do container, a barra grudou e ligamos a sombra.\n let scrollParent = sentinel.parentElement;\n while (scrollParent) {\n const overflowY = getComputedStyle(scrollParent).overflowY;\n if (overflowY === 'auto' || overflowY === 'scroll') break;\n scrollParent = scrollParent.parentElement;\n }\n const observer = new IntersectionObserver(([entry]) => setIsStuck(!entry.isIntersecting), {\n root: scrollParent ?? null,\n threshold: 0,\n });\n observer.observe(sentinel);\n return () => observer.disconnect();\n }, [stickyHeader]);\n\n // Publica a altura da barra fixa como `--sticky-tabs-height` no container rolável, para\n // que conteúdo sticky mais abaixo (ex.: cabeçalho de material da ficha) possa grudar logo\n // abaixo da barra em vez de ficar escondido atrás dela.\n useEffect(() => {\n if (!stickyHeader) return;\n const bar = stickyBarRef.current;\n if (!bar) return;\n let scrollParent = bar.parentElement;\n while (scrollParent) {\n const overflowY = getComputedStyle(scrollParent).overflowY;\n if (overflowY === 'auto' || overflowY === 'scroll') break;\n scrollParent = scrollParent.parentElement;\n }\n if (!scrollParent) return;\n const target = scrollParent;\n let rafId = 0;\n let lastHeight = -1;\n // Escreve a variável só quando a altura muda de fato, e fora do ciclo de entrega do\n // ResizeObserver (via rAF): escrever um valor que afeta layout dentro do callback dispara\n // \"ResizeObserver loop completed with undelivered notifications\".\n const publishHeight = () => {\n const height = bar.offsetHeight;\n if (height === lastHeight) return;\n lastHeight = height;\n target.style.setProperty('--sticky-tabs-height', `${height}px`);\n };\n publishHeight();\n if (typeof ResizeObserver === 'undefined') return;\n const observer = new ResizeObserver(() => {\n cancelAnimationFrame(rafId);\n rafId = requestAnimationFrame(publishHeight);\n });\n observer.observe(bar);\n return () => {\n cancelAnimationFrame(rafId);\n observer.disconnect();\n target.style.removeProperty('--sticky-tabs-height');\n };\n }, [stickyHeader]);\n\n useEffect(() => {\n if (!scrollableHeader) return;\n updateOverflow();\n const onResize = () => updateOverflow();\n window.addEventListener('resize', onResize);\n return () => window.removeEventListener('resize', onResize);\n }, [scrollableHeader, tabs, updateOverflow]);\n\n const scrollByStep = (direction: number) => {\n const container = scrollRef.current;\n if (!container) return;\n const EDGE_TOLERANCE = 1;\n const containerRect = container.getBoundingClientRect();\n const nodes = Array.from(container.querySelectorAll('[data-value]')) as Array<HTMLElement>;\n\n if (direction > 0) {\n const nextHidden = nodes.find(\n (node) => node.getBoundingClientRect().right > containerRect.right + EDGE_TOLERANCE,\n );\n if (!nextHidden) return;\n container.scrollBy({ left: nextHidden.getBoundingClientRect().left - containerRect.left, behavior: 'smooth' });\n return;\n }\n\n const previousHidden = [...nodes]\n .reverse()\n .find((node) => node.getBoundingClientRect().left < containerRect.left - EDGE_TOLERANCE);\n if (!previousHidden) return;\n container.scrollBy({\n left: previousHidden.getBoundingClientRect().right - containerRect.right,\n behavior: 'smooth',\n });\n };\n\n const tabLabel = (label: string, count?: number, alertCount?: number) => {\n const showCount = count !== undefined;\n return (\n <div className=\"flex items-center gap-x-1.5 align-bottom\">\n {label}\n {loading && showCount ? (\n <LoadingSpinner size={3} />\n ) : showCount ? (\n <span className=\"inline-flex min-w-[18px] items-center justify-center rounded-full bg-muted-hover px-1.5 py-px text-[11px] font-medium leading-none text-foreground-subtle\">\n {formatNumber(count)}\n </span>\n ) : null}\n {Boolean(alertCount) && (\n <span className=\"inline-flex min-w-[18px] items-center justify-center rounded-full bg-warning-soft px-1.5 py-px text-[11px] font-semibold leading-none text-warning-emphasis\">\n {formatNumber(alertCount as number)}\n </span>\n )}\n </div>\n );\n };\n\n const handleTabDragEnd = () => {\n const from = dragTabValueRef.current;\n const to = dragOverTabValue;\n\n if (from && to && from !== to) {\n onReorderTab?.(from, to);\n }\n\n dragTabValueRef.current = null;\n setDraggingTabValue(null);\n setDragOverTabValue(null);\n };\n\n const tabElements = tabs.map((tab, index) => {\n const isActive = activeTab === tab.value && tab.isVisible !== false;\n const isInactive = activeTab !== tab.value && tab.isVisible !== false;\n const isTabDropTarget =\n Boolean(draggingTabValue) && dragOverTabValue === tab.value && draggingTabValue !== tab.value;\n const tabDragProps = onReorderTab\n ? {\n draggable: true,\n onDragStart: (event: React.DragEvent) => {\n // Arrastar a partir do input de renomear deve selecionar texto, não mover a aba; e a\n // partir do slot de ações (menu da aba) não deve mover nada.\n if ((event.target as HTMLElement).closest('input, [data-tab-trailing-slot]')) {\n event.preventDefault();\n return;\n }\n event.dataTransfer.effectAllowed = 'move';\n dragTabValueRef.current = tab.value;\n setDraggingTabValue(tab.value);\n },\n onDragEnter: (event: React.DragEvent) => {\n event.preventDefault();\n if (dragTabValueRef.current) setDragOverTabValue(tab.value);\n },\n onDragOver: (event: React.DragEvent) => event.preventDefault(),\n onDragEnd: handleTabDragEnd,\n }\n : {};\n return (\n <Tab\n key={`controlled-tab-${index}-${tab.value}`}\n value={tab.value}\n style={{ zIndex: 2 }}\n disabled={loading}\n onClick={() => handleActiveTab(tab.value)}\n {...tabDragProps}\n className={classNames(\n 'max-w-fit px-4',\n tabClassName,\n // Acabamento mais delicado quando a barra é sticky: pill sutil no hover das abas\n // inativas e peso de fonte mais leve ao trocar de aba.\n stickyHeader && 'rounded-md transition-interactive duration-fast',\n {\n [stickyHeader ? 'font-medium text-brand-emphasis' : 'text-brand-emphasis']: isActive,\n [stickyHeader ? 'font-normal opacity-70 hover:bg-muted-hover hover:opacity-100' : 'opacity-70']:\n isInactive,\n ['!text-foreground-faint line-through opacity-60 hover:!text-foreground-faint']: tab.isVisible === false,\n ['cursor-grab active:cursor-grabbing']: Boolean(onReorderTab),\n ['rounded-md bg-brand-soft !opacity-100 ring-1 ring-brand-border']: isTabDropTarget,\n ['opacity-40']: draggingTabValue === tab.value,\n ['invisible']: clippedTabValues.includes(tab.value),\n },\n )}\n >\n {onRenameTab && tab.editable !== false ? (\n <EditableTabLabel\n label={tab.label}\n count={tab.count}\n loading={loading}\n isVisible={tab.isVisible !== false}\n canToggleVisibility={tab.isVisible === false || tabs.filter((t) => t.isVisible !== false).length > 1}\n onRename={(newLabel) => onRenameTab(tab.value, newLabel)}\n onToggleVisibility={onToggleTabVisibility ? () => handleToggleTabVisibility(tab.value) : undefined}\n />\n ) : tab.tooltip ? (\n <ControlledTooltip content={tab.tooltip} className=\"max-w-60\">\n {tabLabel(tab.label, tab.count, tab.alertCount)}\n </ControlledTooltip>\n ) : (\n tabLabel(tab.label, tab.count, tab.alertCount)\n )}\n {tab.trailingSlot && (\n <span\n data-tab-trailing-slot\n className=\"ml-1 flex items-center\"\n onClick={(event) => event.stopPropagation()}\n onMouseDown={(event) => event.stopPropagation()}\n onPointerDown={(event) => event.stopPropagation()}\n onKeyDown={(event) => event.stopPropagation()}\n >\n {tab.trailingSlot}\n </span>\n )}\n </Tab>\n );\n });\n\n // Sublinhado fino (2px). Na barra sticky mantemos a animação de deslize nativa (transform) com\n // transição suave de ~120ms; nas demais telas o indicador segue estático como antes.\n\n const scrollArrowClass =\n 'flex size-8 shrink-0 items-center justify-center rounded-full text-foreground-subtle transition-colors hover:bg-brand-soft hover:text-brand-emphasis';\n\n return (\n <TabsRoot value={activeTab} className={classNames('!overflow-visible', rootClassName)}>\n {/* Sentinela de 1px: quando sai do topo do container rolável a barra já grudou e acende a sombra. */}\n {stickyHeader && <div ref={stickySentinelRef} className=\"h-px\" aria-hidden />}\n <div\n ref={stickyBarRef}\n className={classNames(\n 'flex shrink-0 items-center',\n // Com header rolável a barra é sempre UMA linha (nowrap): as abas cedem e viram scroll\n // horizontal, enquanto as ações (shrink-0 + ml-auto) permanecem pinadas à direita e nunca\n // caem para baixo. Sem header rolável, mantém o comportamento antigo de wrap.\n scrollableHeader ? 'flex-nowrap gap-x-3' : actionsSlot ? 'flex-wrap gap-x-3 gap-y-2' : 'gap-1',\n // Gruda logo abaixo do header do produto: fundo sólido para tapar o conteúdo que\n // rola por baixo e sombra sutil só depois que a barra encosta no topo.\n stickyHeader && 'sticky top-0 z-20 bg-surface transition-shadow duration-fast',\n stickyHeader && isStuck && 'shadow-elevation1',\n disabled && 'pointer-events-none select-none opacity-50',\n )}\n >\n {headerSlot && <>{headerSlot}</>}\n {scrollableHeader ? (\n <div className=\"flex min-w-0 flex-1 items-center gap-1 border-b border-border-subtle\">\n <ControlledButton\n defaultHtmlButton\n onClick={() => scrollByStep(-1)}\n disabled={!canScrollLeft}\n className={classNames(scrollArrowClass, { ['pointer-events-none opacity-0']: !canScrollLeft })}\n tabIndex={-1}\n >\n <MaterialSymbol icon=\"chevron_left\" size={20} />\n </ControlledButton>\n\n <div ref={scrollRef} onScroll={updateOverflow} className=\"scrollbar-hide min-w-0 flex-1 overflow-x-auto\">\n <TabsHeader className=\"flex w-max flex-nowrap items-center gap-1 rounded-none border-b-0 border-border-subtle bg-transparent p-0\">\n {tabElements}\n </TabsHeader>\n </div>\n\n <ControlledButton\n defaultHtmlButton\n onClick={() => scrollByStep(1)}\n disabled={!canScrollRight}\n className={classNames(scrollArrowClass, { ['pointer-events-none opacity-0']: !canScrollRight })}\n tabIndex={-1}\n >\n <MaterialSymbol icon=\"chevron_right\" size={20} />\n </ControlledButton>\n </div>\n ) : (\n <TabsHeader className=\"flex flex-1 flex-wrap items-center gap-1 rounded-none border-b-0 border-border-subtle bg-transparent p-0\">\n {tabElements}\n </TabsHeader>\n )}\n {actionsSlot && <div className=\"ml-auto flex shrink-0 items-center\">{actionsSlot}</div>}\n </div>\n\n <TabsBody className={tabsBodyClassName}>\n {dataTabs.map((tabPanel) => {\n const isTabVisible = tabs.find((t) => t.value === tabPanel.key)?.isVisible !== false;\n return (\n <TabPanel key={tabPanel.key} value={tabPanel.key} className={classNames('size-full', tabPanelClassName)}>\n <div className={classNames('relative size-full', { ['pointer-events-none']: !isTabVisible })}>\n {useRenderValidation\n ? activeTab === tabPanel.key && (\n <div className=\"size-full animate-fade-in transition-interactive duration-slow ease-standard\">\n {tabPanel.element}\n </div>\n )\n : tabPanel.element}\n {!isTabVisible && (\n <div className=\"absolute inset-0 z-10 flex flex-col items-center justify-center gap-2 rounded-lg bg-muted-hover/70 glass-veil\">\n <MaterialSymbol icon=\"visibility_off\" size={32} className=\"text-foreground-faint\" />\n <span className=\"text-sm font-medium text-foreground-subtle\">\n {t('controlledTabs.tabDisabled')}\n </span>\n {onToggleTabVisibility && (\n <ControlledButton\n size=\"sm\"\n variant=\"outlined\"\n className=\"pointer-events-auto mt-1 flex items-center gap-x-1\"\n onClick={() => onToggleTabVisibility(tabPanel.key)}\n >\n <MaterialSymbol icon=\"visibility\" size={16} />\n {t('controlledTabs.enableTab')}\n </ControlledButton>\n )}\n </div>\n )}\n </div>\n </TabPanel>\n );\n })}\n </TabsBody>\n </TabsRoot>\n );\n },\n);\n\nexport default ControlledTabs;\n"],"names":["t"],"mappings":"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"}
@@ -46,18 +46,20 @@ interface IPageTitleProps {
46
46
  */
47
47
  titleMediaSize?: 'sm' | 'md' | 'lg';
48
48
  /**
49
- * The header pins itself to the top of its scroll container and, once pinned, shrinks: the
50
- * breadcrumb, the eyebrow, the headline and the `subtitleSlot` go, the media drops one size (the
51
- * record's picture stays, at the icon chip's size) and the title takes its compact type. The back
52
- * button and the `slot` never move.
49
+ * The header shrinks once the content beside it has been scrolled: the breadcrumb, the eyebrow, the
50
+ * headline and the `subtitleSlot` go, the media drops one size (the record's picture stays, at the
51
+ * icon chip's size) and the title takes its compact type. The back button and the `slot` never move.
53
52
  *
54
- * EXPANDED, IT RENDERS EXACTLY WHAT IT RENDERED BEFORE THIS EXISTED: no wrapper node, no extra
55
- * element, no background. That is deliberate. The first version added a sentinel node as a sibling
56
- * of the header, and since almost every page wraps it in a `flex flex-col gap-y-*`, that node
57
- * became a flex item and every page grew a stray gap.
53
+ * It only applies where the header STAYS ON SCREEN while something else scrolls, which is a page
54
+ * whose content lives in a container beside the header (`collapseScrollRef`). When the page itself
55
+ * scrolls, the header goes with it and there is nothing to shrink: pinning it there would mean
56
+ * painting a background under it, and a header that grows a surface halfway through a scroll reads
57
+ * as a panel appearing out of nowhere.
58
58
  *
59
- * On by default. Turn it off for a header that must keep its full identity on screen, or one that
60
- * is not at the top of a scrolling page (inside a card, a dialog or a print sheet).
59
+ * EXPANDED, IT RENDERS EXACTLY WHAT IT RENDERED BEFORE THIS EXISTED: no wrapper node, no extra
60
+ * element, no background, no positioning. That is deliberate. The first version added a sentinel
61
+ * node as a sibling of the header, and since almost every page wraps it in a `flex flex-col
62
+ * gap-y-*`, that node became a flex item and every page grew a stray gap.
61
63
  */
62
64
  collapseOnScroll?: boolean;
63
65
  /**
@@ -20,7 +20,6 @@ const MEDIA_ICON_SIZE = { sm: 18, md: 22, lg: 34 };
20
20
  const COLLAPSED_MEDIA_SIZE = { sm: "sm", md: "sm", lg: "md" };
21
21
  const COLLAPSE_AT_SCROLL = 48;
22
22
  const EXPAND_AT_SCROLL = 16;
23
- const PINNED_TOLERANCE_PX = 1;
24
23
  const findScrollParent = (node) => {
25
24
  let current = node?.parentElement ?? null;
26
25
  while (current) {
@@ -103,20 +102,12 @@ const PageTitle = ({
103
102
  const root = rootRef.current;
104
103
  if (!root) return;
105
104
  const container = collapseScrollRef?.current ?? findScrollParent(root);
106
- if (!container) return;
107
- const scrollsUnderTheHeader = container.contains(root);
108
- const containerPaddingTop = Number.parseFloat(getComputedStyle(container).paddingTop) || 0;
109
- const stickyTop = Number.parseFloat(getComputedStyle(root).top) || 0;
105
+ if (!container || container.contains(root)) return;
110
106
  let frame = 0;
111
107
  const evaluate = () => {
112
108
  frame = 0;
113
- if (!scrollsUnderTheHeader) {
114
- const offset = container.scrollTop;
115
- setIsCollapsed((previous) => previous ? offset > EXPAND_AT_SCROLL : offset >= COLLAPSE_AT_SCROLL);
116
- return;
117
- }
118
- const pinnedLine = container.getBoundingClientRect().top + containerPaddingTop + stickyTop;
119
- setIsCollapsed(root.getBoundingClientRect().top <= pinnedLine + PINNED_TOLERANCE_PX);
109
+ const offset = container.scrollTop;
110
+ setIsCollapsed((previous) => previous ? offset > EXPAND_AT_SCROLL : offset >= COLLAPSE_AT_SCROLL);
120
111
  };
121
112
  const handleScroll = () => {
122
113
  if (!frame) frame = requestAnimationFrame(evaluate);
@@ -143,14 +134,10 @@ const PageTitle = ({
143
134
  "flex flex-col",
144
135
  {
145
136
  "zIndexMax fixed top-16 mr-7": canFloat,
146
- // Pins under the app header, which publishes its own height; hosts without the shell stick
147
- // at the top of their own scroll container. `sticky` is inert until it pins, so this costs
148
- // nothing on a page that does not scroll.
149
- "sticky top-[var(--page-header-sticky-top,0px)] z-30": collapses,
150
- // The page's own background, and nothing else. Pinned, the header has to be opaque or the
151
- // content slides visibly through the title; but it is not a PANEL, so no glass, no border
152
- // and no shadow, which read as a second surface appearing out of nowhere mid-scroll.
153
- "bg-canvas": collapses && isCollapsed
137
+ // Collapsed it keeps NO background: the header never has content passing behind it, so
138
+ // painting one would only add a panel nobody asked for. The padding is what keeps the
139
+ // shrunken row off the content below instead.
140
+ "py-2": isCollapsed
154
141
  },
155
142
  className
156
143
  ),
@@ -1 +1 @@
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 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\n/** Tolerance for the pinned test: sub-pixel layout would otherwise flip the state on its own. */\nconst PINNED_TOLERANCE_PX = 1;\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 pins itself to the top of its scroll container and, once pinned, shrinks: the\n * breadcrumb, the eyebrow, the headline and the `subtitleSlot` go, the media drops one size (the\n * record's picture stays, at the icon chip's size) and the title takes its compact type. The back\n * button and the `slot` never move.\n *\n * EXPANDED, IT RENDERS EXACTLY WHAT IT RENDERED BEFORE THIS EXISTED: no wrapper node, no extra\n * element, no background. That is deliberate. The first version added a sentinel node as a sibling\n * of the header, and since almost every page wraps it in a `flex flex-col gap-y-*`, that node\n * became a flex item and every page grew a stray gap.\n *\n * On by default. Turn it off for a header that must keep its full identity on screen, or one that\n * 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 rootRef = 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 const root = rootRef.current;\n if (!root) return;\n\n const container = collapseScrollRef?.current ?? findScrollParent(root);\n if (!container) return;\n\n // Two shapes, one listener. When the container is an ANCESTOR the header travels with the page,\n // so being pinned is a question about its position; when it is a SIBLING (a split layout, where\n // the header never moves) its position says nothing and the scroll offset is all there is.\n const scrollsUnderTheHeader = container.contains(root);\n\n // Read once: both are static for the life of the listener, and reading them per frame would mean\n // a style recalculation on every scroll tick. A sticky offset is measured from the container's\n // CONTENT box, so the pinned line is its padding plus that offset, never the border box alone.\n const containerPaddingTop = Number.parseFloat(getComputedStyle(container).paddingTop) || 0;\n const stickyTop = Number.parseFloat(getComputedStyle(root).top) || 0;\n let frame = 0;\n\n const evaluate = () => {\n frame = 0;\n\n if (!scrollsUnderTheHeader) {\n const offset = container.scrollTop;\n setIsCollapsed((previous) => (previous ? offset > EXPAND_AT_SCROLL : offset >= COLLAPSE_AT_SCROLL));\n return;\n }\n\n const pinnedLine = container.getBoundingClientRect().top + containerPaddingTop + stickyTop;\n setIsCollapsed(root.getBoundingClientRect().top <= pinnedLine + PINNED_TOLERANCE_PX);\n };\n\n const handleScroll = () => {\n if (!frame) frame = requestAnimationFrame(evaluate);\n };\n\n evaluate();\n container.addEventListener('scroll', handleScroll, { passive: true });\n return () => {\n container.removeEventListener('scroll', handleScroll);\n if (frame) cancelAnimationFrame(frame);\n };\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 <div\n ref={rootRef}\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 stick\n // at the top of their own scroll container. `sticky` is inert until it pins, so this costs\n // nothing on a page that does not scroll.\n 'sticky top-[var(--page-header-sticky-top,0px)] z-30': collapses,\n // The page's own background, and nothing else. Pinned, the header has to be opaque or the\n // content slides visibly through the title; but it is not a PANEL, so no glass, no border\n // and no shadow, which read as a second surface appearing out of nowhere mid-scroll.\n 'bg-canvas': collapses && isCollapsed,\n },\n className,\n )}\n style={{\n width: canFloat ? (isFirefox() ? '-moz-available' : '-webkit-fill-available') : '100%',\n }}\n >\n {resolvedBreadcrumb && !isCollapsed && <div className=\"mb-2 shrink-0\">{resolvedBreadcrumb}</div>}\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 && !isCollapsed && (\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 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 && !isCollapsed && <span className=\"mt-0.5 text-xs text-foreground-subtle\">{headline}</span>}\n\n {subtitleSlot && !isCollapsed && <div className=\"mt-1 flex min-w-0 flex-col gap-1\">{subtitleSlot}</div>}\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 <hr className={classNames('border-border-subtle', isCompactLayout ? 'mb-2 mt-2' : 'mb-2 mt-3')} />\n )}\n </div>\n </PageTitleCollapseProvider>\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 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 * Scroll offsets, in px, at which the header shrinks and grows back. They differ on purpose:\n * collapsing and expanding at the same position makes the header flicker for a whole gesture when a\n * 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 content beside it has been scrolled: the breadcrumb, the eyebrow, the\n * headline and the `subtitleSlot` go, the media drops one size (the record's picture stays, at the\n * icon chip's size) and the title takes its compact type. The back button and the `slot` never move.\n *\n * It only applies where the header STAYS ON SCREEN while something else scrolls, which is a page\n * whose content lives in a container beside the header (`collapseScrollRef`). When the page itself\n * scrolls, the header goes with it and there is nothing to shrink: pinning it there would mean\n * painting a background under it, and a header that grows a surface halfway through a scroll reads\n * as a panel appearing out of nowhere.\n *\n * EXPANDED, IT RENDERS EXACTLY WHAT IT RENDERED BEFORE THIS EXISTED: no wrapper node, no extra\n * element, no background, no positioning. That is deliberate. The first version added a sentinel\n * node as a sibling of the header, and since almost every page wraps it in a `flex flex-col\n * gap-y-*`, that node became a flex item and every page grew a stray gap.\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 rootRef = 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 const root = rootRef.current;\n if (!root) return;\n\n const container = collapseScrollRef?.current ?? findScrollParent(root);\n // A container that CONTAINS the header is the page scrolling, header included: it leaves with\n // the content, so there is nothing to shrink. Shrinking only means something where the header\n // stays put while the content moves, which is a container beside it, never around it.\n if (!container || container.contains(root)) return;\n\n let frame = 0;\n\n const evaluate = () => {\n frame = 0;\n const offset = container.scrollTop;\n setIsCollapsed((previous) => (previous ? offset > EXPAND_AT_SCROLL : offset >= COLLAPSE_AT_SCROLL));\n };\n\n const handleScroll = () => {\n if (!frame) frame = requestAnimationFrame(evaluate);\n };\n\n evaluate();\n container.addEventListener('scroll', handleScroll, { passive: true });\n return () => {\n container.removeEventListener('scroll', handleScroll);\n if (frame) cancelAnimationFrame(frame);\n };\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 <div\n ref={rootRef}\n className={classNames(\n 'flex flex-col',\n {\n 'zIndexMax fixed top-16 mr-7': canFloat,\n // Collapsed it keeps NO background: the header never has content passing behind it, so\n // painting one would only add a panel nobody asked for. The padding is what keeps the\n // shrunken row off the content below instead.\n 'py-2': isCollapsed,\n },\n className,\n )}\n style={{\n width: canFloat ? (isFirefox() ? '-moz-available' : '-webkit-fill-available') : '100%',\n }}\n >\n {resolvedBreadcrumb && !isCollapsed && <div className=\"mb-2 shrink-0\">{resolvedBreadcrumb}</div>}\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 && !isCollapsed && (\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 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 && !isCollapsed && <span className=\"mt-0.5 text-xs text-foreground-subtle\">{headline}</span>}\n\n {subtitleSlot && !isCollapsed && <div className=\"mt-1 flex min-w-0 flex-col gap-1\">{subtitleSlot}</div>}\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 <hr className={classNames('border-border-subtle', isCompactLayout ? 'mb-2 mt-2' : 'mb-2 mt-3')} />\n )}\n </div>\n </PageTitleCollapseProvider>\n );\n};\n\nexport default PageTitle;\n"],"names":[],"mappings":";;;;;;;;;;;;;AAwBA,MAAM,sBAA0D;AAAA,EAC9D,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAEA,MAAM,kBAAsD,EAAE,IAAI,IAAI,IAAI,IAAI,IAAI,GAAA;AAGlF,MAAM,uBAAuE,EAAE,IAAI,MAAM,IAAI,MAAM,IAAI,KAAA;AAOvG,MAAM,qBAAqB;AAC3B,MAAM,mBAAmB;AAEzB,MAAM,mBAAmB,CAAC,SAAiD;AACzE,MAAI,UAAU,MAAM,iBAAiB;AACrC,SAAO,SAAS;AACd,UAAM,EAAE,UAAA,IAAc,iBAAiB,OAAO;AAC9C,QAAI,cAAc,UAAU,cAAc,SAAU,QAAO;AAC3D,cAAU,QAAQ;AAAA,EACpB;AACA,SAAO;AACT;AAiFA,MAAM,YAAuC,CAAC;AAAA,EAC5C,UAAU;AAAA,EACV,WAAW;AAAA,EACX;AAAA,EACA,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ;AAAA,EACA;AAAA,EACA;AAAA,EACA,iBAAiB;AAAA,EACjB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,mBAAmB;AAAA,EACnB;AAAA,EACA,WAAW;AAAA,EACX,UAAU;AAAA,EACV,kBAAkB;AAAA,EAClB,cAAc;AAAA,EACd;AAAA,EACA;AAAA,EACA;AAAA,EACA,cAAc;AAChB,MAAM;AACJ,QAAM,EAAE,EAAA,IAAM,eAAe,IAAI;AACjC,QAAM,EAAE,UAAU,QAAQ,SAAA,IAAa,gBAAA;AAEvC,QAAM,YAAY,MAAM,OAAO,cAAc,eAAe,UAAU,UAAU,YAAA,EAAc,SAAS,SAAS;AAEhH,QAAM,aAAa,MAAM;AACvB,QAAI,YAAa;AAEjB,QAAI,aAAa;AACf,kBAAA;AAAA,IACF,WAAW,WAAW;AACpB,eAAS,SAAS;AAAA,IACpB,OAAO;AACL,aAAA;AAAA,IACF;AAAA,EACF;AAEA,QAAM,EAAE,aAAA,IAAiB,uBAAA;AAEzB,QAAM,YAAY,QAAQ,MAAM;AAC9B,UAAM,aAAc,kBAAkB,CAAC,cAAc,CAAC,YAAc,YAAY,CAAC,aAAa,CAAC;AAC/F,WAAO,aAAa,aAAa,SAAS,QAAQ,IAAI,CAAA;AAAA,EACxD,GAAG,CAAC,gBAAgB,YAAY,UAAU,UAAU,WAAW,YAAY,SAAS,UAAU,YAAY,CAAC;AAE3G,QAAM,qBACJ,eACC,kBAAkB,CAAC,YAAY,UAAU,UAAU,IAClD;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,WAAW,EAAE,2BAA2B;AAAA,MACxC,OAAO,UAAU,IAAI,CAAC,MAAM,WAAW;AAAA,QACrC,KAAK,KAAK;AAAA,QACV,OAAO,KAAK;AAAA,QACZ,IAAI,QAAQ,UAAU,SAAS,IAAI,KAAK,KAAK;AAAA,MAAA,EAC7C;AAAA,IAAA;AAAA,EAAA,IAEF;AAEN,QAAM,oBAAoB,cAAc,WAAW,UAAU,UAAU,SAAS,CAAC,GAAG,OAAO;AAG3F,QAAM,CAAC,qBAAqB,sBAAsB,IAAI,SAAwB,IAAI;AAClF,QAAM,oBAAoB,QAAQ,YAAY,GAAG,KAAK,YAAY,QAAQ;AAE1E,QAAM,UAAU,OAAuB,IAAI;AAC3C,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,KAAK;AAEpD,QAAM,YAAY,oBAAoB,CAAC;AAEvC,YAAU,MAAM;AACd,QAAI,CAAC,WAAW;AACd,qBAAe,KAAK;AACpB;AAAA,IACF;AAEA,UAAM,OAAO,QAAQ;AACrB,QAAI,CAAC,KAAM;AAEX,UAAM,YAAY,mBAAmB,WAAW,iBAAiB,IAAI;AAIrE,QAAI,CAAC,aAAa,UAAU,SAAS,IAAI,EAAG;AAE5C,QAAI,QAAQ;AAEZ,UAAM,WAAW,MAAM;AACrB,cAAQ;AACR,YAAM,SAAS,UAAU;AACzB,qBAAe,CAAC,aAAc,WAAW,SAAS,mBAAmB,UAAU,kBAAmB;AAAA,IACpG;AAEA,UAAM,eAAe,MAAM;AACzB,UAAI,CAAC,MAAO,SAAQ,sBAAsB,QAAQ;AAAA,IACpD;AAEA,aAAA;AACA,cAAU,iBAAiB,UAAU,cAAc,EAAE,SAAS,MAAM;AACpE,WAAO,MAAM;AACX,gBAAU,oBAAoB,UAAU,YAAY;AACpD,UAAI,4BAA4B,KAAK;AAAA,IACvC;AAAA,EACF,GAAG,CAAC,WAAW,iBAAiB,CAAC;AAEjC,QAAM,kBAAkB,WAAW;AACnC,QAAM,oBAAoB,mBAAmB,UAAU,OAAO;AAC9D,QAAM,YAAY,cAAc,qBAAqB,iBAAiB,IAAI;AAC1E,QAAM,oBAAoB,oBAAoB,SAAS;AACvD,QAAM,gBAAgB,gBAAgB,SAAS;AAC/C,QAAM,2BAA2B;AACjC,QAAM,gBAAgB,QAAQ,OAAO,EAAE,gBAAgB,CAAC,WAAW,CAAC;AAEpE,SACE,oBAAC,2BAAA,EAA0B,OAAO,eAChC,UAAA;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,KAAK;AAAA,MACL,WAAW;AAAA,QACT;AAAA,QACA;AAAA,UACE,+BAA+B;AAAA;AAAA;AAAA;AAAA,UAI/B,QAAQ;AAAA,QAAA;AAAA,QAEV;AAAA,MAAA;AAAA,MAEF,OAAO;AAAA,QACL,OAAO,WAAY,cAAc,mBAAmB,2BAA4B;AAAA,MAAA;AAAA,MAGjF,UAAA;AAAA,QAAA,sBAAsB,CAAC,eAAe,oBAAC,OAAA,EAAI,WAAU,iBAAiB,UAAA,oBAAmB;AAAA,QAE1F;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,WAAW,WAAW,yDAAyD;AAAA,cAC7E,eAAe,cAAc;AAAA,cAC7B,gBAAgB,cAAc;AAAA,cAC9B,mBAAmB;AAAA,YAAA,CACpB;AAAA,YAED,UAAA;AAAA,cAAA,qBAAC,SAAI,WAAW,WAAW,6CAA6C,kBAAkB,UAAU,OAAO,GACxG,UAAA;AAAA,gBAAA;AAAA,gBAEA,WACC,oBAAC,mBAAA,EAAkB,SAAS,EAAE,cAAc,GAC1C,UAAA;AAAA,kBAAC;AAAA,kBAAA;AAAA,oBACC,mBAAiB;AAAA,oBACjB,SAAS;AAAA,oBACT,UAAU;AAAA,oBACV,WAAW;AAAA,sBACT;AAAA,sBACA,kBAAkB,WAAW;AAAA,oBAAA;AAAA,oBAG/B,8BAAC,gBAAA,EAAe,MAAK,cAAa,MAAM,kBAAkB,KAAK,GAAA,CAAI;AAAA,kBAAA;AAAA,gBAAA,GAEvE;AAAA,gBAGD,qBAAqB,aACpB;AAAA,kBAAC;AAAA,kBAAA;AAAA,oBACC,KAAK,WAAW;AAAA,oBAChB,KAAK,WAAW;AAAA,oBAChB,SAAS,MAAM,uBAAuB,WAAW,GAAG;AAAA,oBACpD,WAAW;AAAA,sBACT;AAAA,sBACA;AAAA,sBACA;AAAA,sBACA,WAAW;AAAA,oBAAA;AAAA,kBACb;AAAA,gBAAA,IAGF,qBACE;AAAA,kBAAC;AAAA,kBAAA;AAAA,oBACC,WAAW;AAAA,sBACT;AAAA,sBACA;AAAA,sBACA;AAAA,sBACA,sBAAsB;AAAA,oBAAA;AAAA,oBAGxB,UAAA,oBAAC,kBAAe,MAAM,mBAAmB,MAAM,eAAe,MAAI,MAAC,WAAU,eAAA,CAAe;AAAA,kBAAA;AAAA,gBAAA;AAAA,gBAKlG,qBAAC,OAAA,EAAI,WAAU,gCACZ,UAAA;AAAA,kBAAA,WAAW,CAAC,eACX,oBAAC,QAAA,EAAK,WAAU,gFACb,UAAA,SACH;AAAA,kBAGF,qBAAC,OAAA,EAAI,WAAU,mCACZ,UAAA;AAAA,oBAAA,gBACC;AAAA,sBAAC;AAAA,sBAAA;AAAA,wBACC,WAAW;AAAA,0BACT;AAAA,0BACA,kBACI,0CACA;AAAA,0BACJ;AAAA,wBAAA;AAAA,wBAGD,UAAA,kBAAkB,qBAAqB,KAAK,IAAI;AAAA,sBAAA;AAAA,oBAAA;AAAA,oBAIpD;AAAA,kBAAA,GACH;AAAA,kBAEC,YAAY,CAAC,mCAAgB,QAAA,EAAK,WAAU,yCAAyC,UAAA,UAAS;AAAA,kBAE9F,gBAAgB,CAAC,mCAAgB,OAAA,EAAI,WAAU,oCAAoC,UAAA,aAAA,CAAa;AAAA,gBAAA,EAAA,CACnG;AAAA,cAAA,GACF;AAAA,cAEC,QACC;AAAA,gBAAC;AAAA,gBAAA;AAAA,kBACC,WAAW;AAAA,oBACT;AAAA,oBACA,cAAc,UAAU,gBAAgB;AAAA,kBAAA;AAAA,kBAGzC,UAAA;AAAA,gBAAA;AAAA,cAAA;AAAA,YACH;AAAA,UAAA;AAAA,QAAA;AAAA,QAGH,eAAe,CAAC,aAAa,MAAM,SAAS,KAAK,QAAQ,YAAY,MACpE,oBAAC,QAAG,WAAW,WAAW,wBAAwB,kBAAkB,cAAc,WAAW,EAAA,CAAG;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA,GAGtG;AAEJ;"}
@@ -84,14 +84,8 @@ const AppShell = ({
84
84
  {
85
85
  id: "scrollableDiv",
86
86
  ref: scrollRef,
87
- className: "z-30 h-screen w-full px-4 tablet:px-6",
88
- style: {
89
- overflow: "auto",
90
- paddingTop: "var(--page-scroll-padding-top)",
91
- ["--page-scroll-padding-top"]: "6rem",
92
- ["--app-header-height"]: "4rem",
93
- ["--page-header-sticky-top"]: "calc(var(--app-header-height) - var(--page-scroll-padding-top))"
94
- },
87
+ className: "z-30 h-screen w-full px-4 pt-24 tablet:px-6",
88
+ style: { overflow: "auto" },
95
89
  children
96
90
  }
97
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 tablet:px-6\"\n /*\n * Where a PageTitle pins itself when the page scrolls. A sticky offset is measured from this\n * container's CONTENT box, so the padding that clears the fixed app header has to be\n * subtracted, or the header ends up pushed DOWN by the app header's height and covers the\n * content below it before the page is even scrolled. The padding moves into the same object\n * as the two numbers it depends on, so the relation between them stays visible.\n */\n style={{\n overflow: 'auto',\n paddingTop: 'var(--page-scroll-padding-top)',\n ['--page-scroll-padding-top' as string]: '6rem',\n ['--app-header-height' as string]: '4rem',\n ['--page-header-sticky-top' as string]: 'calc(var(--app-header-height) - var(--page-scroll-padding-top))',\n }}\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,cAQV,OAAO;AAAA,gBACL,UAAU;AAAA,gBACV,YAAY;AAAA,gBACZ,CAAC,2BAAqC,GAAG;AAAA,gBACzC,CAAC,qBAA+B,GAAG;AAAA,gBACnC,CAAC,0BAAoC,GAAG;AAAA,cAAA;AAAA,cAGzC;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 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;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umodeapporg/ui",
3
- "version": "0.9.2",
3
+ "version": "0.10.0",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "sideEffects": [