@umodeapporg/ui 0.13.3 → 0.15.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.
- package/dist/components/Shell/AppSidebar.js +20 -10
- package/dist/components/Shell/AppSidebar.js.map +1 -1
- package/dist/components/Shell/SidebarMenu.d.ts +1 -1
- package/dist/components/Shell/SidebarMenu.js +32 -6
- package/dist/components/Shell/SidebarMenu.js.map +1 -1
- package/dist/components/Shell/sidebarFavorites.d.ts +21 -0
- package/dist/components/Shell/sidebarFavorites.js +69 -0
- package/dist/components/Shell/sidebarFavorites.js.map +1 -0
- package/dist/components/UI/DataTable/DataTable.js +15 -4
- package/dist/components/UI/DataTable/DataTable.js.map +1 -1
- package/dist/components/UI/DataTable/DataTableBody.d.ts +5 -1
- package/dist/components/UI/DataTable/DataTableBody.js +15 -2
- package/dist/components/UI/DataTable/DataTableBody.js.map +1 -1
- package/dist/components/UI/DataTable/DataTableBodyCell.js +11 -0
- package/dist/components/UI/DataTable/DataTableBodyCell.js.map +1 -1
- package/dist/components/UI/DataTable/cells/DataTableEditableCell.js +7 -10
- package/dist/components/UI/DataTable/cells/DataTableEditableCell.js.map +1 -1
- package/dist/components/UI/DataTable/grid/GridProvider.d.ts +3 -0
- package/dist/components/UI/DataTable/grid/GridProvider.js +25 -3
- package/dist/components/UI/DataTable/grid/GridProvider.js.map +1 -1
- package/dist/components/UI/DataTable/grid/gridStore.d.ts +10 -2
- package/dist/components/UI/DataTable/grid/gridStore.js +30 -3
- package/dist/components/UI/DataTable/grid/gridStore.js.map +1 -1
- package/dist/components/UI/DataTable/grid/gridTypes.d.ts +17 -5
- package/dist/components/UI/DataTable/grid/gridTypes.js.map +1 -1
- package/dist/components/UI/DataTable/grid/useGridKeyboard.d.ts +4 -2
- package/dist/components/UI/DataTable/grid/useGridKeyboard.js +25 -11
- package/dist/components/UI/DataTable/grid/useGridKeyboard.js.map +1 -1
- package/dist/locales/en/ui.json.d.ts +5 -0
- package/dist/locales/en/ui.json.js +1 -1
- package/dist/locales/index.d.ts +10 -0
- package/dist/locales/pt/ui.json.d.ts +5 -0
- package/dist/locales/pt/ui.json.js +1 -1
- package/package.json +1 -1
|
@@ -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 <
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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;"}
|
|
@@ -235,7 +235,7 @@ const DataTable = ({
|
|
|
235
235
|
() => !!config.editing?.onCellChange && config.columns.some((columnConfig) => !!columnConfig.editable),
|
|
236
236
|
[config.editing, config.columns]
|
|
237
237
|
);
|
|
238
|
-
const editModeBehavior = spreadsheet?.mode ??
|
|
238
|
+
const editModeBehavior = spreadsheet?.mode ?? "always";
|
|
239
239
|
const gridEnabled = !!spreadsheet && (hasEditableColumns || !!config.spreadsheet);
|
|
240
240
|
const gridStore = useGridInstance({
|
|
241
241
|
spreadsheet,
|
|
@@ -244,7 +244,8 @@ const DataTable = ({
|
|
|
244
244
|
});
|
|
245
245
|
const isEditMode = useGridEditMode(gridStore);
|
|
246
246
|
const showEditModeToggle = gridEnabled && editModeBehavior === "toggle";
|
|
247
|
-
const
|
|
247
|
+
const isEditingSurface = isEditMode && (editModeBehavior === "toggle" || !config.onRowClick);
|
|
248
|
+
const forceCardsOnMobile = isMobile && (!isFit || hasCardView) && !isEditingSurface;
|
|
248
249
|
const isGroupedView = hasGroupedView && viewMode === "grouped";
|
|
249
250
|
const isKanbanView = hasKanbanView && viewMode === "kanban";
|
|
250
251
|
const isDelegatedView = isGroupedView || isKanbanView;
|
|
@@ -273,9 +274,18 @@ const DataTable = ({
|
|
|
273
274
|
},
|
|
274
275
|
[config.editing, columnConfigById]
|
|
275
276
|
);
|
|
277
|
+
const acceptsTypedValue = useCallback(
|
|
278
|
+
({ columnId }) => {
|
|
279
|
+
const editable = columnConfigById.get(columnId)?.editable;
|
|
280
|
+
if (!editable || editable.renderEditor) return false;
|
|
281
|
+
return editable.type === "text" || editable.type === "number";
|
|
282
|
+
},
|
|
283
|
+
[columnConfigById]
|
|
284
|
+
);
|
|
276
285
|
const handleGridKeyDown = useGridKeyboard({
|
|
277
286
|
store: gridStore,
|
|
278
|
-
canEditCell,
|
|
287
|
+
canEditCell: hasEditableColumns ? canEditCell : void 0,
|
|
288
|
+
acceptsTypedValue,
|
|
279
289
|
onSave: spreadsheet?.onSave
|
|
280
290
|
});
|
|
281
291
|
const handleFillCommit = useGridFillApply({
|
|
@@ -433,6 +443,7 @@ const DataTable = ({
|
|
|
433
443
|
spacious,
|
|
434
444
|
fillEnabled: !!spreadsheet?.fill,
|
|
435
445
|
editMode: isEditMode,
|
|
446
|
+
canEditCell,
|
|
436
447
|
bodyTopRow,
|
|
437
448
|
bodyTopRowColSpan: table.getVisibleLeafColumns().length
|
|
438
449
|
}
|
|
@@ -578,7 +589,7 @@ const DataTable = ({
|
|
|
578
589
|
);
|
|
579
590
|
const editableContent = config.editing?.clickToEdit ? /* @__PURE__ */ jsx(DataTableCellEditingProvider, { children: content }) : content;
|
|
580
591
|
if (!gridStore) return editableContent;
|
|
581
|
-
return /* @__PURE__ */ jsx(GridProvider, { store: gridStore, onFillCommit: handleFillCommit, children: editableContent });
|
|
592
|
+
return /* @__PURE__ */ jsx(GridProvider, { store: gridStore, canEditCell, onFillCommit: handleFillCommit, children: editableContent });
|
|
582
593
|
};
|
|
583
594
|
export {
|
|
584
595
|
DataTable as default
|