@umodeapporg/ui 0.13.2 → 0.14.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.
@@ -1,9 +1,28 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { useMemo } from "react";
2
3
  import classNames from "classnames";
3
4
  import { useTranslation } from "react-i18next";
4
5
  import ControlledButton from "../Form/ControlledButton.js";
5
6
  import { MaterialSymbol } from "../UI/MaterialSymbol/index.js";
7
+ import { SidebarFavoritesProvider, useSidebarFavorites, buildFavoriteMenuItems } from "./sidebarFavorites.js";
6
8
  import SidebarMenu from "./SidebarMenu.js";
9
+ const SidebarMenuWithFavorites = ({ items, collapsed, onNavigate }) => {
10
+ const { favoriteKeys } = useSidebarFavorites();
11
+ const menuItems = useMemo(
12
+ () => [...buildFavoriteMenuItems(items, favoriteKeys), ...items ?? []],
13
+ [favoriteKeys, items]
14
+ );
15
+ return /* @__PURE__ */ jsx(
16
+ SidebarMenu,
17
+ {
18
+ items: menuItems,
19
+ collapsed,
20
+ activeClass: "text-brand-emphasis bg-brand-soft",
21
+ activeIconClass: "text-brand-emphasis",
22
+ onNavigate
23
+ }
24
+ );
25
+ };
7
26
  const AppSidebar = ({
8
27
  items,
9
28
  isMobile,
@@ -31,16 +50,7 @@ const AppSidebar = ({
31
50
  }
32
51
  ),
33
52
  children: /* @__PURE__ */ jsxs("nav", { className: "sidebar-scroll flex h-full flex-col justify-between gap-y-5 overflow-y-auto overflow-x-hidden px-3 py-5 pt-20", children: [
34
- /* @__PURE__ */ jsx(
35
- SidebarMenu,
36
- {
37
- items,
38
- collapsed,
39
- activeClass: "text-brand-emphasis bg-brand-soft",
40
- activeIconClass: "text-brand-emphasis",
41
- onNavigate
42
- }
43
- ),
53
+ /* @__PURE__ */ jsx(SidebarFavoritesProvider, { children: /* @__PURE__ */ jsx(SidebarMenuWithFavorites, { items, collapsed, onNavigate }) }),
44
54
  onLogout && /* @__PURE__ */ jsx("div", { className: "text-foreground-muted", children: /* @__PURE__ */ jsxs(
45
55
  ControlledButton,
46
56
  {
@@ -1 +1 @@
1
- {"version":3,"file":"AppSidebar.js","sources":["../../../src/components/Shell/AppSidebar.tsx"],"sourcesContent":["import classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\n\nimport ControlledButton from '@/components/Form/ControlledButton';\nimport { MaterialSymbol } from '@/components/UI/MaterialSymbol';\n\nimport SidebarMenu, { ISidebarMenuItem } from './SidebarMenu';\n\ninterface IAppSidebarProps {\n items: Array<ISidebarMenuItem>;\n isMobile: boolean;\n isVisible: boolean;\n collapsed: boolean;\n isFloating: boolean;\n onHoverEnter: () => void;\n onHoverLeave: () => void;\n onNavigate?: () => void;\n /** Hides the footer sign-out row when absent. */\n onLogout?: () => void;\n}\n\nconst AppSidebar: React.FC<IAppSidebarProps> = ({\n items,\n isMobile,\n isVisible,\n collapsed,\n isFloating,\n onHoverEnter,\n onHoverLeave,\n onNavigate,\n onLogout,\n}) => {\n const { t } = useTranslation('ui');\n\n return (\n <aside\n onMouseEnter={onHoverEnter}\n onMouseLeave={onHoverLeave}\n className={classNames(\n 'sidebar-aside fixed left-0 top-0 z-40 h-full overflow-hidden border-r border-border bg-muted',\n {\n 'is-rail': !isMobile && collapsed,\n 'is-mobile': isMobile,\n 'is-floating': isFloating,\n 'is-hidden pointer-events-none': isMobile && !isVisible,\n },\n )}\n >\n <nav className=\"sidebar-scroll flex h-full flex-col justify-between gap-y-5 overflow-y-auto overflow-x-hidden px-3 py-5 pt-20\">\n <SidebarMenu\n items={items}\n collapsed={collapsed}\n activeClass=\"text-brand-emphasis bg-brand-soft\"\n activeIconClass=\"text-brand-emphasis\"\n onNavigate={onNavigate}\n />\n\n {onLogout && (\n <div className=\"text-foreground-muted\">\n <ControlledButton\n defaultHtmlButton\n onClick={onLogout}\n className=\"flex h-9 w-full items-center gap-x-3 rounded-lg px-3 text-left text-foreground-muted transition-colors duration-base hover:bg-muted-hover hover:text-foreground\"\n >\n <MaterialSymbol icon=\"logout\" size={20} weight={400} className=\"shrink-0\" />\n <span className={classNames('sidebar-label text-sm', { 'is-collapsed': collapsed })}>\n {t('sidebar.logout')}\n </span>\n </ControlledButton>\n </div>\n )}\n </nav>\n </aside>\n );\n};\n\nexport default AppSidebar;\n"],"names":[],"mappings":";;;;;;AAqBA,MAAM,aAAyC,CAAC;AAAA,EAC9C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MAAM;AACJ,QAAM,EAAE,EAAA,IAAM,eAAe,IAAI;AAEjC,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,cAAc;AAAA,MACd,cAAc;AAAA,MACd,WAAW;AAAA,QACT;AAAA,QACA;AAAA,UACE,WAAW,CAAC,YAAY;AAAA,UACxB,aAAa;AAAA,UACb,eAAe;AAAA,UACf,iCAAiC,YAAY,CAAC;AAAA,QAAA;AAAA,MAChD;AAAA,MAGF,UAAA,qBAAC,OAAA,EAAI,WAAU,iHACb,UAAA;AAAA,QAAA;AAAA,UAAC;AAAA,UAAA;AAAA,YACC;AAAA,YACA;AAAA,YACA,aAAY;AAAA,YACZ,iBAAgB;AAAA,YAChB;AAAA,UAAA;AAAA,QAAA;AAAA,QAGD,YACC,oBAAC,OAAA,EAAI,WAAU,yBACb,UAAA;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,mBAAiB;AAAA,YACjB,SAAS;AAAA,YACT,WAAU;AAAA,YAEV,UAAA;AAAA,cAAA,oBAAC,gBAAA,EAAe,MAAK,UAAS,MAAM,IAAI,QAAQ,KAAK,WAAU,WAAA,CAAW;AAAA,cAC1E,oBAAC,QAAA,EAAK,WAAW,WAAW,yBAAyB,EAAE,gBAAgB,UAAA,CAAW,GAC/E,UAAA,EAAE,gBAAgB,EAAA,CACrB;AAAA,YAAA;AAAA,UAAA;AAAA,QAAA,EACF,CACF;AAAA,MAAA,EAAA,CAEJ;AAAA,IAAA;AAAA,EAAA;AAGN;"}
1
+ {"version":3,"file":"AppSidebar.js","sources":["../../../src/components/Shell/AppSidebar.tsx"],"sourcesContent":["import { useMemo } from 'react';\nimport classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\n\nimport ControlledButton from '@/components/Form/ControlledButton';\nimport { MaterialSymbol } from '@/components/UI/MaterialSymbol';\n\nimport { buildFavoriteMenuItems, SidebarFavoritesProvider, useSidebarFavorites } from './sidebarFavorites';\nimport SidebarMenu, { ISidebarMenuItem } from './SidebarMenu';\n\ninterface IAppSidebarProps {\n items: Array<ISidebarMenuItem>;\n isMobile: boolean;\n isVisible: boolean;\n collapsed: boolean;\n isFloating: boolean;\n onHoverEnter: () => void;\n onHoverLeave: () => void;\n onNavigate?: () => void;\n /** Hides the footer sign-out row when absent. */\n onLogout?: () => void;\n}\n\ninterface ISidebarMenuWithFavoritesProps {\n items: Array<ISidebarMenuItem>;\n collapsed: boolean;\n onNavigate?: () => void;\n}\n\nconst SidebarMenuWithFavorites: React.FC<ISidebarMenuWithFavoritesProps> = ({ items, collapsed, onNavigate }) => {\n const { favoriteKeys } = useSidebarFavorites();\n\n const menuItems = useMemo(\n () => [...buildFavoriteMenuItems(items, favoriteKeys), ...(items ?? [])],\n [favoriteKeys, items],\n );\n\n return (\n <SidebarMenu\n items={menuItems}\n collapsed={collapsed}\n activeClass=\"text-brand-emphasis bg-brand-soft\"\n activeIconClass=\"text-brand-emphasis\"\n onNavigate={onNavigate}\n />\n );\n};\n\nconst AppSidebar: React.FC<IAppSidebarProps> = ({\n items,\n isMobile,\n isVisible,\n collapsed,\n isFloating,\n onHoverEnter,\n onHoverLeave,\n onNavigate,\n onLogout,\n}) => {\n const { t } = useTranslation('ui');\n\n return (\n <aside\n onMouseEnter={onHoverEnter}\n onMouseLeave={onHoverLeave}\n className={classNames(\n 'sidebar-aside fixed left-0 top-0 z-40 h-full overflow-hidden border-r border-border bg-muted',\n {\n 'is-rail': !isMobile && collapsed,\n 'is-mobile': isMobile,\n 'is-floating': isFloating,\n 'is-hidden pointer-events-none': isMobile && !isVisible,\n },\n )}\n >\n <nav className=\"sidebar-scroll flex h-full flex-col justify-between gap-y-5 overflow-y-auto overflow-x-hidden px-3 py-5 pt-20\">\n <SidebarFavoritesProvider>\n <SidebarMenuWithFavorites items={items} collapsed={collapsed} onNavigate={onNavigate} />\n </SidebarFavoritesProvider>\n\n {onLogout && (\n <div className=\"text-foreground-muted\">\n <ControlledButton\n defaultHtmlButton\n onClick={onLogout}\n className=\"flex h-9 w-full items-center gap-x-3 rounded-lg px-3 text-left text-foreground-muted transition-colors duration-base hover:bg-muted-hover hover:text-foreground\"\n >\n <MaterialSymbol icon=\"logout\" size={20} weight={400} className=\"shrink-0\" />\n <span className={classNames('sidebar-label text-sm', { 'is-collapsed': collapsed })}>\n {t('sidebar.logout')}\n </span>\n </ControlledButton>\n </div>\n )}\n </nav>\n </aside>\n );\n};\n\nexport default AppSidebar;\n"],"names":[],"mappings":";;;;;;;;AA6BA,MAAM,2BAAqE,CAAC,EAAE,OAAO,WAAW,iBAAiB;AAC/G,QAAM,EAAE,aAAA,IAAiB,oBAAA;AAEzB,QAAM,YAAY;AAAA,IAChB,MAAM,CAAC,GAAG,uBAAuB,OAAO,YAAY,GAAG,GAAI,SAAS,EAAG;AAAA,IACvE,CAAC,cAAc,KAAK;AAAA,EAAA;AAGtB,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,OAAO;AAAA,MACP;AAAA,MACA,aAAY;AAAA,MACZ,iBAAgB;AAAA,MAChB;AAAA,IAAA;AAAA,EAAA;AAGN;AAEA,MAAM,aAAyC,CAAC;AAAA,EAC9C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MAAM;AACJ,QAAM,EAAE,EAAA,IAAM,eAAe,IAAI;AAEjC,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,cAAc;AAAA,MACd,cAAc;AAAA,MACd,WAAW;AAAA,QACT;AAAA,QACA;AAAA,UACE,WAAW,CAAC,YAAY;AAAA,UACxB,aAAa;AAAA,UACb,eAAe;AAAA,UACf,iCAAiC,YAAY,CAAC;AAAA,QAAA;AAAA,MAChD;AAAA,MAGF,UAAA,qBAAC,OAAA,EAAI,WAAU,iHACb,UAAA;AAAA,QAAA,oBAAC,4BACC,UAAA,oBAAC,0BAAA,EAAyB,OAAc,WAAsB,YAAwB,GACxF;AAAA,QAEC,YACC,oBAAC,OAAA,EAAI,WAAU,yBACb,UAAA;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,mBAAiB;AAAA,YACjB,SAAS;AAAA,YACT,WAAU;AAAA,YAEV,UAAA;AAAA,cAAA,oBAAC,gBAAA,EAAe,MAAK,UAAS,MAAM,IAAI,QAAQ,KAAK,WAAU,WAAA,CAAW;AAAA,cAC1E,oBAAC,QAAA,EAAK,WAAW,WAAW,yBAAyB,EAAE,gBAAgB,UAAA,CAAW,GAC/E,UAAA,EAAE,gBAAgB,EAAA,CACrB;AAAA,YAAA;AAAA,UAAA;AAAA,QAAA,EACF,CACF;AAAA,MAAA,EAAA,CAEJ;AAAA,IAAA;AAAA,EAAA;AAGN;"}
@@ -1,5 +1,5 @@
1
1
  import { MaterialSymbolWeight, SymbolCodepoints } from '../UI/MaterialSymbol';
2
- export type TUiSidebarSection = 'management' | 'registration';
2
+ export type TUiSidebarSection = 'favorites' | 'management' | 'registration';
3
3
  export interface ISidebarMenuItem {
4
4
  id?: string;
5
5
  weight?: MaterialSymbolWeight;
@@ -4,7 +4,9 @@ import classNames from "classnames";
4
4
  import { useTranslation } from "react-i18next";
5
5
  import ControlledButton from "../Form/ControlledButton.js";
6
6
  import { useUiNavigation } from "../Navigation/UiNavigationContext.js";
7
+ import ControlledTooltip from "../UI/ControlledTooltip.js";
7
8
  import { MaterialSymbol } from "../UI/MaterialSymbol/index.js";
9
+ import { useSidebarFavorites, resolveSidebarFavoriteKey } from "./sidebarFavorites.js";
8
10
  const resolveSidebarItemId = (item) => item.id ?? `${item.to}:${item.text}`;
9
11
  const matchesRoute = (location, target, end = false) => {
10
12
  if (!target || target === "#") return false;
@@ -19,6 +21,24 @@ const matchesRoute = (location, target, end = false) => {
19
21
  return end ? location.pathname === target : location.pathname === target || location.pathname.startsWith(`${target}/`);
20
22
  };
21
23
  const containsActiveRoute = (item, location) => item.children?.length ? item.children.some((child) => containsActiveRoute(child, location)) : matchesRoute(location, item.to, item.end);
24
+ const SidebarFavoriteToggle = ({ favoriteKey }) => {
25
+ const { t } = useTranslation("ui");
26
+ const { isFavorite, toggleFavorite } = useSidebarFavorites();
27
+ const favorite = isFavorite(favoriteKey);
28
+ return /* @__PURE__ */ jsx(ControlledTooltip, { content: t(favorite ? "sidebar.favorites.remove" : "sidebar.favorites.add"), placement: "right", children: /* @__PURE__ */ jsx(
29
+ ControlledButton,
30
+ {
31
+ defaultHtmlButton: true,
32
+ onClick: () => toggleFavorite(favoriteKey),
33
+ "aria-pressed": favorite,
34
+ className: classNames(
35
+ "absolute right-1 top-1/2 flex size-7 -translate-y-1/2 items-center justify-center rounded-md transition-interactive duration-fast hover:bg-muted-active focus-visible:opacity-100 group-hover/row:opacity-100 [@media(hover:none)]:opacity-100",
36
+ favorite ? "text-warning-emphasis opacity-0" : "text-foreground-subtle opacity-0 hover:text-foreground"
37
+ ),
38
+ children: /* @__PURE__ */ jsx(MaterialSymbol, { icon: "star", size: 16, fill: favorite })
39
+ }
40
+ ) });
41
+ };
22
42
  const SidebarMenuRow = ({
23
43
  item,
24
44
  isOpen,
@@ -30,6 +50,7 @@ const SidebarMenuRow = ({
30
50
  level
31
51
  }) => {
32
52
  const { location, Link: NavigationLink } = useUiNavigation();
53
+ const { canFavorite } = useSidebarFavorites();
33
54
  const contentRef = useRef(null);
34
55
  const [maxHeight, setMaxHeight] = useState("0px");
35
56
  const hasChildren = Boolean(item.children?.length);
@@ -107,13 +128,18 @@ const SidebarMenuRow = ({
107
128
  ) }) })
108
129
  ] });
109
130
  }
110
- return /* @__PURE__ */ jsx("li", { className: "flex flex-col", children: /* @__PURE__ */ jsxs(NavigationLink, { to: item.to, className: rowClassName, onClick: onNavigate, children: [
111
- activeBar,
112
- icon,
113
- label
114
- ] }) });
131
+ const favoriteKey = canFavorite && !collapsed ? resolveSidebarFavoriteKey(item) : null;
132
+ return /* @__PURE__ */ jsxs("li", { className: "group/row relative flex flex-col", children: [
133
+ /* @__PURE__ */ jsxs(NavigationLink, { to: item.to, className: classNames(rowClassName, { "pr-9": favoriteKey }), onClick: onNavigate, children: [
134
+ activeBar,
135
+ icon,
136
+ label
137
+ ] }),
138
+ favoriteKey && /* @__PURE__ */ jsx(SidebarFavoriteToggle, { favoriteKey })
139
+ ] });
115
140
  };
116
141
  const SECTION_LABEL_KEYS = {
142
+ favorites: "sidebar.sections.favorites",
117
143
  management: "sidebar.sections.management",
118
144
  registration: "sidebar.sections.registration"
119
145
  };
@@ -128,7 +154,7 @@ const SidebarSectionHeader = ({ section, collapsed, withDivider }) => {
128
154
  /* @__PURE__ */ jsx("span", { className: "px-3 pb-0.5 text-xxs font-semibold uppercase tracking-wide text-foreground-subtle", children: t(SECTION_LABEL_KEYS[section]) })
129
155
  ] });
130
156
  };
131
- const SECTION_ORDER = ["management", "registration"];
157
+ const SECTION_ORDER = ["favorites", "management", "registration"];
132
158
  const groupItemsBySection = (items) => {
133
159
  const itemsWithoutSection = (items ?? []).filter((item) => !item.section);
134
160
  const sectionGroups = SECTION_ORDER.map((section) => ({
@@ -1 +1 @@
1
- {"version":3,"file":"SidebarMenu.js","sources":["../../../src/components/Shell/SidebarMenu.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from 'react';\nimport classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\n\nimport ControlledButton from '@/components/Form/ControlledButton';\nimport { useUiNavigation } from '@/components/Navigation/UiNavigationContext';\nimport { MaterialSymbol, MaterialSymbolWeight, SymbolCodepoints } from '@/components/UI/MaterialSymbol';\n\nexport type TUiSidebarSection = 'management' | 'registration';\n\nexport interface ISidebarMenuItem {\n id?: string;\n weight?: MaterialSymbolWeight;\n icon: SymbolCodepoints;\n text: string;\n to: string;\n externalUrl?: string;\n end?: boolean;\n section?: TUiSidebarSection;\n children?: Array<ISidebarMenuItem>;\n}\n\nexport const resolveSidebarItemId = (item: ISidebarMenuItem): string => item.id ?? `${item.to}:${item.text}`;\n\ninterface ISidebarLocation {\n pathname: string;\n search: string;\n}\n\ninterface ISidebarMenuProps {\n items: Array<ISidebarMenuItem>;\n activeClass: string;\n collapsed: boolean;\n activeIconClass?: string;\n onNavigate?: () => void;\n level?: number;\n}\n\nconst matchesRoute = (location: ISidebarLocation, target: string, end = false): boolean => {\n if (!target || target === '#') return false;\n\n if (target.includes('?')) {\n const [pathPart, queryPart] = target.split('?');\n const samePath = end\n ? location.pathname === pathPart\n : location.pathname === pathPart || location.pathname.startsWith(`${pathPart}/`);\n if (!samePath) return false;\n\n const currentParams = new URLSearchParams(location.search);\n const expectedParams = new URLSearchParams(queryPart);\n\n return Array.from(expectedParams.entries()).every(([key, value]) => currentParams.get(key) === value);\n }\n\n return end\n ? location.pathname === target\n : location.pathname === target || location.pathname.startsWith(`${target}/`);\n};\n\nconst containsActiveRoute = (item: ISidebarMenuItem, location: ISidebarLocation): boolean =>\n item.children?.length\n ? item.children.some((child) => containsActiveRoute(child, location))\n : matchesRoute(location, item.to, item.end);\n\ninterface ISidebarMenuRowProps {\n item: ISidebarMenuItem;\n isOpen: boolean;\n collapsed: boolean;\n onToggle: () => void;\n activeClass: string;\n activeIconClass?: string;\n onNavigate?: () => void;\n level: number;\n}\n\nconst SidebarMenuRow: React.FC<ISidebarMenuRowProps> = ({\n item,\n isOpen,\n collapsed,\n onToggle,\n activeClass,\n activeIconClass,\n onNavigate,\n level,\n}) => {\n const { location, Link: NavigationLink } = useUiNavigation();\n const contentRef = useRef<HTMLDivElement>(null);\n const [maxHeight, setMaxHeight] = useState('0px');\n\n const hasChildren = Boolean(item.children?.length);\n const isActive = hasChildren ? containsActiveRoute(item, location) : matchesRoute(location, item.to, item.end);\n const submenuOpen = isOpen && !collapsed;\n\n useEffect(() => {\n const content = contentRef.current;\n if (!content) return;\n\n if (!submenuOpen) {\n setMaxHeight('0px');\n return;\n }\n\n const updateHeight = () => setMaxHeight(`${content.scrollHeight}px`);\n updateHeight();\n\n const observer = new ResizeObserver(updateHeight);\n observer.observe(content);\n\n return () => observer.disconnect();\n }, [submenuOpen]);\n\n const rowClassName = classNames(\n 'relative flex h-9 w-full items-center gap-x-3 rounded-lg px-3 text-left transition-colors duration-base',\n isActive ? activeClass : 'text-foreground-muted hover:bg-muted-hover hover:text-foreground',\n );\n\n const iconClassName = classNames(\n 'shrink-0',\n isActive ? (activeIconClass ?? 'text-brand-emphasis') : 'text-foreground-muted',\n );\n\n const activeBar = isActive ? (\n <span aria-hidden className=\"absolute left-0 top-1/2 h-5 w-0.5 -translate-y-1/2 rounded-full bg-brand\" />\n ) : null;\n\n const icon = <MaterialSymbol icon={item.icon} size={20} weight={item.weight || 400} className={iconClassName} />;\n const label = <span className={classNames('sidebar-label text-sm', { 'is-collapsed': collapsed })}>{item.text}</span>;\n\n if (item.externalUrl) {\n return (\n <li className=\"flex flex-col\">\n <a\n href={item.externalUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className={rowClassName}\n onClick={onNavigate}\n >\n {activeBar}\n {icon}\n {label}\n </a>\n </li>\n );\n }\n\n if (hasChildren) {\n return (\n <li className=\"flex flex-col\">\n <ControlledButton defaultHtmlButton onClick={onToggle} aria-expanded={submenuOpen} className={rowClassName}>\n {activeBar}\n {icon}\n {label}\n <MaterialSymbol\n icon=\"chevron_right\"\n size={18}\n className={classNames('sidebar-chevron ml-auto shrink-0 text-foreground-subtle', {\n 'is-collapsed': collapsed,\n 'is-open': submenuOpen,\n })}\n />\n </ControlledButton>\n\n <div style={{ maxHeight }} className={classNames('sidebar-submenu', { 'mt-1': submenuOpen })}>\n <div ref={contentRef} className=\"pl-5\">\n <SidebarMenu\n items={item.children ?? []}\n collapsed={collapsed}\n activeClass={activeClass}\n activeIconClass={activeIconClass}\n onNavigate={onNavigate}\n level={level + 1}\n />\n </div>\n </div>\n </li>\n );\n }\n\n return (\n <li className=\"flex flex-col\">\n <NavigationLink to={item.to} className={rowClassName} onClick={onNavigate}>\n {activeBar}\n {icon}\n {label}\n </NavigationLink>\n </li>\n );\n};\n\nconst SECTION_LABEL_KEYS: Record<TUiSidebarSection, string> = {\n management: 'sidebar.sections.management',\n registration: 'sidebar.sections.registration',\n};\n\ninterface ISidebarSectionHeaderProps {\n section: TUiSidebarSection;\n collapsed: boolean;\n withDivider: boolean;\n}\n\nconst SidebarSectionHeader: React.FC<ISidebarSectionHeaderProps> = ({ section, collapsed, withDivider }) => {\n const { t } = useTranslation('ui');\n\n if (collapsed) {\n if (!withDivider) return null;\n return (\n <li aria-hidden className=\"flex flex-col px-3\">\n <span className=\"border-t border-border/70\" />\n </li>\n );\n }\n\n return (\n <li className={classNames('flex flex-col', withDivider ? 'mt-2' : 'mt-1')}>\n {withDivider && <span aria-hidden className=\"mx-3 mb-2 border-t border-border/70\" />}\n <span className=\"px-3 pb-0.5 text-xxs font-semibold uppercase tracking-wide text-foreground-subtle\">\n {t(SECTION_LABEL_KEYS[section])}\n </span>\n </li>\n );\n};\n\nconst SECTION_ORDER: Array<TUiSidebarSection> = ['management', 'registration'];\n\ninterface ISidebarMenuGroup {\n section: TUiSidebarSection | null;\n items: Array<ISidebarMenuItem>;\n}\n\nconst groupItemsBySection = (items: Array<ISidebarMenuItem>): Array<ISidebarMenuGroup> => {\n const itemsWithoutSection = (items ?? []).filter((item) => !item.section);\n const sectionGroups = SECTION_ORDER.map((section) => ({\n section,\n items: (items ?? []).filter((item) => item.section === section),\n })).filter((group) => group.items.length > 0);\n\n return itemsWithoutSection.length ? [{ section: null, items: itemsWithoutSection }, ...sectionGroups] : sectionGroups;\n};\n\nconst SidebarMenu: React.FC<ISidebarMenuProps> = ({\n items,\n activeClass,\n collapsed,\n activeIconClass,\n onNavigate,\n level = 0,\n}) => {\n const { location } = useUiNavigation();\n\n const activeKey =\n items\n .filter((item) => Boolean(item.children?.length) && containsActiveRoute(item, location))\n .map(resolveSidebarItemId)[0] ?? null;\n\n const [openKey, setOpenKey] = useState<string | null>(activeKey);\n const previousActiveKey = useRef(activeKey);\n\n useEffect(() => {\n if (activeKey && activeKey !== previousActiveKey.current) {\n setOpenKey(activeKey);\n }\n previousActiveKey.current = activeKey;\n }, [activeKey]);\n\n const handleToggle = (key: string) => {\n setOpenKey((current) => (current === key ? null : key));\n };\n\n const showSections = level === 0;\n const groups = showSections ? groupItemsBySection(items) : [{ section: null, items: items ?? [] }];\n\n return (\n <ul className={classNames('flex flex-col', level === 0 ? 'gap-y-2' : 'gap-y-1')}>\n {groups.flatMap((group, groupIndex) => {\n const rows: Array<React.ReactElement> = [];\n\n if (group.section) {\n rows.push(\n <SidebarSectionHeader\n key={`section-${group.section}`}\n section={group.section}\n collapsed={collapsed}\n withDivider={groupIndex > 0}\n />,\n );\n }\n\n group.items.forEach((item) => {\n const itemId = resolveSidebarItemId(item);\n\n rows.push(\n <SidebarMenuRow\n key={itemId}\n item={item}\n isOpen={openKey === itemId}\n collapsed={collapsed}\n onToggle={() => handleToggle(itemId)}\n activeClass={activeClass}\n activeIconClass={activeIconClass}\n onNavigate={onNavigate}\n level={level}\n />,\n );\n });\n\n return rows;\n })}\n </ul>\n );\n};\n\nexport default SidebarMenu;\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"SidebarMenu.js","sources":["../../../src/components/Shell/SidebarMenu.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from 'react';\nimport classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\n\nimport ControlledButton from '@/components/Form/ControlledButton';\nimport { useUiNavigation } from '@/components/Navigation/UiNavigationContext';\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { MaterialSymbol, MaterialSymbolWeight, SymbolCodepoints } from '@/components/UI/MaterialSymbol';\n\nimport { resolveSidebarFavoriteKey, useSidebarFavorites } from './sidebarFavorites';\n\nexport type TUiSidebarSection = 'favorites' | 'management' | 'registration';\n\nexport interface ISidebarMenuItem {\n id?: string;\n weight?: MaterialSymbolWeight;\n icon: SymbolCodepoints;\n text: string;\n to: string;\n externalUrl?: string;\n end?: boolean;\n section?: TUiSidebarSection;\n children?: Array<ISidebarMenuItem>;\n}\n\nexport const resolveSidebarItemId = (item: ISidebarMenuItem): string => item.id ?? `${item.to}:${item.text}`;\n\ninterface ISidebarLocation {\n pathname: string;\n search: string;\n}\n\ninterface ISidebarMenuProps {\n items: Array<ISidebarMenuItem>;\n activeClass: string;\n collapsed: boolean;\n activeIconClass?: string;\n onNavigate?: () => void;\n level?: number;\n}\n\nconst matchesRoute = (location: ISidebarLocation, target: string, end = false): boolean => {\n if (!target || target === '#') return false;\n\n if (target.includes('?')) {\n const [pathPart, queryPart] = target.split('?');\n const samePath = end\n ? location.pathname === pathPart\n : location.pathname === pathPart || location.pathname.startsWith(`${pathPart}/`);\n if (!samePath) return false;\n\n const currentParams = new URLSearchParams(location.search);\n const expectedParams = new URLSearchParams(queryPart);\n\n return Array.from(expectedParams.entries()).every(([key, value]) => currentParams.get(key) === value);\n }\n\n return end\n ? location.pathname === target\n : location.pathname === target || location.pathname.startsWith(`${target}/`);\n};\n\nconst containsActiveRoute = (item: ISidebarMenuItem, location: ISidebarLocation): boolean =>\n item.children?.length\n ? item.children.some((child) => containsActiveRoute(child, location))\n : matchesRoute(location, item.to, item.end);\n\ninterface ISidebarMenuRowProps {\n item: ISidebarMenuItem;\n isOpen: boolean;\n collapsed: boolean;\n onToggle: () => void;\n activeClass: string;\n activeIconClass?: string;\n onNavigate?: () => void;\n level: number;\n}\n\ninterface ISidebarFavoriteToggleProps {\n favoriteKey: string;\n}\n\nconst SidebarFavoriteToggle: React.FC<ISidebarFavoriteToggleProps> = ({ favoriteKey }) => {\n const { t } = useTranslation('ui');\n const { isFavorite, toggleFavorite } = useSidebarFavorites();\n const favorite = isFavorite(favoriteKey);\n\n return (\n <ControlledTooltip content={t(favorite ? 'sidebar.favorites.remove' : 'sidebar.favorites.add')} placement=\"right\">\n <ControlledButton\n defaultHtmlButton\n onClick={() => toggleFavorite(favoriteKey)}\n aria-pressed={favorite}\n className={classNames(\n 'absolute right-1 top-1/2 flex size-7 -translate-y-1/2 items-center justify-center rounded-md transition-interactive duration-fast hover:bg-muted-active focus-visible:opacity-100 group-hover/row:opacity-100 [@media(hover:none)]:opacity-100',\n favorite ? 'text-warning-emphasis opacity-0' : 'text-foreground-subtle opacity-0 hover:text-foreground',\n )}\n >\n <MaterialSymbol icon=\"star\" size={16} fill={favorite} />\n </ControlledButton>\n </ControlledTooltip>\n );\n};\n\nconst SidebarMenuRow: React.FC<ISidebarMenuRowProps> = ({\n item,\n isOpen,\n collapsed,\n onToggle,\n activeClass,\n activeIconClass,\n onNavigate,\n level,\n}) => {\n const { location, Link: NavigationLink } = useUiNavigation();\n const { canFavorite } = useSidebarFavorites();\n const contentRef = useRef<HTMLDivElement>(null);\n const [maxHeight, setMaxHeight] = useState('0px');\n\n const hasChildren = Boolean(item.children?.length);\n const isActive = hasChildren ? containsActiveRoute(item, location) : matchesRoute(location, item.to, item.end);\n const submenuOpen = isOpen && !collapsed;\n\n useEffect(() => {\n const content = contentRef.current;\n if (!content) return;\n\n if (!submenuOpen) {\n setMaxHeight('0px');\n return;\n }\n\n const updateHeight = () => setMaxHeight(`${content.scrollHeight}px`);\n updateHeight();\n\n const observer = new ResizeObserver(updateHeight);\n observer.observe(content);\n\n return () => observer.disconnect();\n }, [submenuOpen]);\n\n const rowClassName = classNames(\n 'relative flex h-9 w-full items-center gap-x-3 rounded-lg px-3 text-left transition-colors duration-base',\n isActive ? activeClass : 'text-foreground-muted hover:bg-muted-hover hover:text-foreground',\n );\n\n const iconClassName = classNames(\n 'shrink-0',\n isActive ? (activeIconClass ?? 'text-brand-emphasis') : 'text-foreground-muted',\n );\n\n const activeBar = isActive ? (\n <span aria-hidden className=\"absolute left-0 top-1/2 h-5 w-0.5 -translate-y-1/2 rounded-full bg-brand\" />\n ) : null;\n\n const icon = <MaterialSymbol icon={item.icon} size={20} weight={item.weight || 400} className={iconClassName} />;\n const label = <span className={classNames('sidebar-label text-sm', { 'is-collapsed': collapsed })}>{item.text}</span>;\n\n if (item.externalUrl) {\n return (\n <li className=\"flex flex-col\">\n <a\n href={item.externalUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className={rowClassName}\n onClick={onNavigate}\n >\n {activeBar}\n {icon}\n {label}\n </a>\n </li>\n );\n }\n\n if (hasChildren) {\n return (\n <li className=\"flex flex-col\">\n <ControlledButton defaultHtmlButton onClick={onToggle} aria-expanded={submenuOpen} className={rowClassName}>\n {activeBar}\n {icon}\n {label}\n <MaterialSymbol\n icon=\"chevron_right\"\n size={18}\n className={classNames('sidebar-chevron ml-auto shrink-0 text-foreground-subtle', {\n 'is-collapsed': collapsed,\n 'is-open': submenuOpen,\n })}\n />\n </ControlledButton>\n\n <div style={{ maxHeight }} className={classNames('sidebar-submenu', { 'mt-1': submenuOpen })}>\n <div ref={contentRef} className=\"pl-5\">\n <SidebarMenu\n items={item.children ?? []}\n collapsed={collapsed}\n activeClass={activeClass}\n activeIconClass={activeIconClass}\n onNavigate={onNavigate}\n level={level + 1}\n />\n </div>\n </div>\n </li>\n );\n }\n\n const favoriteKey = canFavorite && !collapsed ? resolveSidebarFavoriteKey(item) : null;\n\n return (\n <li className=\"group/row relative flex flex-col\">\n <NavigationLink to={item.to} className={classNames(rowClassName, { 'pr-9': favoriteKey })} onClick={onNavigate}>\n {activeBar}\n {icon}\n {label}\n </NavigationLink>\n {favoriteKey && <SidebarFavoriteToggle favoriteKey={favoriteKey} />}\n </li>\n );\n};\n\nconst SECTION_LABEL_KEYS: Record<TUiSidebarSection, string> = {\n favorites: 'sidebar.sections.favorites',\n management: 'sidebar.sections.management',\n registration: 'sidebar.sections.registration',\n};\n\ninterface ISidebarSectionHeaderProps {\n section: TUiSidebarSection;\n collapsed: boolean;\n withDivider: boolean;\n}\n\nconst SidebarSectionHeader: React.FC<ISidebarSectionHeaderProps> = ({ section, collapsed, withDivider }) => {\n const { t } = useTranslation('ui');\n\n if (collapsed) {\n if (!withDivider) return null;\n return (\n <li aria-hidden className=\"flex flex-col px-3\">\n <span className=\"border-t border-border/70\" />\n </li>\n );\n }\n\n return (\n <li className={classNames('flex flex-col', withDivider ? 'mt-2' : 'mt-1')}>\n {withDivider && <span aria-hidden className=\"mx-3 mb-2 border-t border-border/70\" />}\n <span className=\"px-3 pb-0.5 text-xxs font-semibold uppercase tracking-wide text-foreground-subtle\">\n {t(SECTION_LABEL_KEYS[section])}\n </span>\n </li>\n );\n};\n\nconst SECTION_ORDER: Array<TUiSidebarSection> = ['favorites', 'management', 'registration'];\n\ninterface ISidebarMenuGroup {\n section: TUiSidebarSection | null;\n items: Array<ISidebarMenuItem>;\n}\n\nconst groupItemsBySection = (items: Array<ISidebarMenuItem>): Array<ISidebarMenuGroup> => {\n const itemsWithoutSection = (items ?? []).filter((item) => !item.section);\n const sectionGroups = SECTION_ORDER.map((section) => ({\n section,\n items: (items ?? []).filter((item) => item.section === section),\n })).filter((group) => group.items.length > 0);\n\n return itemsWithoutSection.length ? [{ section: null, items: itemsWithoutSection }, ...sectionGroups] : sectionGroups;\n};\n\nconst SidebarMenu: React.FC<ISidebarMenuProps> = ({\n items,\n activeClass,\n collapsed,\n activeIconClass,\n onNavigate,\n level = 0,\n}) => {\n const { location } = useUiNavigation();\n\n const activeKey =\n items\n .filter((item) => Boolean(item.children?.length) && containsActiveRoute(item, location))\n .map(resolveSidebarItemId)[0] ?? null;\n\n const [openKey, setOpenKey] = useState<string | null>(activeKey);\n const previousActiveKey = useRef(activeKey);\n\n useEffect(() => {\n if (activeKey && activeKey !== previousActiveKey.current) {\n setOpenKey(activeKey);\n }\n previousActiveKey.current = activeKey;\n }, [activeKey]);\n\n const handleToggle = (key: string) => {\n setOpenKey((current) => (current === key ? null : key));\n };\n\n const showSections = level === 0;\n const groups = showSections ? groupItemsBySection(items) : [{ section: null, items: items ?? [] }];\n\n return (\n <ul className={classNames('flex flex-col', level === 0 ? 'gap-y-2' : 'gap-y-1')}>\n {groups.flatMap((group, groupIndex) => {\n const rows: Array<React.ReactElement> = [];\n\n if (group.section) {\n rows.push(\n <SidebarSectionHeader\n key={`section-${group.section}`}\n section={group.section}\n collapsed={collapsed}\n withDivider={groupIndex > 0}\n />,\n );\n }\n\n group.items.forEach((item) => {\n const itemId = resolveSidebarItemId(item);\n\n rows.push(\n <SidebarMenuRow\n key={itemId}\n item={item}\n isOpen={openKey === itemId}\n collapsed={collapsed}\n onToggle={() => handleToggle(itemId)}\n activeClass={activeClass}\n activeIconClass={activeIconClass}\n onNavigate={onNavigate}\n level={level}\n />,\n );\n });\n\n return rows;\n })}\n </ul>\n );\n};\n\nexport default SidebarMenu;\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import { ReactNode } from 'react';
2
+ import { ISidebarMenuItem } from './SidebarMenu';
3
+ /** A favorite is a destination, so only rows that open something (no children) can be one. */
4
+ export declare const resolveSidebarFavoriteKey: (item: ISidebarMenuItem) => string | null;
5
+ interface ISidebarFavoritesContext {
6
+ canFavorite: boolean;
7
+ favoriteKeys: Array<string>;
8
+ isFavorite: (favoriteKey: string) => boolean;
9
+ toggleFavorite: (favoriteKey: string) => void;
10
+ }
11
+ export declare const SidebarFavoritesProvider: React.FC<{
12
+ children: ReactNode;
13
+ }>;
14
+ export declare const useSidebarFavorites: () => ISidebarFavoritesContext;
15
+ /**
16
+ * The rows of the Favorites section, in the order the user starred them. A favorite the menu no
17
+ * longer offers (service turned off, permission revoked) is simply not rendered, and comes back
18
+ * on its own when the entry does.
19
+ */
20
+ export declare const buildFavoriteMenuItems: (items: Array<ISidebarMenuItem>, favoriteKeys: Array<string>) => Array<ISidebarMenuItem>;
21
+ export {};
@@ -0,0 +1,69 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { createContext, useState, useCallback, useMemo, useContext } from "react";
3
+ import { resolveUiPreferenceScopeId, writeUiPreference, readUiPreference } from "../../helpers/uiPreferences.js";
4
+ const FAVORITES_STORAGE_KEY = "sidebar_favorites";
5
+ const readFavoritesStore = () => {
6
+ const store = readUiPreference(FAVORITES_STORAGE_KEY);
7
+ return store && typeof store === "object" ? store : {};
8
+ };
9
+ const readScopedFavorites = (scopeId) => {
10
+ if (!scopeId) return [];
11
+ const favorites = readFavoritesStore()[scopeId];
12
+ return Array.isArray(favorites) ? favorites.filter((favoriteKey) => typeof favoriteKey === "string") : [];
13
+ };
14
+ const resolveSidebarFavoriteKey = (item) => item.children?.length ? null : (item.externalUrl ?? item.to) || null;
15
+ const SidebarFavoritesContext = createContext({
16
+ canFavorite: false,
17
+ favoriteKeys: [],
18
+ isFavorite: () => false,
19
+ toggleFavorite: () => void 0
20
+ });
21
+ const SidebarFavoritesProvider = ({ children }) => {
22
+ const [scopeId] = useState(resolveUiPreferenceScopeId);
23
+ const [favoriteKeys, setFavoriteKeys] = useState(() => readScopedFavorites(scopeId));
24
+ const toggleFavorite = useCallback(
25
+ (favoriteKey) => {
26
+ if (!scopeId) return;
27
+ setFavoriteKeys((currentKeys) => {
28
+ const nextKeys = currentKeys.includes(favoriteKey) ? currentKeys.filter((currentKey) => currentKey !== favoriteKey) : [...currentKeys, favoriteKey];
29
+ writeUiPreference(FAVORITES_STORAGE_KEY, { ...readFavoritesStore(), [scopeId]: nextKeys });
30
+ return nextKeys;
31
+ });
32
+ },
33
+ [scopeId]
34
+ );
35
+ const value = useMemo(
36
+ () => ({
37
+ canFavorite: Boolean(scopeId),
38
+ favoriteKeys,
39
+ isFavorite: (favoriteKey) => favoriteKeys.includes(favoriteKey),
40
+ toggleFavorite
41
+ }),
42
+ [favoriteKeys, scopeId, toggleFavorite]
43
+ );
44
+ return /* @__PURE__ */ jsx(SidebarFavoritesContext.Provider, { value, children });
45
+ };
46
+ const useSidebarFavorites = () => useContext(SidebarFavoritesContext);
47
+ const collectFavoritableItems = (items, itemsByFavoriteKey) => {
48
+ (items ?? []).forEach((item) => {
49
+ const favoriteKey = resolveSidebarFavoriteKey(item);
50
+ if (favoriteKey && !itemsByFavoriteKey.has(favoriteKey)) itemsByFavoriteKey.set(favoriteKey, item);
51
+ collectFavoritableItems(item.children ?? [], itemsByFavoriteKey);
52
+ });
53
+ return itemsByFavoriteKey;
54
+ };
55
+ const buildFavoriteMenuItems = (items, favoriteKeys) => {
56
+ const itemsByFavoriteKey = collectFavoritableItems(items, /* @__PURE__ */ new Map());
57
+ return favoriteKeys.reduce((favoriteItems, favoriteKey) => {
58
+ const item = itemsByFavoriteKey.get(favoriteKey);
59
+ if (item) favoriteItems.push({ ...item, id: `favorite:${favoriteKey}`, section: "favorites" });
60
+ return favoriteItems;
61
+ }, []);
62
+ };
63
+ export {
64
+ SidebarFavoritesProvider,
65
+ buildFavoriteMenuItems,
66
+ resolveSidebarFavoriteKey,
67
+ useSidebarFavorites
68
+ };
69
+ //# sourceMappingURL=sidebarFavorites.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sidebarFavorites.js","sources":["../../../src/components/Shell/sidebarFavorites.tsx"],"sourcesContent":["import { createContext, ReactNode, useCallback, useContext, useMemo, useState } from 'react';\n\nimport { readUiPreference, resolveUiPreferenceScopeId, writeUiPreference } from '@/helpers/uiPreferences';\n\nimport type { ISidebarMenuItem } from './SidebarMenu';\n\n// One consolidated entry for every user on the browser profile, keyed by the preference scope,\n// so a shared machine never mixes two people's favorites. Without a scope nothing is persisted.\nconst FAVORITES_STORAGE_KEY = 'sidebar_favorites';\n\ntype TSidebarFavoritesStore = Record<string, Array<string>>;\n\nconst readFavoritesStore = (): TSidebarFavoritesStore => {\n const store = readUiPreference<TSidebarFavoritesStore>(FAVORITES_STORAGE_KEY);\n\n return store && typeof store === 'object' ? store : {};\n};\n\nconst readScopedFavorites = (scopeId: string | null): Array<string> => {\n if (!scopeId) return [];\n\n const favorites = readFavoritesStore()[scopeId];\n\n return Array.isArray(favorites) ? favorites.filter((favoriteKey) => typeof favoriteKey === 'string') : [];\n};\n\n/** A favorite is a destination, so only rows that open something (no children) can be one. */\nexport const resolveSidebarFavoriteKey = (item: ISidebarMenuItem): string | null =>\n item.children?.length ? null : (item.externalUrl ?? item.to) || null;\n\ninterface ISidebarFavoritesContext {\n canFavorite: boolean;\n favoriteKeys: Array<string>;\n isFavorite: (favoriteKey: string) => boolean;\n toggleFavorite: (favoriteKey: string) => void;\n}\n\nconst SidebarFavoritesContext = createContext<ISidebarFavoritesContext>({\n canFavorite: false,\n favoriteKeys: [],\n isFavorite: () => false,\n toggleFavorite: () => undefined,\n});\n\nexport const SidebarFavoritesProvider: React.FC<{ children: ReactNode }> = ({ children }) => {\n const [scopeId] = useState(resolveUiPreferenceScopeId);\n const [favoriteKeys, setFavoriteKeys] = useState<Array<string>>(() => readScopedFavorites(scopeId));\n\n const toggleFavorite = useCallback(\n (favoriteKey: string) => {\n if (!scopeId) return;\n\n setFavoriteKeys((currentKeys) => {\n const nextKeys = currentKeys.includes(favoriteKey)\n ? currentKeys.filter((currentKey) => currentKey !== favoriteKey)\n : [...currentKeys, favoriteKey];\n\n writeUiPreference(FAVORITES_STORAGE_KEY, { ...readFavoritesStore(), [scopeId]: nextKeys });\n\n return nextKeys;\n });\n },\n [scopeId],\n );\n\n const value = useMemo<ISidebarFavoritesContext>(\n () => ({\n canFavorite: Boolean(scopeId),\n favoriteKeys,\n isFavorite: (favoriteKey) => favoriteKeys.includes(favoriteKey),\n toggleFavorite,\n }),\n [favoriteKeys, scopeId, toggleFavorite],\n );\n\n return <SidebarFavoritesContext.Provider value={value}>{children}</SidebarFavoritesContext.Provider>;\n};\n\nexport const useSidebarFavorites = (): ISidebarFavoritesContext => useContext(SidebarFavoritesContext);\n\nconst collectFavoritableItems = (\n items: Array<ISidebarMenuItem>,\n itemsByFavoriteKey: Map<string, ISidebarMenuItem>,\n): Map<string, ISidebarMenuItem> => {\n (items ?? []).forEach((item) => {\n const favoriteKey = resolveSidebarFavoriteKey(item);\n\n if (favoriteKey && !itemsByFavoriteKey.has(favoriteKey)) itemsByFavoriteKey.set(favoriteKey, item);\n\n collectFavoritableItems(item.children ?? [], itemsByFavoriteKey);\n });\n\n return itemsByFavoriteKey;\n};\n\n/**\n * The rows of the Favorites section, in the order the user starred them. A favorite the menu no\n * longer offers (service turned off, permission revoked) is simply not rendered, and comes back\n * on its own when the entry does.\n */\nexport const buildFavoriteMenuItems = (\n items: Array<ISidebarMenuItem>,\n favoriteKeys: Array<string>,\n): Array<ISidebarMenuItem> => {\n const itemsByFavoriteKey = collectFavoritableItems(items, new Map());\n\n return favoriteKeys.reduce<Array<ISidebarMenuItem>>((favoriteItems, favoriteKey) => {\n const item = itemsByFavoriteKey.get(favoriteKey);\n\n if (item) favoriteItems.push({ ...item, id: `favorite:${favoriteKey}`, section: 'favorites' });\n\n return favoriteItems;\n }, []);\n};\n"],"names":[],"mappings":";;;AAQA,MAAM,wBAAwB;AAI9B,MAAM,qBAAqB,MAA8B;AACvD,QAAM,QAAQ,iBAAyC,qBAAqB;AAE5E,SAAO,SAAS,OAAO,UAAU,WAAW,QAAQ,CAAA;AACtD;AAEA,MAAM,sBAAsB,CAAC,YAA0C;AACrE,MAAI,CAAC,QAAS,QAAO,CAAA;AAErB,QAAM,YAAY,mBAAA,EAAqB,OAAO;AAE9C,SAAO,MAAM,QAAQ,SAAS,IAAI,UAAU,OAAO,CAAC,gBAAgB,OAAO,gBAAgB,QAAQ,IAAI,CAAA;AACzG;AAGO,MAAM,4BAA4B,CAAC,SACxC,KAAK,UAAU,SAAS,QAAQ,KAAK,eAAe,KAAK,OAAO;AASlE,MAAM,0BAA0B,cAAwC;AAAA,EACtE,aAAa;AAAA,EACb,cAAc,CAAA;AAAA,EACd,YAAY,MAAM;AAAA,EAClB,gBAAgB,MAAM;AACxB,CAAC;AAEM,MAAM,2BAA8D,CAAC,EAAE,eAAe;AAC3F,QAAM,CAAC,OAAO,IAAI,SAAS,0BAA0B;AACrD,QAAM,CAAC,cAAc,eAAe,IAAI,SAAwB,MAAM,oBAAoB,OAAO,CAAC;AAElG,QAAM,iBAAiB;AAAA,IACrB,CAAC,gBAAwB;AACvB,UAAI,CAAC,QAAS;AAEd,sBAAgB,CAAC,gBAAgB;AAC/B,cAAM,WAAW,YAAY,SAAS,WAAW,IAC7C,YAAY,OAAO,CAAC,eAAe,eAAe,WAAW,IAC7D,CAAC,GAAG,aAAa,WAAW;AAEhC,0BAAkB,uBAAuB,EAAE,GAAG,mBAAA,GAAsB,CAAC,OAAO,GAAG,UAAU;AAEzF,eAAO;AAAA,MACT,CAAC;AAAA,IACH;AAAA,IACA,CAAC,OAAO;AAAA,EAAA;AAGV,QAAM,QAAQ;AAAA,IACZ,OAAO;AAAA,MACL,aAAa,QAAQ,OAAO;AAAA,MAC5B;AAAA,MACA,YAAY,CAAC,gBAAgB,aAAa,SAAS,WAAW;AAAA,MAC9D;AAAA,IAAA;AAAA,IAEF,CAAC,cAAc,SAAS,cAAc;AAAA,EAAA;AAGxC,SAAO,oBAAC,wBAAwB,UAAxB,EAAiC,OAAe,SAAA,CAAS;AACnE;AAEO,MAAM,sBAAsB,MAAgC,WAAW,uBAAuB;AAErG,MAAM,0BAA0B,CAC9B,OACA,uBACkC;AAClC,GAAC,SAAS,CAAA,GAAI,QAAQ,CAAC,SAAS;AAC9B,UAAM,cAAc,0BAA0B,IAAI;AAElD,QAAI,eAAe,CAAC,mBAAmB,IAAI,WAAW,EAAG,oBAAmB,IAAI,aAAa,IAAI;AAEjG,4BAAwB,KAAK,YAAY,CAAA,GAAI,kBAAkB;AAAA,EACjE,CAAC;AAED,SAAO;AACT;AAOO,MAAM,yBAAyB,CACpC,OACA,iBAC4B;AAC5B,QAAM,qBAAqB,wBAAwB,OAAO,oBAAI,KAAK;AAEnE,SAAO,aAAa,OAAgC,CAAC,eAAe,gBAAgB;AAClF,UAAM,OAAO,mBAAmB,IAAI,WAAW;AAE/C,QAAI,KAAM,eAAc,KAAK,EAAE,GAAG,MAAM,IAAI,YAAY,WAAW,IAAI,SAAS,YAAA,CAAa;AAE7F,WAAO;AAAA,EACT,GAAG,CAAA,CAAE;AACP;"}
@@ -7,7 +7,7 @@ import { twMerge } from "tailwind-merge";
7
7
  import ControlledTooltip from "./ControlledTooltip.js";
8
8
  import { MaterialSymbol } from "./MaterialSymbol/index.js";
9
9
  import { composeRefs } from "../../helpers/composeRefs.js";
10
- import { isPressOnLayeredSurface } from "../../helpers/layeredSurfaces.js";
10
+ import { LAYERED_SURFACE_ATTRIBUTE, isPressOnLayeredSurface } from "../../helpers/layeredSurfaces.js";
11
11
  import { getPortalHost } from "../../helpers/portalHost.js";
12
12
  import { useAnchoredFloating } from "../../hooks/useAnchoredFloating.js";
13
13
  import { floatingVariants } from "../../motion/motionPresets.js";
@@ -117,6 +117,7 @@ const ControlledMenuList = React.forwardRef(
117
117
  animate: "visible",
118
118
  exit: "exit",
119
119
  role: "menu",
120
+ ...LAYERED_SURFACE_ATTRIBUTE,
120
121
  ref: (node) => {
121
122
  setFloating(node);
122
123
  if (typeof forwardedRef === "function") forwardedRef(node);
@@ -1 +1 @@
1
- {"version":3,"file":"ControlledMenu.js","sources":["../../../src/components/UI/ControlledMenu.tsx"],"sourcesContent":["import React, {\n cloneElement,\n createContext,\n isValidElement,\n useCallback,\n useContext,\n useEffect,\n useId,\n useMemo,\n useState,\n} from 'react';\nimport classNames from 'classnames';\nimport { AnimatePresence, motion } from 'motion/react';\nimport { createPortal } from 'react-dom';\nimport { twMerge } from 'tailwind-merge';\n\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { MaterialSymbol, SymbolCodepoints } from '@/components/UI/MaterialSymbol';\nimport { composeRefs } from '@/helpers/composeRefs';\nimport { isPressOnLayeredSurface } from '@/helpers/layeredSurfaces';\nimport { getPortalHost } from '@/helpers/portalHost';\nimport { TFloatingPlacement, useAnchoredFloating } from '@/hooks/useAnchoredFloating';\nimport { floatingVariants } from '@/motion';\n\nexport type TMenuPlacement = TFloatingPlacement;\nexport type TMenuItemTone = 'default' | 'danger';\n\nexport interface IMenuDismissConfig {\n /** Closes the menu when an item is pressed. Multi-select surfaces pass false. */\n itemPress?: boolean;\n escapeKey?: boolean;\n outsidePress?: boolean;\n}\n\nexport interface IControlledMenuItem {\n key: string;\n label: string;\n icon?: SymbolCodepoints;\n hint?: string;\n onClick?: () => void;\n disabled?: boolean;\n tone?: TMenuItemTone;\n}\n\ninterface IMenuContext {\n open: boolean;\n setOpen: (open: boolean) => void;\n dismiss?: IMenuDismissConfig;\n listId: string;\n setReference: (node: HTMLElement | null) => void;\n setFloating: (node: HTMLElement | null) => void;\n floatingRef: React.MutableRefObject<HTMLElement | null>;\n referenceRef: React.MutableRefObject<HTMLElement | null>;\n}\n\nconst MenuContext = createContext<IMenuContext | null>(null);\n\nconst useMenuContext = (): IMenuContext => {\n const context = useContext(MenuContext);\n if (!context) throw new Error('Menu parts must be rendered inside ControlledMenuRoot');\n return context;\n};\n\ninterface IControlledMenuRootProps {\n children: React.ReactNode;\n open?: boolean;\n handler?: (open: boolean) => void;\n placement?: TMenuPlacement;\n offsetPx?: number;\n dismiss?: IMenuDismissConfig;\n matchTriggerWidth?: boolean;\n}\n\nexport const ControlledMenuRoot: React.FC<IControlledMenuRootProps> = ({\n children,\n open,\n handler,\n placement = 'bottom-start',\n offsetPx = 6,\n dismiss,\n matchTriggerWidth = false,\n}) => {\n const listId = useId();\n const [uncontrolledOpen, setUncontrolledOpen] = useState(false);\n\n const isControlled = open !== undefined;\n const isOpen = isControlled ? Boolean(open) : uncontrolledOpen;\n\n const setOpen = useCallback(\n (next: boolean) => {\n if (!isControlled) setUncontrolledOpen(next);\n handler?.(next);\n },\n [isControlled, handler],\n );\n\n const { setReference, setFloating, floatingRef, referenceRef } = useAnchoredFloating({\n open: isOpen,\n placement,\n offsetPx,\n matchReferenceWidth: matchTriggerWidth,\n });\n\n const value = useMemo(\n () => ({ open: isOpen, setOpen, dismiss, listId, setReference, setFloating, floatingRef, referenceRef }),\n [isOpen, setOpen, dismiss, listId, setReference, setFloating, floatingRef, referenceRef],\n );\n\n return <MenuContext.Provider value={value}>{children}</MenuContext.Provider>;\n};\n\ninterface IHandlerChildProps {\n onClick?: (event: React.MouseEvent<HTMLElement>) => void;\n ref?: React.Ref<HTMLElement>;\n}\n\n/**\n * Repassa ao filho o que ele recebe de fora, além do que injeta.\n *\n * O gatilho é o alvo natural de um `ControlledTooltip`, e o tooltip se prende clonando o filho e\n * injetando `onPointerEnter`. Aceitando só `children`, este componente engolia essa injeção e o\n * tooltip simplesmente nunca abria, mesmo com a ordem que a documentação manda usar\n * (tooltip POR FORA do handler). Os `onClick` são encadeados: o de fora roda antes de abrir.\n */\nexport const ControlledMenuHandler = React.forwardRef<\n HTMLElement,\n {\n children: React.ReactNode;\n /**\n * Leaves the popup ARIA to the child: set it when the real trigger is an element INSIDE the\n * child that owns its own role (the select's combobox input), so the wrapper does not\n * claim `aria-haspopup=\"menu\"` for a listbox.\n */\n skipTriggerAria?: boolean;\n } & React.HTMLAttributes<HTMLElement>\n>(({ children, onClick: onOuterClick, skipTriggerAria = false, ...rest }, forwardedRef) => {\n const { open, setOpen, setReference, listId } = useMenuContext();\n\n if (!isValidElement(children)) return <>{children}</>;\n\n const child = children as React.ReactElement<IHandlerChildProps>;\n const triggerAria = skipTriggerAria\n ? {}\n : { 'aria-haspopup': 'menu', 'aria-expanded': open, 'aria-controls': open ? listId : undefined };\n\n return cloneElement(child, {\n ...rest,\n ref: composeRefs<HTMLElement>(setReference, forwardedRef, (child as { ref?: React.Ref<HTMLElement> }).ref),\n ...triggerAria,\n onClick: (event: React.MouseEvent<HTMLElement>) => {\n onOuterClick?.(event);\n child.props.onClick?.(event);\n setOpen(!open);\n },\n } as IHandlerChildProps);\n});\n\nControlledMenuHandler.displayName = 'ControlledMenuHandler';\n\ninterface IControlledMenuListProps extends Omit<\n React.HTMLAttributes<HTMLDivElement>,\n 'className' | 'onDrag' | 'onDragStart' | 'onDragEnd' | 'onAnimationStart' | 'onAnimationEnd' | 'onAnimationIteration'\n> {\n children: React.ReactNode;\n className?: string;\n}\n\nconst FOCUSABLE_ITEM_SELECTOR = '[role=\"menuitem\"]:not([aria-disabled=\"true\"])';\n\nexport const ControlledMenuList = React.forwardRef<HTMLDivElement, IControlledMenuListProps>(\n ({ children, className, onClick, onKeyDown, ...rest }, forwardedRef) => {\n const { open, setOpen, dismiss, listId, setFloating, floatingRef, referenceRef } = useMenuContext();\n\n useEffect(() => {\n if (!open) return undefined;\n\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key !== 'Escape' || dismiss?.escapeKey === false) return;\n setOpen(false);\n };\n\n const handlePointerDown = (event: PointerEvent) => {\n if (dismiss?.outsidePress === false) return;\n const target = event.target as Node | null;\n if (!target) return;\n if (floatingRef.current?.contains(target) || referenceRef.current?.contains(target)) return;\n if (isPressOnLayeredSurface(target, floatingRef.current)) return;\n setOpen(false);\n };\n\n document.addEventListener('keydown', handleKeyDown);\n document.addEventListener('pointerdown', handlePointerDown, true);\n\n return () => {\n document.removeEventListener('keydown', handleKeyDown);\n document.removeEventListener('pointerdown', handlePointerDown, true);\n };\n }, [open, setOpen, dismiss, floatingRef, referenceRef]);\n\n const handleListKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n onKeyDown?.(event);\n if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return;\n\n const list = floatingRef.current;\n if (!list) return;\n\n const items = Array.from(list.querySelectorAll<HTMLElement>(FOCUSABLE_ITEM_SELECTOR));\n if (items.length === 0) return;\n\n event.preventDefault();\n const currentIndex = items.indexOf(document.activeElement as HTMLElement);\n const step = event.key === 'ArrowDown' ? 1 : -1;\n const nextIndex = currentIndex < 0 ? 0 : (currentIndex + step + items.length) % items.length;\n items[nextIndex]?.focus();\n };\n\n const portalHost = getPortalHost();\n if (portalHost === null) return null;\n\n return createPortal(\n <AnimatePresence>\n {open && (\n <motion.div\n key=\"menu-list\"\n id={listId}\n variants={floatingVariants}\n initial=\"hidden\"\n animate=\"visible\"\n exit=\"exit\"\n role=\"menu\"\n ref={(node) => {\n setFloating(node);\n if (typeof forwardedRef === 'function') forwardedRef(node);\n else if (forwardedRef) forwardedRef.current = node;\n }}\n onKeyDown={handleListKeyDown}\n onClick={(event) => {\n onClick?.(event);\n if (dismiss?.itemPress === false) return;\n const target = event.target as HTMLElement | null;\n if (target?.closest('[role=\"menuitem\"]')) setOpen(false);\n }}\n {...rest}\n className={twMerge(\n 'fixed left-0 top-0 z-[9999] min-w-44 overflow-auto rounded-lg border border-border p-1 text-sm text-foreground glass',\n className,\n )}\n >\n {children}\n </motion.div>\n )}\n </AnimatePresence>,\n portalHost,\n );\n },\n);\n\nControlledMenuList.displayName = 'ControlledMenuList';\n\ninterface IControlledMenuItemProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'className'> {\n children: React.ReactNode;\n className?: string;\n disabled?: boolean;\n}\n\nexport const ControlledMenuItem: React.FC<IControlledMenuItemProps> = ({\n children,\n className,\n disabled = false,\n onClick,\n ...rest\n}) => (\n <div\n role=\"menuitem\"\n tabIndex={disabled ? -1 : 0}\n aria-disabled={disabled || undefined}\n onClick={disabled ? undefined : onClick}\n onKeyDown={(event) => {\n if (disabled) return;\n if (event.key !== 'Enter' && event.key !== ' ') return;\n event.preventDefault();\n (event.currentTarget as HTMLElement).click();\n }}\n {...rest}\n className={twMerge(\n classNames(\n 'flex w-full cursor-pointer select-none items-center rounded-md px-3 py-2 text-sm outline-none',\n 'transition-colors hover:bg-muted-hover focus:bg-muted-hover',\n { 'pointer-events-none opacity-50': disabled },\n ),\n className,\n )}\n >\n {children}\n </div>\n);\n\ninterface IControlledMenuProps {\n trigger: React.ReactNode;\n items?: Array<IControlledMenuItem>;\n children?: React.ReactNode;\n triggerTooltip?: string;\n placement?: TMenuPlacement;\n open?: boolean;\n onOpenChange?: (open: boolean) => void;\n disabled?: boolean;\n dismissOnItemClick?: boolean;\n listClassName?: string;\n tooltipClassName?: string;\n listRef?: React.Ref<HTMLDivElement>;\n}\n\nconst ITEM_TONE_CLASS: Record<TMenuItemTone, string> = {\n default: 'text-foreground-muted',\n danger: 'text-danger-emphasis',\n};\n\nconst ICON_TONE_CLASS: Record<TMenuItemTone, string> = {\n default: 'text-foreground-subtle',\n danger: 'text-danger-emphasis',\n};\n\n/**\n * A tooltip on the trigger must sit OUTSIDE the handler: the handler clones its child to\n * inject the click handler and the reference ref, and a tooltip in that position swallows\n * both, so the menu stops opening.\n */\nconst ControlledMenu: React.FC<IControlledMenuProps> = ({\n trigger,\n items,\n children,\n triggerTooltip,\n placement = 'bottom-start',\n open,\n onOpenChange,\n disabled = false,\n dismissOnItemClick = true,\n listClassName,\n tooltipClassName,\n listRef,\n}) => {\n const menuItems = useMemo(() => (Array.isArray(items) ? items : []), [items]);\n const dismiss = useMemo(() => ({ itemPress: dismissOnItemClick }), [dismissOnItemClick]);\n const handle = <ControlledMenuHandler>{trigger}</ControlledMenuHandler>;\n\n if (disabled) return <>{trigger}</>;\n\n return (\n <ControlledMenuRoot open={open} handler={onOpenChange} placement={placement} dismiss={dismiss}>\n {triggerTooltip ? (\n <ControlledTooltip content={triggerTooltip} className={tooltipClassName}>\n {handle}\n </ControlledTooltip>\n ) : (\n handle\n )}\n\n <ControlledMenuList ref={listRef} className={listClassName}>\n {children ??\n menuItems.map((item) => {\n const tone = item.tone ?? 'default';\n\n return (\n <ControlledMenuItem\n key={item.key}\n disabled={item.disabled}\n onClick={item.onClick}\n className={classNames('items-start gap-x-2', ITEM_TONE_CLASS[tone])}\n >\n {item.icon && <MaterialSymbol icon={item.icon} size={16} className={ICON_TONE_CLASS[tone]} />}\n\n <span className=\"flex flex-col items-start gap-y-0.5\">\n <span>{item.label}</span>\n {item.hint && <span className=\"text-xs text-foreground-subtle\">{item.hint}</span>}\n </span>\n </ControlledMenuItem>\n );\n })}\n </ControlledMenuList>\n </ControlledMenuRoot>\n );\n};\n\nexport default ControlledMenu;\n"],"names":[],"mappings":";;;;;;;;;;;;;;AAuDA,MAAM,cAAc,cAAmC,IAAI;AAE3D,MAAM,iBAAiB,MAAoB;AACzC,QAAM,UAAU,WAAW,WAAW;AACtC,MAAI,CAAC,QAAS,OAAM,IAAI,MAAM,uDAAuD;AACrF,SAAO;AACT;AAYO,MAAM,qBAAyD,CAAC;AAAA,EACrE;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ,WAAW;AAAA,EACX;AAAA,EACA,oBAAoB;AACtB,MAAM;AACJ,QAAM,SAAS,MAAA;AACf,QAAM,CAAC,kBAAkB,mBAAmB,IAAI,SAAS,KAAK;AAE9D,QAAM,eAAe,SAAS;AAC9B,QAAM,SAAS,eAAe,QAAQ,IAAI,IAAI;AAE9C,QAAM,UAAU;AAAA,IACd,CAAC,SAAkB;AACjB,UAAI,CAAC,aAAc,qBAAoB,IAAI;AAC3C,gBAAU,IAAI;AAAA,IAChB;AAAA,IACA,CAAC,cAAc,OAAO;AAAA,EAAA;AAGxB,QAAM,EAAE,cAAc,aAAa,aAAa,aAAA,IAAiB,oBAAoB;AAAA,IACnF,MAAM;AAAA,IACN;AAAA,IACA;AAAA,IACA,qBAAqB;AAAA,EAAA,CACtB;AAED,QAAM,QAAQ;AAAA,IACZ,OAAO,EAAE,MAAM,QAAQ,SAAS,SAAS,QAAQ,cAAc,aAAa,aAAa;IACzF,CAAC,QAAQ,SAAS,SAAS,QAAQ,cAAc,aAAa,aAAa,YAAY;AAAA,EAAA;AAGzF,SAAO,oBAAC,YAAY,UAAZ,EAAqB,OAAe,SAAA,CAAS;AACvD;AAeO,MAAM,wBAAwB,MAAM,WAWzC,CAAC,EAAE,UAAU,SAAS,cAAc,kBAAkB,OAAO,GAAG,KAAA,GAAQ,iBAAiB;AACzF,QAAM,EAAE,MAAM,SAAS,cAAc,OAAA,IAAW,eAAA;AAEhD,MAAI,CAAC,eAAe,QAAQ,EAAG,wCAAU,UAAS;AAElD,QAAM,QAAQ;AACd,QAAM,cAAc,kBAChB,CAAA,IACA,EAAE,iBAAiB,QAAQ,iBAAiB,MAAM,iBAAiB,OAAO,SAAS,OAAA;AAEvF,SAAO,aAAa,OAAO;AAAA,IACzB,GAAG;AAAA,IACH,KAAK,YAAyB,cAAc,cAAe,MAA2C,GAAG;AAAA,IACzG,GAAG;AAAA,IACH,SAAS,CAAC,UAAyC;AACjD,qBAAe,KAAK;AACpB,YAAM,MAAM,UAAU,KAAK;AAC3B,cAAQ,CAAC,IAAI;AAAA,IACf;AAAA,EAAA,CACqB;AACzB,CAAC;AAED,sBAAsB,cAAc;AAUpC,MAAM,0BAA0B;AAEzB,MAAM,qBAAqB,MAAM;AAAA,EACtC,CAAC,EAAE,UAAU,WAAW,SAAS,WAAW,GAAG,KAAA,GAAQ,iBAAiB;AACtE,UAAM,EAAE,MAAM,SAAS,SAAS,QAAQ,aAAa,aAAa,aAAA,IAAiB,eAAA;AAEnF,cAAU,MAAM;AACd,UAAI,CAAC,KAAM,QAAO;AAElB,YAAM,gBAAgB,CAAC,UAAyB;AAC9C,YAAI,MAAM,QAAQ,YAAY,SAAS,cAAc,MAAO;AAC5D,gBAAQ,KAAK;AAAA,MACf;AAEA,YAAM,oBAAoB,CAAC,UAAwB;AACjD,YAAI,SAAS,iBAAiB,MAAO;AACrC,cAAM,SAAS,MAAM;AACrB,YAAI,CAAC,OAAQ;AACb,YAAI,YAAY,SAAS,SAAS,MAAM,KAAK,aAAa,SAAS,SAAS,MAAM,EAAG;AACrF,YAAI,wBAAwB,QAAQ,YAAY,OAAO,EAAG;AAC1D,gBAAQ,KAAK;AAAA,MACf;AAEA,eAAS,iBAAiB,WAAW,aAAa;AAClD,eAAS,iBAAiB,eAAe,mBAAmB,IAAI;AAEhE,aAAO,MAAM;AACX,iBAAS,oBAAoB,WAAW,aAAa;AACrD,iBAAS,oBAAoB,eAAe,mBAAmB,IAAI;AAAA,MACrE;AAAA,IACF,GAAG,CAAC,MAAM,SAAS,SAAS,aAAa,YAAY,CAAC;AAEtD,UAAM,oBAAoB,CAAC,UAA+C;AACxE,kBAAY,KAAK;AACjB,UAAI,MAAM,QAAQ,eAAe,MAAM,QAAQ,UAAW;AAE1D,YAAM,OAAO,YAAY;AACzB,UAAI,CAAC,KAAM;AAEX,YAAM,QAAQ,MAAM,KAAK,KAAK,iBAA8B,uBAAuB,CAAC;AACpF,UAAI,MAAM,WAAW,EAAG;AAExB,YAAM,eAAA;AACN,YAAM,eAAe,MAAM,QAAQ,SAAS,aAA4B;AACxE,YAAM,OAAO,MAAM,QAAQ,cAAc,IAAI;AAC7C,YAAM,YAAY,eAAe,IAAI,KAAK,eAAe,OAAO,MAAM,UAAU,MAAM;AACtF,YAAM,SAAS,GAAG,MAAA;AAAA,IACpB;AAEA,UAAM,aAAa,cAAA;AACnB,QAAI,eAAe,KAAM,QAAO;AAEhC,WAAO;AAAA,MACL,oBAAC,mBACE,UAAA,QACC;AAAA,QAAC,OAAO;AAAA,QAAP;AAAA,UAEC,IAAI;AAAA,UACJ,UAAU;AAAA,UACV,SAAQ;AAAA,UACR,SAAQ;AAAA,UACR,MAAK;AAAA,UACL,MAAK;AAAA,UACL,KAAK,CAAC,SAAS;AACb,wBAAY,IAAI;AAChB,gBAAI,OAAO,iBAAiB,WAAY,cAAa,IAAI;AAAA,qBAChD,2BAA2B,UAAU;AAAA,UAChD;AAAA,UACA,WAAW;AAAA,UACX,SAAS,CAAC,UAAU;AAClB,sBAAU,KAAK;AACf,gBAAI,SAAS,cAAc,MAAO;AAClC,kBAAM,SAAS,MAAM;AACrB,gBAAI,QAAQ,QAAQ,mBAAmB,WAAW,KAAK;AAAA,UACzD;AAAA,UACC,GAAG;AAAA,UACJ,WAAW;AAAA,YACT;AAAA,YACA;AAAA,UAAA;AAAA,UAGD;AAAA,QAAA;AAAA,QAzBG;AAAA,MAAA,GA4BV;AAAA,MACA;AAAA,IAAA;AAAA,EAEJ;AACF;AAEA,mBAAmB,cAAc;AAQ1B,MAAM,qBAAyD,CAAC;AAAA,EACrE;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX;AAAA,EACA,GAAG;AACL,MACE;AAAA,EAAC;AAAA,EAAA;AAAA,IACC,MAAK;AAAA,IACL,UAAU,WAAW,KAAK;AAAA,IAC1B,iBAAe,YAAY;AAAA,IAC3B,SAAS,WAAW,SAAY;AAAA,IAChC,WAAW,CAAC,UAAU;AACpB,UAAI,SAAU;AACd,UAAI,MAAM,QAAQ,WAAW,MAAM,QAAQ,IAAK;AAChD,YAAM,eAAA;AACL,YAAM,cAA8B,MAAA;AAAA,IACvC;AAAA,IACC,GAAG;AAAA,IACJ,WAAW;AAAA,MACT;AAAA,QACE;AAAA,QACA;AAAA,QACA,EAAE,kCAAkC,SAAA;AAAA,MAAS;AAAA,MAE/C;AAAA,IAAA;AAAA,IAGD;AAAA,EAAA;AACH;AAkBF,MAAM,kBAAiD;AAAA,EACrD,SAAS;AAAA,EACT,QAAQ;AACV;AAEA,MAAM,kBAAiD;AAAA,EACrD,SAAS;AAAA,EACT,QAAQ;AACV;AAOA,MAAM,iBAAiD,CAAC;AAAA,EACtD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX,qBAAqB;AAAA,EACrB;AAAA,EACA;AAAA,EACA;AACF,MAAM;AACJ,QAAM,YAAY,QAAQ,MAAO,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAA,GAAK,CAAC,KAAK,CAAC;AAC5E,QAAM,UAAU,QAAQ,OAAO,EAAE,WAAW,uBAAuB,CAAC,kBAAkB,CAAC;AACvF,QAAM,SAAS,oBAAC,uBAAA,EAAuB,UAAA,QAAA,CAAQ;AAE/C,MAAI,SAAU,QAAO,oBAAA,UAAA,EAAG,UAAA,SAAQ;AAEhC,8BACG,oBAAA,EAAmB,MAAY,SAAS,cAAc,WAAsB,SAC1E,UAAA;AAAA,IAAA,qCACE,mBAAA,EAAkB,SAAS,gBAAgB,WAAW,kBACpD,kBACH,IAEA;AAAA,IAGF,oBAAC,oBAAA,EAAmB,KAAK,SAAS,WAAW,eAC1C,UAAA,YACC,UAAU,IAAI,CAAC,SAAS;AACtB,YAAM,OAAO,KAAK,QAAQ;AAE1B,aACE;AAAA,QAAC;AAAA,QAAA;AAAA,UAEC,UAAU,KAAK;AAAA,UACf,SAAS,KAAK;AAAA,UACd,WAAW,WAAW,uBAAuB,gBAAgB,IAAI,CAAC;AAAA,UAEjE,UAAA;AAAA,YAAA,KAAK,QAAQ,oBAAC,gBAAA,EAAe,MAAM,KAAK,MAAM,MAAM,IAAI,WAAW,gBAAgB,IAAI,EAAA,CAAG;AAAA,YAE3F,qBAAC,QAAA,EAAK,WAAU,uCACd,UAAA;AAAA,cAAA,oBAAC,QAAA,EAAM,eAAK,MAAA,CAAM;AAAA,cACjB,KAAK,QAAQ,oBAAC,UAAK,WAAU,kCAAkC,eAAK,KAAA,CAAK;AAAA,YAAA,EAAA,CAC5E;AAAA,UAAA;AAAA,QAAA;AAAA,QAVK,KAAK;AAAA,MAAA;AAAA,IAahB,CAAC,EAAA,CACL;AAAA,EAAA,GACF;AAEJ;"}
1
+ {"version":3,"file":"ControlledMenu.js","sources":["../../../src/components/UI/ControlledMenu.tsx"],"sourcesContent":["import React, {\n cloneElement,\n createContext,\n isValidElement,\n useCallback,\n useContext,\n useEffect,\n useId,\n useMemo,\n useState,\n} from 'react';\nimport classNames from 'classnames';\nimport { AnimatePresence, motion } from 'motion/react';\nimport { createPortal } from 'react-dom';\nimport { twMerge } from 'tailwind-merge';\n\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { MaterialSymbol, SymbolCodepoints } from '@/components/UI/MaterialSymbol';\nimport { composeRefs } from '@/helpers/composeRefs';\nimport { isPressOnLayeredSurface, LAYERED_SURFACE_ATTRIBUTE } from '@/helpers/layeredSurfaces';\nimport { getPortalHost } from '@/helpers/portalHost';\nimport { TFloatingPlacement, useAnchoredFloating } from '@/hooks/useAnchoredFloating';\nimport { floatingVariants } from '@/motion';\n\nexport type TMenuPlacement = TFloatingPlacement;\nexport type TMenuItemTone = 'default' | 'danger';\n\nexport interface IMenuDismissConfig {\n /** Closes the menu when an item is pressed. Multi-select surfaces pass false. */\n itemPress?: boolean;\n escapeKey?: boolean;\n outsidePress?: boolean;\n}\n\nexport interface IControlledMenuItem {\n key: string;\n label: string;\n icon?: SymbolCodepoints;\n hint?: string;\n onClick?: () => void;\n disabled?: boolean;\n tone?: TMenuItemTone;\n}\n\ninterface IMenuContext {\n open: boolean;\n setOpen: (open: boolean) => void;\n dismiss?: IMenuDismissConfig;\n listId: string;\n setReference: (node: HTMLElement | null) => void;\n setFloating: (node: HTMLElement | null) => void;\n floatingRef: React.MutableRefObject<HTMLElement | null>;\n referenceRef: React.MutableRefObject<HTMLElement | null>;\n}\n\nconst MenuContext = createContext<IMenuContext | null>(null);\n\nconst useMenuContext = (): IMenuContext => {\n const context = useContext(MenuContext);\n if (!context) throw new Error('Menu parts must be rendered inside ControlledMenuRoot');\n return context;\n};\n\ninterface IControlledMenuRootProps {\n children: React.ReactNode;\n open?: boolean;\n handler?: (open: boolean) => void;\n placement?: TMenuPlacement;\n offsetPx?: number;\n dismiss?: IMenuDismissConfig;\n matchTriggerWidth?: boolean;\n}\n\nexport const ControlledMenuRoot: React.FC<IControlledMenuRootProps> = ({\n children,\n open,\n handler,\n placement = 'bottom-start',\n offsetPx = 6,\n dismiss,\n matchTriggerWidth = false,\n}) => {\n const listId = useId();\n const [uncontrolledOpen, setUncontrolledOpen] = useState(false);\n\n const isControlled = open !== undefined;\n const isOpen = isControlled ? Boolean(open) : uncontrolledOpen;\n\n const setOpen = useCallback(\n (next: boolean) => {\n if (!isControlled) setUncontrolledOpen(next);\n handler?.(next);\n },\n [isControlled, handler],\n );\n\n const { setReference, setFloating, floatingRef, referenceRef } = useAnchoredFloating({\n open: isOpen,\n placement,\n offsetPx,\n matchReferenceWidth: matchTriggerWidth,\n });\n\n const value = useMemo(\n () => ({ open: isOpen, setOpen, dismiss, listId, setReference, setFloating, floatingRef, referenceRef }),\n [isOpen, setOpen, dismiss, listId, setReference, setFloating, floatingRef, referenceRef],\n );\n\n return <MenuContext.Provider value={value}>{children}</MenuContext.Provider>;\n};\n\ninterface IHandlerChildProps {\n onClick?: (event: React.MouseEvent<HTMLElement>) => void;\n ref?: React.Ref<HTMLElement>;\n}\n\n/**\n * Repassa ao filho o que ele recebe de fora, além do que injeta.\n *\n * O gatilho é o alvo natural de um `ControlledTooltip`, e o tooltip se prende clonando o filho e\n * injetando `onPointerEnter`. Aceitando só `children`, este componente engolia essa injeção e o\n * tooltip simplesmente nunca abria, mesmo com a ordem que a documentação manda usar\n * (tooltip POR FORA do handler). Os `onClick` são encadeados: o de fora roda antes de abrir.\n */\nexport const ControlledMenuHandler = React.forwardRef<\n HTMLElement,\n {\n children: React.ReactNode;\n /**\n * Leaves the popup ARIA to the child: set it when the real trigger is an element INSIDE the\n * child that owns its own role (the select's combobox input), so the wrapper does not\n * claim `aria-haspopup=\"menu\"` for a listbox.\n */\n skipTriggerAria?: boolean;\n } & React.HTMLAttributes<HTMLElement>\n>(({ children, onClick: onOuterClick, skipTriggerAria = false, ...rest }, forwardedRef) => {\n const { open, setOpen, setReference, listId } = useMenuContext();\n\n if (!isValidElement(children)) return <>{children}</>;\n\n const child = children as React.ReactElement<IHandlerChildProps>;\n const triggerAria = skipTriggerAria\n ? {}\n : { 'aria-haspopup': 'menu', 'aria-expanded': open, 'aria-controls': open ? listId : undefined };\n\n return cloneElement(child, {\n ...rest,\n ref: composeRefs<HTMLElement>(setReference, forwardedRef, (child as { ref?: React.Ref<HTMLElement> }).ref),\n ...triggerAria,\n onClick: (event: React.MouseEvent<HTMLElement>) => {\n onOuterClick?.(event);\n child.props.onClick?.(event);\n setOpen(!open);\n },\n } as IHandlerChildProps);\n});\n\nControlledMenuHandler.displayName = 'ControlledMenuHandler';\n\ninterface IControlledMenuListProps extends Omit<\n React.HTMLAttributes<HTMLDivElement>,\n 'className' | 'onDrag' | 'onDragStart' | 'onDragEnd' | 'onAnimationStart' | 'onAnimationEnd' | 'onAnimationIteration'\n> {\n children: React.ReactNode;\n className?: string;\n}\n\nconst FOCUSABLE_ITEM_SELECTOR = '[role=\"menuitem\"]:not([aria-disabled=\"true\"])';\n\nexport const ControlledMenuList = React.forwardRef<HTMLDivElement, IControlledMenuListProps>(\n ({ children, className, onClick, onKeyDown, ...rest }, forwardedRef) => {\n const { open, setOpen, dismiss, listId, setFloating, floatingRef, referenceRef } = useMenuContext();\n\n useEffect(() => {\n if (!open) return undefined;\n\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key !== 'Escape' || dismiss?.escapeKey === false) return;\n setOpen(false);\n };\n\n const handlePointerDown = (event: PointerEvent) => {\n if (dismiss?.outsidePress === false) return;\n const target = event.target as Node | null;\n if (!target) return;\n if (floatingRef.current?.contains(target) || referenceRef.current?.contains(target)) return;\n if (isPressOnLayeredSurface(target, floatingRef.current)) return;\n setOpen(false);\n };\n\n document.addEventListener('keydown', handleKeyDown);\n document.addEventListener('pointerdown', handlePointerDown, true);\n\n return () => {\n document.removeEventListener('keydown', handleKeyDown);\n document.removeEventListener('pointerdown', handlePointerDown, true);\n };\n }, [open, setOpen, dismiss, floatingRef, referenceRef]);\n\n const handleListKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n onKeyDown?.(event);\n if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return;\n\n const list = floatingRef.current;\n if (!list) return;\n\n const items = Array.from(list.querySelectorAll<HTMLElement>(FOCUSABLE_ITEM_SELECTOR));\n if (items.length === 0) return;\n\n event.preventDefault();\n const currentIndex = items.indexOf(document.activeElement as HTMLElement);\n const step = event.key === 'ArrowDown' ? 1 : -1;\n const nextIndex = currentIndex < 0 ? 0 : (currentIndex + step + items.length) % items.length;\n items[nextIndex]?.focus();\n };\n\n const portalHost = getPortalHost();\n if (portalHost === null) return null;\n\n return createPortal(\n <AnimatePresence>\n {open && (\n <motion.div\n key=\"menu-list\"\n id={listId}\n variants={floatingVariants}\n initial=\"hidden\"\n animate=\"visible\"\n exit=\"exit\"\n role=\"menu\"\n {...LAYERED_SURFACE_ATTRIBUTE}\n ref={(node) => {\n setFloating(node);\n if (typeof forwardedRef === 'function') forwardedRef(node);\n else if (forwardedRef) forwardedRef.current = node;\n }}\n onKeyDown={handleListKeyDown}\n onClick={(event) => {\n onClick?.(event);\n if (dismiss?.itemPress === false) return;\n const target = event.target as HTMLElement | null;\n if (target?.closest('[role=\"menuitem\"]')) setOpen(false);\n }}\n {...rest}\n className={twMerge(\n 'fixed left-0 top-0 z-[9999] min-w-44 overflow-auto rounded-lg border border-border p-1 text-sm text-foreground glass',\n className,\n )}\n >\n {children}\n </motion.div>\n )}\n </AnimatePresence>,\n portalHost,\n );\n },\n);\n\nControlledMenuList.displayName = 'ControlledMenuList';\n\ninterface IControlledMenuItemProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'className'> {\n children: React.ReactNode;\n className?: string;\n disabled?: boolean;\n}\n\nexport const ControlledMenuItem: React.FC<IControlledMenuItemProps> = ({\n children,\n className,\n disabled = false,\n onClick,\n ...rest\n}) => (\n <div\n role=\"menuitem\"\n tabIndex={disabled ? -1 : 0}\n aria-disabled={disabled || undefined}\n onClick={disabled ? undefined : onClick}\n onKeyDown={(event) => {\n if (disabled) return;\n if (event.key !== 'Enter' && event.key !== ' ') return;\n event.preventDefault();\n (event.currentTarget as HTMLElement).click();\n }}\n {...rest}\n className={twMerge(\n classNames(\n 'flex w-full cursor-pointer select-none items-center rounded-md px-3 py-2 text-sm outline-none',\n 'transition-colors hover:bg-muted-hover focus:bg-muted-hover',\n { 'pointer-events-none opacity-50': disabled },\n ),\n className,\n )}\n >\n {children}\n </div>\n);\n\ninterface IControlledMenuProps {\n trigger: React.ReactNode;\n items?: Array<IControlledMenuItem>;\n children?: React.ReactNode;\n triggerTooltip?: string;\n placement?: TMenuPlacement;\n open?: boolean;\n onOpenChange?: (open: boolean) => void;\n disabled?: boolean;\n dismissOnItemClick?: boolean;\n listClassName?: string;\n tooltipClassName?: string;\n listRef?: React.Ref<HTMLDivElement>;\n}\n\nconst ITEM_TONE_CLASS: Record<TMenuItemTone, string> = {\n default: 'text-foreground-muted',\n danger: 'text-danger-emphasis',\n};\n\nconst ICON_TONE_CLASS: Record<TMenuItemTone, string> = {\n default: 'text-foreground-subtle',\n danger: 'text-danger-emphasis',\n};\n\n/**\n * A tooltip on the trigger must sit OUTSIDE the handler: the handler clones its child to\n * inject the click handler and the reference ref, and a tooltip in that position swallows\n * both, so the menu stops opening.\n */\nconst ControlledMenu: React.FC<IControlledMenuProps> = ({\n trigger,\n items,\n children,\n triggerTooltip,\n placement = 'bottom-start',\n open,\n onOpenChange,\n disabled = false,\n dismissOnItemClick = true,\n listClassName,\n tooltipClassName,\n listRef,\n}) => {\n const menuItems = useMemo(() => (Array.isArray(items) ? items : []), [items]);\n const dismiss = useMemo(() => ({ itemPress: dismissOnItemClick }), [dismissOnItemClick]);\n const handle = <ControlledMenuHandler>{trigger}</ControlledMenuHandler>;\n\n if (disabled) return <>{trigger}</>;\n\n return (\n <ControlledMenuRoot open={open} handler={onOpenChange} placement={placement} dismiss={dismiss}>\n {triggerTooltip ? (\n <ControlledTooltip content={triggerTooltip} className={tooltipClassName}>\n {handle}\n </ControlledTooltip>\n ) : (\n handle\n )}\n\n <ControlledMenuList ref={listRef} className={listClassName}>\n {children ??\n menuItems.map((item) => {\n const tone = item.tone ?? 'default';\n\n return (\n <ControlledMenuItem\n key={item.key}\n disabled={item.disabled}\n onClick={item.onClick}\n className={classNames('items-start gap-x-2', ITEM_TONE_CLASS[tone])}\n >\n {item.icon && <MaterialSymbol icon={item.icon} size={16} className={ICON_TONE_CLASS[tone]} />}\n\n <span className=\"flex flex-col items-start gap-y-0.5\">\n <span>{item.label}</span>\n {item.hint && <span className=\"text-xs text-foreground-subtle\">{item.hint}</span>}\n </span>\n </ControlledMenuItem>\n );\n })}\n </ControlledMenuList>\n </ControlledMenuRoot>\n );\n};\n\nexport default ControlledMenu;\n"],"names":[],"mappings":"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"}
@@ -1,3 +1,21 @@
1
+ /**
2
+ * Surfaces that portal to `document.body`: they land OUTSIDE their opener in the DOM while
3
+ * sitting visually ON TOP of it.
4
+ *
5
+ * A dismissible surface (drawer, popover, menu) must not read a press on one of these as
6
+ * "the user clicked outside, close me". Confirming a dialog or picking from a select's
7
+ * listbox is layered work happening ABOVE the surface, not a dismissal of it.
8
+ *
9
+ * A floating panel also carries `data-layered-surface`, whatever `role` its consumer gives it:
10
+ * the select renders its menu panel as `role="presentation"` (the real `role="listbox"` is the
11
+ * options container inside it), so a role-only selector missed the search field sitting in that
12
+ * panel and read a press on it as a press outside.
13
+ */
14
+ export declare const LAYERED_SURFACE_DATA_ATTRIBUTE = "data-layered-surface";
15
+ export declare const LAYERED_SURFACE_ATTRIBUTE: {
16
+ readonly "data-layered-surface": "";
17
+ };
18
+ export declare const LAYERED_SURFACE_SELECTOR = "[role=\"dialog\"],[role=\"menu\"],[role=\"tooltip\"],[role=\"listbox\"],[data-layered-surface]";
1
19
  /**
2
20
  * Whether the press landed on a surface stacked ABOVE `currentSurface`.
3
21
  *
@@ -1,4 +1,6 @@
1
- const LAYERED_SURFACE_SELECTOR = '[role="dialog"],[role="menu"],[role="tooltip"],[role="listbox"]';
1
+ const LAYERED_SURFACE_DATA_ATTRIBUTE = "data-layered-surface";
2
+ const LAYERED_SURFACE_ATTRIBUTE = { [LAYERED_SURFACE_DATA_ATTRIBUTE]: "" };
3
+ const LAYERED_SURFACE_SELECTOR = `[role="dialog"],[role="menu"],[role="tooltip"],[role="listbox"],[${LAYERED_SURFACE_DATA_ATTRIBUTE}]`;
2
4
  const ALWAYS_ON_TOP_SELECTOR = ".Toastify";
3
5
  const isPressOnLayeredSurface = (target, currentSurface) => {
4
6
  if (!(target instanceof Element)) return false;
@@ -9,6 +11,9 @@ const isPressOnLayeredSurface = (target, currentSurface) => {
9
11
  return Boolean(currentSurface.compareDocumentPosition(pressed) & Node.DOCUMENT_POSITION_FOLLOWING);
10
12
  };
11
13
  export {
14
+ LAYERED_SURFACE_ATTRIBUTE,
15
+ LAYERED_SURFACE_DATA_ATTRIBUTE,
16
+ LAYERED_SURFACE_SELECTOR,
12
17
  isPressOnLayeredSurface
13
18
  };
14
19
  //# sourceMappingURL=layeredSurfaces.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"layeredSurfaces.js","sources":["../../src/helpers/layeredSurfaces.ts"],"sourcesContent":["/**\n * Surfaces that portal to `document.body`: they land OUTSIDE their opener in the DOM while\n * sitting visually ON TOP of it.\n *\n * A dismissible surface (drawer, popover, menu) must not read a press on one of these as\n * \"the user clicked outside, close me\". Confirming a dialog or picking from a select's\n * listbox is layered work happening ABOVE the surface, not a dismissal of it.\n */\nconst LAYERED_SURFACE_SELECTOR = '[role=\"dialog\"],[role=\"menu\"],[role=\"tooltip\"],[role=\"listbox\"]';\n\n/**\n * Superfícies que estão acima de TUDO por construção, e não por posição no documento.\n *\n * O toast é o caso: ele vive num container próprio montado no topo da árvore do app, com\n * z-index máximo, e não é portado junto das outras superfícies. Então a comparação por ordem no\n * documento diz que ele vem ANTES de um drawer portado para o fim do `body`, e o drawer conclui\n * que o clique foi \"fora, feche-me\". Era isso que fazia o X do toast fechar o drawer do produto\n * junto com a notificação.\n *\n * Um toast nunca é dismissal de nada: ele aparece por conta própria, sobre o que estiver aberto.\n */\nconst ALWAYS_ON_TOP_SELECTOR = '.Toastify';\n\n/**\n * Whether the press landed on a surface stacked ABOVE `currentSurface`.\n *\n * The direction matters and checking only \"is it a layered surface\" is wrong: a select\n * opened inside a drawer sits above a panel that is itself `role=\"dialog\"`, so a press\n * anywhere in that drawer would look layered and the select would never close. Every one of\n * these surfaces portals to `document.body`, so the one appended LATER is the one painted on\n * top: document order is the stacking order.\n */\nexport const isPressOnLayeredSurface = (target: EventTarget | null, currentSurface?: Element | null): boolean => {\n if (!(target instanceof Element)) return false;\n if (target.closest(ALWAYS_ON_TOP_SELECTOR)) return true;\n\n const pressed = target.closest(LAYERED_SURFACE_SELECTOR);\n if (!pressed) return false;\n if (!currentSurface || pressed === currentSurface) return false;\n\n return Boolean(currentSurface.compareDocumentPosition(pressed) & Node.DOCUMENT_POSITION_FOLLOWING);\n};\n"],"names":[],"mappings":"AAQA,MAAM,2BAA2B;AAajC,MAAM,yBAAyB;AAWxB,MAAM,0BAA0B,CAAC,QAA4B,mBAA6C;AAC/G,MAAI,EAAE,kBAAkB,SAAU,QAAO;AACzC,MAAI,OAAO,QAAQ,sBAAsB,EAAG,QAAO;AAEnD,QAAM,UAAU,OAAO,QAAQ,wBAAwB;AACvD,MAAI,CAAC,QAAS,QAAO;AACrB,MAAI,CAAC,kBAAkB,YAAY,eAAgB,QAAO;AAE1D,SAAO,QAAQ,eAAe,wBAAwB,OAAO,IAAI,KAAK,2BAA2B;AACnG;"}
1
+ {"version":3,"file":"layeredSurfaces.js","sources":["../../src/helpers/layeredSurfaces.ts"],"sourcesContent":["/**\n * Surfaces that portal to `document.body`: they land OUTSIDE their opener in the DOM while\n * sitting visually ON TOP of it.\n *\n * A dismissible surface (drawer, popover, menu) must not read a press on one of these as\n * \"the user clicked outside, close me\". Confirming a dialog or picking from a select's\n * listbox is layered work happening ABOVE the surface, not a dismissal of it.\n *\n * A floating panel also carries `data-layered-surface`, whatever `role` its consumer gives it:\n * the select renders its menu panel as `role=\"presentation\"` (the real `role=\"listbox\"` is the\n * options container inside it), so a role-only selector missed the search field sitting in that\n * panel and read a press on it as a press outside.\n */\nexport const LAYERED_SURFACE_DATA_ATTRIBUTE = 'data-layered-surface';\nexport const LAYERED_SURFACE_ATTRIBUTE = { [LAYERED_SURFACE_DATA_ATTRIBUTE]: '' } as const;\n\nexport const LAYERED_SURFACE_SELECTOR = `[role=\"dialog\"],[role=\"menu\"],[role=\"tooltip\"],[role=\"listbox\"],[${LAYERED_SURFACE_DATA_ATTRIBUTE}]`;\n\n/**\n * Superfícies que estão acima de TUDO por construção, e não por posição no documento.\n *\n * O toast é o caso: ele vive num container próprio montado no topo da árvore do app, com\n * z-index máximo, e não é portado junto das outras superfícies. Então a comparação por ordem no\n * documento diz que ele vem ANTES de um drawer portado para o fim do `body`, e o drawer conclui\n * que o clique foi \"fora, feche-me\". Era isso que fazia o X do toast fechar o drawer do produto\n * junto com a notificação.\n *\n * Um toast nunca é dismissal de nada: ele aparece por conta própria, sobre o que estiver aberto.\n */\nconst ALWAYS_ON_TOP_SELECTOR = '.Toastify';\n\n/**\n * Whether the press landed on a surface stacked ABOVE `currentSurface`.\n *\n * The direction matters and checking only \"is it a layered surface\" is wrong: a select\n * opened inside a drawer sits above a panel that is itself `role=\"dialog\"`, so a press\n * anywhere in that drawer would look layered and the select would never close. Every one of\n * these surfaces portals to `document.body`, so the one appended LATER is the one painted on\n * top: document order is the stacking order.\n */\nexport const isPressOnLayeredSurface = (target: EventTarget | null, currentSurface?: Element | null): boolean => {\n if (!(target instanceof Element)) return false;\n if (target.closest(ALWAYS_ON_TOP_SELECTOR)) return true;\n\n const pressed = target.closest(LAYERED_SURFACE_SELECTOR);\n if (!pressed) return false;\n if (!currentSurface || pressed === currentSurface) return false;\n\n return Boolean(currentSurface.compareDocumentPosition(pressed) & Node.DOCUMENT_POSITION_FOLLOWING);\n};\n"],"names":[],"mappings":"AAaO,MAAM,iCAAiC;AACvC,MAAM,4BAA4B,EAAE,CAAC,8BAA8B,GAAG,GAAA;AAEtE,MAAM,2BAA2B,oEAAoE,8BAA8B;AAa1I,MAAM,yBAAyB;AAWxB,MAAM,0BAA0B,CAAC,QAA4B,mBAA6C;AAC/G,MAAI,EAAE,kBAAkB,SAAU,QAAO;AACzC,MAAI,OAAO,QAAQ,sBAAsB,EAAG,QAAO;AAEnD,QAAM,UAAU,OAAO,QAAQ,wBAAwB;AACvD,MAAI,CAAC,QAAS,QAAO;AACrB,MAAI,CAAC,kBAAkB,YAAY,eAAgB,QAAO;AAE1D,SAAO,QAAQ,eAAe,wBAAwB,OAAO,IAAI,KAAK,2BAA2B;AACnG;"}
@@ -1,4 +1,5 @@
1
1
  import { useRef, useEffect } from "react";
2
+ import { LAYERED_SURFACE_SELECTOR } from "../helpers/layeredSurfaces.js";
2
3
  const commitActiveElement = (container) => {
3
4
  const active = document.activeElement;
4
5
  if (active instanceof HTMLElement && container?.contains(active)) active.blur();
@@ -15,7 +16,7 @@ const useInlineEditor = (active, onStopEdit, onNavigate) => {
15
16
  const handlePointerDown = (event) => {
16
17
  const target = event.target instanceof Element ? event.target : null;
17
18
  if (!target || containerRef.current?.contains(target)) return;
18
- if (target.closest('[role="menu"], [role="listbox"]')) return;
19
+ if (target.closest(LAYERED_SURFACE_SELECTOR)) return;
19
20
  commitActiveElement(containerRef.current);
20
21
  onStopEdit();
21
22
  };
@@ -1 +1 @@
1
- {"version":3,"file":"useInlineEditor.js","sources":["../../src/hooks/useInlineEditor.ts"],"sourcesContent":["import React, { useEffect, useRef } from 'react';\n\n// Persiste editores que salvam no blur (ex.: ProductDataSheetField) antes do\n// container desmontar ao navegar/fechar.\nconst commitActiveElement = (container: HTMLElement | null) => {\n const active = document.activeElement;\n if (active instanceof HTMLElement && container?.contains(active)) active.blur();\n};\n\n// Wiring compartilhado dos editores inline: foca o primeiro controle ao abrir,\n// fecha no clique fora e navega entre campos editáveis com Tab / Shift+Tab.\nexport const useInlineEditor = (active: boolean, onStopEdit: () => void, onNavigate: (direction: 1 | -1) => void) => {\n const containerRef = useRef<HTMLDivElement>(null);\n\n useEffect(() => {\n if (!active) return;\n const focusable = containerRef.current?.querySelector<HTMLElement>('input, textarea, select, button, [tabindex]');\n focusable?.focus();\n }, [active]);\n\n useEffect(() => {\n if (!active) return undefined;\n\n const handlePointerDown = (event: PointerEvent) => {\n const target = event.target instanceof Element ? event.target : null;\n if (!target || containerRef.current?.contains(target)) return;\n // Menus dos selects renderizam em portal fora do container; interagir\n // com eles não encerra a edição.\n if (target.closest('[role=\"menu\"], [role=\"listbox\"]')) return;\n commitActiveElement(containerRef.current);\n onStopEdit();\n };\n\n document.addEventListener('pointerdown', handlePointerDown);\n return () => document.removeEventListener('pointerdown', handlePointerDown);\n }, [active, onStopEdit]);\n\n const handleKeyDown = (event: React.KeyboardEvent) => {\n if (event.key === 'Tab') {\n event.preventDefault();\n commitActiveElement(containerRef.current);\n onNavigate(event.shiftKey ? -1 : 1);\n } else if (event.key === 'Escape') {\n event.stopPropagation();\n commitActiveElement(containerRef.current);\n onStopEdit();\n }\n };\n\n return { containerRef, handleKeyDown };\n};\n"],"names":[],"mappings":";AAIA,MAAM,sBAAsB,CAAC,cAAkC;AAC7D,QAAM,SAAS,SAAS;AACxB,MAAI,kBAAkB,eAAe,WAAW,SAAS,MAAM,UAAU,KAAA;AAC3E;AAIO,MAAM,kBAAkB,CAAC,QAAiB,YAAwB,eAA4C;AACnH,QAAM,eAAe,OAAuB,IAAI;AAEhD,YAAU,MAAM;AACd,QAAI,CAAC,OAAQ;AACb,UAAM,YAAY,aAAa,SAAS,cAA2B,6CAA6C;AAChH,eAAW,MAAA;AAAA,EACb,GAAG,CAAC,MAAM,CAAC;AAEX,YAAU,MAAM;AACd,QAAI,CAAC,OAAQ,QAAO;AAEpB,UAAM,oBAAoB,CAAC,UAAwB;AACjD,YAAM,SAAS,MAAM,kBAAkB,UAAU,MAAM,SAAS;AAChE,UAAI,CAAC,UAAU,aAAa,SAAS,SAAS,MAAM,EAAG;AAGvD,UAAI,OAAO,QAAQ,iCAAiC,EAAG;AACvD,0BAAoB,aAAa,OAAO;AACxC,iBAAA;AAAA,IACF;AAEA,aAAS,iBAAiB,eAAe,iBAAiB;AAC1D,WAAO,MAAM,SAAS,oBAAoB,eAAe,iBAAiB;AAAA,EAC5E,GAAG,CAAC,QAAQ,UAAU,CAAC;AAEvB,QAAM,gBAAgB,CAAC,UAA+B;AACpD,QAAI,MAAM,QAAQ,OAAO;AACvB,YAAM,eAAA;AACN,0BAAoB,aAAa,OAAO;AACxC,iBAAW,MAAM,WAAW,KAAK,CAAC;AAAA,IACpC,WAAW,MAAM,QAAQ,UAAU;AACjC,YAAM,gBAAA;AACN,0BAAoB,aAAa,OAAO;AACxC,iBAAA;AAAA,IACF;AAAA,EACF;AAEA,SAAO,EAAE,cAAc,cAAA;AACzB;"}
1
+ {"version":3,"file":"useInlineEditor.js","sources":["../../src/hooks/useInlineEditor.ts"],"sourcesContent":["import React, { useEffect, useRef } from 'react';\n\nimport { LAYERED_SURFACE_SELECTOR } from '@/helpers/layeredSurfaces';\n\n// Persiste editores que salvam no blur (ex.: ProductDataSheetField) antes do\n// container desmontar ao navegar/fechar.\nconst commitActiveElement = (container: HTMLElement | null) => {\n const active = document.activeElement;\n if (active instanceof HTMLElement && container?.contains(active)) active.blur();\n};\n\n// Wiring compartilhado dos editores inline: foca o primeiro controle ao abrir,\n// fecha no clique fora e navega entre campos editáveis com Tab / Shift+Tab.\nexport const useInlineEditor = (active: boolean, onStopEdit: () => void, onNavigate: (direction: 1 | -1) => void) => {\n const containerRef = useRef<HTMLDivElement>(null);\n\n useEffect(() => {\n if (!active) return;\n const focusable = containerRef.current?.querySelector<HTMLElement>('input, textarea, select, button, [tabindex]');\n focusable?.focus();\n }, [active]);\n\n useEffect(() => {\n if (!active) return undefined;\n\n const handlePointerDown = (event: PointerEvent) => {\n const target = event.target instanceof Element ? event.target : null;\n if (!target || containerRef.current?.contains(target)) return;\n // Select menus portal outside the container; interacting with them (including the search\n // field, which sits in the panel but outside the listbox) does not end the edit.\n if (target.closest(LAYERED_SURFACE_SELECTOR)) return;\n commitActiveElement(containerRef.current);\n onStopEdit();\n };\n\n document.addEventListener('pointerdown', handlePointerDown);\n return () => document.removeEventListener('pointerdown', handlePointerDown);\n }, [active, onStopEdit]);\n\n const handleKeyDown = (event: React.KeyboardEvent) => {\n if (event.key === 'Tab') {\n event.preventDefault();\n commitActiveElement(containerRef.current);\n onNavigate(event.shiftKey ? -1 : 1);\n } else if (event.key === 'Escape') {\n event.stopPropagation();\n commitActiveElement(containerRef.current);\n onStopEdit();\n }\n };\n\n return { containerRef, handleKeyDown };\n};\n"],"names":[],"mappings":";;AAMA,MAAM,sBAAsB,CAAC,cAAkC;AAC7D,QAAM,SAAS,SAAS;AACxB,MAAI,kBAAkB,eAAe,WAAW,SAAS,MAAM,UAAU,KAAA;AAC3E;AAIO,MAAM,kBAAkB,CAAC,QAAiB,YAAwB,eAA4C;AACnH,QAAM,eAAe,OAAuB,IAAI;AAEhD,YAAU,MAAM;AACd,QAAI,CAAC,OAAQ;AACb,UAAM,YAAY,aAAa,SAAS,cAA2B,6CAA6C;AAChH,eAAW,MAAA;AAAA,EACb,GAAG,CAAC,MAAM,CAAC;AAEX,YAAU,MAAM;AACd,QAAI,CAAC,OAAQ,QAAO;AAEpB,UAAM,oBAAoB,CAAC,UAAwB;AACjD,YAAM,SAAS,MAAM,kBAAkB,UAAU,MAAM,SAAS;AAChE,UAAI,CAAC,UAAU,aAAa,SAAS,SAAS,MAAM,EAAG;AAGvD,UAAI,OAAO,QAAQ,wBAAwB,EAAG;AAC9C,0BAAoB,aAAa,OAAO;AACxC,iBAAA;AAAA,IACF;AAEA,aAAS,iBAAiB,eAAe,iBAAiB;AAC1D,WAAO,MAAM,SAAS,oBAAoB,eAAe,iBAAiB;AAAA,EAC5E,GAAG,CAAC,QAAQ,UAAU,CAAC;AAEvB,QAAM,gBAAgB,CAAC,UAA+B;AACpD,QAAI,MAAM,QAAQ,OAAO;AACvB,YAAM,eAAA;AACN,0BAAoB,aAAa,OAAO;AACxC,iBAAW,MAAM,WAAW,KAAK,CAAC;AAAA,IACpC,WAAW,MAAM,QAAQ,UAAU;AACjC,YAAM,gBAAA;AACN,0BAAoB,aAAa,OAAO;AACxC,iBAAA;AAAA,IACF;AAAA,EACF;AAEA,SAAO,EAAE,cAAc,cAAA;AACzB;"}
package/dist/index.js CHANGED
@@ -42,7 +42,7 @@ import { default as default36 } from "./components/UI/NoDataFound/index.js";
42
42
  import { default as default37 } from "./components/UI/SkeletonLoading.js";
43
43
  import { default as default38 } from "./components/UI/UserMenu.js";
44
44
  import { composeRefs } from "./helpers/composeRefs.js";
45
- import { isPressOnLayeredSurface } from "./helpers/layeredSurfaces.js";
45
+ import { LAYERED_SURFACE_ATTRIBUTE, LAYERED_SURFACE_DATA_ATTRIBUTE, LAYERED_SURFACE_SELECTOR, isPressOnLayeredSurface } from "./helpers/layeredSurfaces.js";
46
46
  import { readThemeToken, useThemeChartTokens } from "./helpers/themeTokens.js";
47
47
  import { configureUiLogger, uiLogger } from "./helpers/uiLogger.js";
48
48
  import { configureUiPreferences, readUiPreference, resolveUiPreferenceScopeId, writeUiPreference } from "./helpers/uiPreferences.js";
@@ -86,6 +86,9 @@ export {
86
86
  default33 as CustomCard,
87
87
  default14 as ErrorPage,
88
88
  default34 as InformationCard,
89
+ LAYERED_SURFACE_ATTRIBUTE,
90
+ LAYERED_SURFACE_DATA_ATTRIBUTE,
91
+ LAYERED_SURFACE_SELECTOR,
89
92
  default15 as LinkButton,
90
93
  default35 as LoadingSpinner,
91
94
  MOTION_OFFSET_PX,
@@ -460,7 +460,12 @@ declare const _default: {
460
460
  "logout": "Sign out",
461
461
  "openSidebar": "Open sidebar",
462
462
  "collapseSidebar": "Collapse sidebar",
463
+ "favorites": {
464
+ "add": "Add to favorites",
465
+ "remove": "Remove from favorites"
466
+ },
463
467
  "sections": {
468
+ "favorites": "Favorites",
464
469
  "management": "Management",
465
470
  "registration": "Records"
466
471
  }
@@ -25,7 +25,7 @@ const pageTitle = { "breadcrumbLabel": "Breadcrumb trail" };
25
25
  const pagination = { "resultsPerPage": "Results per page", "pageOf": "Page {{current}} of {{total}}", "range": "{{from}}-{{to}} of {{total}}", "first": "First page", "last": "Last page", "gap": "..." };
26
26
  const permissions = { "deniedAction": "This action is not open to your profile. An account administrator can grant access.", "deniedRequest": "This action is not open to your user profile.", "checkingAccess": "Checking permissions...", "deniedPage": { "resource": "Page: {{resource}}", "backToHome": "Back to start", "nextStepsTitle": "What to do now", "service": { "title": "This module is not part of your account", "message": "The module behind this page is not among the services your account has, so no user in it can open the page.", "nextStep": "Talk to whoever owns the account at uMode to add this module. Everything else you already use keeps working." }, "capability": { "title": "This page is not open to you yet", "message": "Your account has this page, but your user profile does not include permission to open it yet.", "nextStep": "An account administrator can grant it in Settings, Access Control, under your user profile. It takes seconds and applies from your next visit." }, "request": { "title": "This page needs a permission you do not have yet", "message": "You got this far, but while loading this screen we found that your user profile does not have access to its information.", "nextStep": "An account administrator can grant it in Settings, Access Control. If access was just granted, use Try again. None of your work was affected." } } };
27
27
  const requestErrors = { "denied": { "title": "This area is not open to you yet", "description": "Your user profile does not include access to this content. An account administrator can grant it in Settings, Access Control." }, "notFound": { "title": "We could not find this item", "description": "It may have been deleted by someone on the team, or the link that brought you here is out of date." }, "offline": { "title": "The connection seems to have dropped", "description": "Check your connection and try again in a few seconds. Nothing you had already saved was lost." }, "server": { "title": "Something is unstable on our side", "description": "The problem is on our end, not something you did. Try again in a moment; if it keeps happening, contact support." }, "unknown": { "title": "We could not load this part", "description": "This is usually temporary. Try again and, if it stays like this, contact support and tell them what you were doing." } };
28
- const sidebar = { "home": "Home", "logout": "Sign out", "openSidebar": "Open sidebar", "collapseSidebar": "Collapse sidebar", "sections": { "management": "Management", "registration": "Records" } };
28
+ const sidebar = { "home": "Home", "logout": "Sign out", "openSidebar": "Open sidebar", "collapseSidebar": "Collapse sidebar", "favorites": { "add": "Add to favorites", "remove": "Remove from favorites" }, "sections": { "favorites": "Favorites", "management": "Management", "registration": "Records" } };
29
29
  const theme = { "toggle": "Theme", "light": "Light theme", "dark": "Dark theme", "system": "Follow system" };
30
30
  const toast = { "learnMore": "Learn more" };
31
31
  const unifiedSearchBar = { "clearFilter": "Clear filter" };
@@ -462,7 +462,12 @@ export declare const uiLocales: {
462
462
  logout: string;
463
463
  openSidebar: string;
464
464
  collapseSidebar: string;
465
+ favorites: {
466
+ add: string;
467
+ remove: string;
468
+ };
465
469
  sections: {
470
+ favorites: string;
466
471
  management: string;
467
472
  registration: string;
468
473
  };
@@ -975,7 +980,12 @@ export declare const uiLocales: {
975
980
  logout: string;
976
981
  openSidebar: string;
977
982
  collapseSidebar: string;
983
+ favorites: {
984
+ add: string;
985
+ remove: string;
986
+ };
978
987
  sections: {
988
+ favorites: string;
979
989
  management: string;
980
990
  registration: string;
981
991
  };
@@ -460,7 +460,12 @@ declare const _default: {
460
460
  "logout": "Sair",
461
461
  "openSidebar": "Abrir barra lateral",
462
462
  "collapseSidebar": "Recolher barra lateral",
463
+ "favorites": {
464
+ "add": "Adicionar aos favoritos",
465
+ "remove": "Remover dos favoritos"
466
+ },
463
467
  "sections": {
468
+ "favorites": "Favoritos",
464
469
  "management": "Gestão",
465
470
  "registration": "Cadastros"
466
471
  }
@@ -25,7 +25,7 @@ const pageTitle = { "breadcrumbLabel": "Trilha de navegação" };
25
25
  const pagination = { "resultsPerPage": "Resultados por página", "pageOf": "Página {{current}} de {{total}}", "range": "{{from}}-{{to}} de {{total}}", "first": "Primeira página", "last": "Última página", "gap": "..." };
26
26
  const permissions = { "deniedAction": "Esta ação não está liberada para o seu perfil. Um administrador da conta pode liberar o acesso.", "deniedRequest": "Esta ação não está liberada para o seu perfil de usuário.", "checkingAccess": "Verificando permissões...", "deniedPage": { "resource": "Página: {{resource}}", "backToHome": "Voltar para o início", "nextStepsTitle": "O que fazer agora", "service": { "title": "Este módulo não faz parte da sua conta", "message": "O módulo que abre esta página ainda não está entre os serviços contratados pela sua conta, então ele não pode ser aberto por nenhum usuário dela.", "nextStep": "Fale com a pessoa responsável pela conta na uMode para incluir este módulo. Enquanto isso, o restante do que você já usa segue funcionando normalmente." }, "capability": { "title": "Esta página ainda não está liberada para você", "message": "A sua conta tem esta página, mas o seu perfil de usuário ainda não inclui a permissão para abri-la.", "nextStep": "Um administrador da conta pode liberar em Configurações, Controle de Acesso, escolhendo o seu perfil de usuário. Leva alguns segundos e vale a partir do próximo acesso." }, "request": { "title": "Esta página precisa de uma permissão que você ainda não tem", "message": "Você chegou até aqui, mas ao buscar as informações desta tela percebemos que o seu perfil de usuário não tem acesso a elas.", "nextStep": "Um administrador da conta pode liberar em Configurações, Controle de Acesso. Se o acesso acabou de ser concedido, use Tentar novamente. Nada do seu trabalho foi afetado." } } };
27
27
  const requestErrors = { "denied": { "title": "Esta área ainda não está liberada para você", "description": "Seu perfil de usuário não inclui o acesso a este conteúdo. Um administrador da conta pode liberar em Configurações, Controle de Acesso." }, "notFound": { "title": "Não encontramos este item", "description": "Ele pode ter sido excluído por alguém da equipe, ou o link usado para chegar até aqui está desatualizado." }, "offline": { "title": "Parece que a conexão caiu", "description": "Confira sua conexão e tente de novo em alguns segundos. Nada do que já foi salvo se perdeu." }, "server": { "title": "Estamos com uma instabilidade", "description": "O problema é aqui do nosso lado, não em algo que você fez. Tente de novo em alguns instantes; se continuar, fale com o suporte." }, "unknown": { "title": "Não conseguimos carregar esta parte", "description": "Isso costuma ser passageiro. Tente de novo e, se continuar assim, fale com o suporte contando o que você estava fazendo." } };
28
- const sidebar = { "home": "Início", "logout": "Sair", "openSidebar": "Abrir barra lateral", "collapseSidebar": "Recolher barra lateral", "sections": { "management": "Gestão", "registration": "Cadastros" } };
28
+ const sidebar = { "home": "Início", "logout": "Sair", "openSidebar": "Abrir barra lateral", "collapseSidebar": "Recolher barra lateral", "favorites": { "add": "Adicionar aos favoritos", "remove": "Remover dos favoritos" }, "sections": { "favorites": "Favoritos", "management": "Gestão", "registration": "Cadastros" } };
29
29
  const theme = { "toggle": "Tema", "light": "Tema claro", "dark": "Tema escuro", "system": "Seguir o sistema" };
30
30
  const toast = { "learnMore": "Saiba Mais" };
31
31
  const unifiedSearchBar = { "clearFilter": "Limpar Filtro" };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umodeapporg/ui",
3
- "version": "0.13.2",
3
+ "version": "0.14.0",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "sideEffects": [