@umodeapporg/ui 0.13.3 → 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":";;;;;;;AAsBO,MAAM,uBAAuB,CAAC,SAAmC,KAAK,MAAM,GAAG,KAAK,EAAE,IAAI,KAAK,IAAI;AAgB1G,MAAM,eAAe,CAAC,UAA4B,QAAgB,MAAM,UAAmB;AACzF,MAAI,CAAC,UAAU,WAAW,IAAK,QAAO;AAEtC,MAAI,OAAO,SAAS,GAAG,GAAG;AACxB,UAAM,CAAC,UAAU,SAAS,IAAI,OAAO,MAAM,GAAG;AAC9C,UAAM,WAAW,MACb,SAAS,aAAa,WACtB,SAAS,aAAa,YAAY,SAAS,SAAS,WAAW,GAAG,QAAQ,GAAG;AACjF,QAAI,CAAC,SAAU,QAAO;AAEtB,UAAM,gBAAgB,IAAI,gBAAgB,SAAS,MAAM;AACzD,UAAM,iBAAiB,IAAI,gBAAgB,SAAS;AAEpD,WAAO,MAAM,KAAK,eAAe,QAAA,CAAS,EAAE,MAAM,CAAC,CAAC,KAAK,KAAK,MAAM,cAAc,IAAI,GAAG,MAAM,KAAK;AAAA,EACtG;AAEA,SAAO,MACH,SAAS,aAAa,SACtB,SAAS,aAAa,UAAU,SAAS,SAAS,WAAW,GAAG,MAAM,GAAG;AAC/E;AAEA,MAAM,sBAAsB,CAAC,MAAwB,aACnD,KAAK,UAAU,SACX,KAAK,SAAS,KAAK,CAAC,UAAU,oBAAoB,OAAO,QAAQ,CAAC,IAClE,aAAa,UAAU,KAAK,IAAI,KAAK,GAAG;AAa9C,MAAM,iBAAiD,CAAC;AAAA,EACtD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MAAM;AACJ,QAAM,EAAE,UAAU,MAAM,eAAA,IAAmB,gBAAA;AAC3C,QAAM,aAAa,OAAuB,IAAI;AAC9C,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,KAAK;AAEhD,QAAM,cAAc,QAAQ,KAAK,UAAU,MAAM;AACjD,QAAM,WAAW,cAAc,oBAAoB,MAAM,QAAQ,IAAI,aAAa,UAAU,KAAK,IAAI,KAAK,GAAG;AAC7G,QAAM,cAAc,UAAU,CAAC;AAE/B,YAAU,MAAM;AACd,UAAM,UAAU,WAAW;AAC3B,QAAI,CAAC,QAAS;AAEd,QAAI,CAAC,aAAa;AAChB,mBAAa,KAAK;AAClB;AAAA,IACF;AAEA,UAAM,eAAe,MAAM,aAAa,GAAG,QAAQ,YAAY,IAAI;AACnE,iBAAA;AAEA,UAAM,WAAW,IAAI,eAAe,YAAY;AAChD,aAAS,QAAQ,OAAO;AAExB,WAAO,MAAM,SAAS,WAAA;AAAA,EACxB,GAAG,CAAC,WAAW,CAAC;AAEhB,QAAM,eAAe;AAAA,IACnB;AAAA,IACA,WAAW,cAAc;AAAA,EAAA;AAG3B,QAAM,gBAAgB;AAAA,IACpB;AAAA,IACA,WAAY,mBAAmB,wBAAyB;AAAA,EAAA;AAG1D,QAAM,YAAY,WAChB,oBAAC,QAAA,EAAK,eAAW,MAAC,WAAU,4EAA2E,IACrG;AAEJ,QAAM,OAAO,oBAAC,gBAAA,EAAe,MAAM,KAAK,MAAM,MAAM,IAAI,QAAQ,KAAK,UAAU,KAAK,WAAW,eAAe;AAC9G,QAAM,QAAQ,oBAAC,QAAA,EAAK,WAAW,WAAW,yBAAyB,EAAE,gBAAgB,UAAA,CAAW,GAAI,UAAA,KAAK,KAAA,CAAK;AAE9G,MAAI,KAAK,aAAa;AACpB,WACE,oBAAC,MAAA,EAAG,WAAU,iBACZ,UAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,MAAM,KAAK;AAAA,QACX,QAAO;AAAA,QACP,KAAI;AAAA,QACJ,WAAW;AAAA,QACX,SAAS;AAAA,QAER,UAAA;AAAA,UAAA;AAAA,UACA;AAAA,UACA;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA,GAEL;AAAA,EAEJ;AAEA,MAAI,aAAa;AACf,WACE,qBAAC,MAAA,EAAG,WAAU,iBACZ,UAAA;AAAA,MAAA,qBAAC,kBAAA,EAAiB,mBAAiB,MAAC,SAAS,UAAU,iBAAe,aAAa,WAAW,cAC3F,UAAA;AAAA,QAAA;AAAA,QACA;AAAA,QACA;AAAA,QACD;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,MAAK;AAAA,YACL,MAAM;AAAA,YACN,WAAW,WAAW,2DAA2D;AAAA,cAC/E,gBAAgB;AAAA,cAChB,WAAW;AAAA,YAAA,CACZ;AAAA,UAAA;AAAA,QAAA;AAAA,MACH,GACF;AAAA,0BAEC,OAAA,EAAI,OAAO,EAAE,UAAA,GAAa,WAAW,WAAW,mBAAmB,EAAE,QAAQ,YAAA,CAAa,GACzF,UAAA,oBAAC,SAAI,KAAK,YAAY,WAAU,QAC9B,UAAA;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,OAAO,KAAK,YAAY,CAAA;AAAA,UACxB;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA,OAAO,QAAQ;AAAA,QAAA;AAAA,MAAA,GAEnB,EAAA,CACF;AAAA,IAAA,GACF;AAAA,EAEJ;AAEA,SACE,oBAAC,MAAA,EAAG,WAAU,iBACZ,UAAA,qBAAC,gBAAA,EAAe,IAAI,KAAK,IAAI,WAAW,cAAc,SAAS,YAC5D,UAAA;AAAA,IAAA;AAAA,IACA;AAAA,IACA;AAAA,EAAA,EAAA,CACH,EAAA,CACF;AAEJ;AAEA,MAAM,qBAAwD;AAAA,EAC5D,YAAY;AAAA,EACZ,cAAc;AAChB;AAQA,MAAM,uBAA6D,CAAC,EAAE,SAAS,WAAW,kBAAkB;AAC1G,QAAM,EAAE,EAAA,IAAM,eAAe,IAAI;AAEjC,MAAI,WAAW;AACb,QAAI,CAAC,YAAa,QAAO;AACzB,WACE,oBAAC,MAAA,EAAG,eAAW,MAAC,WAAU,sBACxB,UAAA,oBAAC,QAAA,EAAK,WAAU,4BAAA,CAA4B,EAAA,CAC9C;AAAA,EAEJ;AAEA,SACE,qBAAC,QAAG,WAAW,WAAW,iBAAiB,cAAc,SAAS,MAAM,GACrE,UAAA;AAAA,IAAA,eAAe,oBAAC,QAAA,EAAK,eAAW,MAAC,WAAU,uCAAsC;AAAA,IAClF,oBAAC,UAAK,WAAU,qFACb,YAAE,mBAAmB,OAAO,CAAC,EAAA,CAChC;AAAA,EAAA,GACF;AAEJ;AAEA,MAAM,gBAA0C,CAAC,cAAc,cAAc;AAO7E,MAAM,sBAAsB,CAAC,UAA6D;AACxF,QAAM,uBAAuB,SAAS,CAAA,GAAI,OAAO,CAAC,SAAS,CAAC,KAAK,OAAO;AACxE,QAAM,gBAAgB,cAAc,IAAI,CAAC,aAAa;AAAA,IACpD;AAAA,IACA,QAAQ,SAAS,IAAI,OAAO,CAAC,SAAS,KAAK,YAAY,OAAO;AAAA,EAAA,EAC9D,EAAE,OAAO,CAAC,UAAU,MAAM,MAAM,SAAS,CAAC;AAE5C,SAAO,oBAAoB,SAAS,CAAC,EAAE,SAAS,MAAM,OAAO,oBAAA,GAAuB,GAAG,aAAa,IAAI;AAC1G;AAEA,MAAM,cAA2C,CAAC;AAAA,EAChD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAQ;AACV,MAAM;AACJ,QAAM,EAAE,SAAA,IAAa,gBAAA;AAErB,QAAM,YACJ,MACG,OAAO,CAAC,SAAS,QAAQ,KAAK,UAAU,MAAM,KAAK,oBAAoB,MAAM,QAAQ,CAAC,EACtF,IAAI,oBAAoB,EAAE,CAAC,KAAK;AAErC,QAAM,CAAC,SAAS,UAAU,IAAI,SAAwB,SAAS;AAC/D,QAAM,oBAAoB,OAAO,SAAS;AAE1C,YAAU,MAAM;AACd,QAAI,aAAa,cAAc,kBAAkB,SAAS;AACxD,iBAAW,SAAS;AAAA,IACtB;AACA,sBAAkB,UAAU;AAAA,EAC9B,GAAG,CAAC,SAAS,CAAC;AAEd,QAAM,eAAe,CAAC,QAAgB;AACpC,eAAW,CAAC,YAAa,YAAY,MAAM,OAAO,GAAI;AAAA,EACxD;AAEA,QAAM,eAAe,UAAU;AAC/B,QAAM,SAAS,eAAe,oBAAoB,KAAK,IAAI,CAAC,EAAE,SAAS,MAAM,OAAO,SAAS,CAAA,GAAI;AAEjG,SACE,oBAAC,MAAA,EAAG,WAAW,WAAW,iBAAiB,UAAU,IAAI,YAAY,SAAS,GAC3E,UAAA,OAAO,QAAQ,CAAC,OAAO,eAAe;AACrC,UAAM,OAAkC,CAAA;AAExC,QAAI,MAAM,SAAS;AACjB,WAAK;AAAA,QACH;AAAA,UAAC;AAAA,UAAA;AAAA,YAEC,SAAS,MAAM;AAAA,YACf;AAAA,YACA,aAAa,aAAa;AAAA,UAAA;AAAA,UAHrB,WAAW,MAAM,OAAO;AAAA,QAAA;AAAA,MAI/B;AAAA,IAEJ;AAEA,UAAM,MAAM,QAAQ,CAAC,SAAS;AAC5B,YAAM,SAAS,qBAAqB,IAAI;AAExC,WAAK;AAAA,QACH;AAAA,UAAC;AAAA,UAAA;AAAA,YAEC;AAAA,YACA,QAAQ,YAAY;AAAA,YACpB;AAAA,YACA,UAAU,MAAM,aAAa,MAAM;AAAA,YACnC;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,UAAA;AAAA,UARK;AAAA,QAAA;AAAA,MASP;AAAA,IAEJ,CAAC;AAED,WAAO;AAAA,EACT,CAAC,EAAA,CACH;AAEJ;"}
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;"}
@@ -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.3",
3
+ "version": "0.14.0",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "sideEffects": [