@umodeapporg/ui 0.17.2 → 0.18.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.
@@ -7,6 +7,8 @@ interface IAppShellProps {
7
7
  toolsSlot?: ReactNode;
8
8
  controlsSlot?: ReactNode;
9
9
  userMenuSlot?: ReactNode;
10
+ /** Sidebar rows pinned to the foot, above the sign-out row (profile, settings, help). */
11
+ footerItems?: Array<ISidebarMenuItem>;
10
12
  /** Sidebar footer sign-out row; hidden when absent. */
11
13
  onLogout?: () => void;
12
14
  children: ReactNode;
@@ -19,6 +19,7 @@ const AppShell = ({
19
19
  toolsSlot,
20
20
  controlsSlot,
21
21
  userMenuSlot,
22
+ footerItems,
22
23
  onLogout,
23
24
  children
24
25
  }) => {
@@ -120,6 +121,7 @@ const AppShell = ({
120
121
  onHoverEnter,
121
122
  onHoverLeave,
122
123
  onNavigate: isMobile ? closeMobileDrawer : void 0,
124
+ footerItems,
123
125
  onLogout
124
126
  }
125
127
  ),
@@ -1 +1 @@
1
- {"version":3,"file":"AppShell.js","sources":["../../../src/components/Shell/AppShell.tsx"],"sourcesContent":["import { ReactNode, useEffect, useId, useRef } from 'react';\nimport classNames from 'classnames';\n\nimport { getTabbableElements } from '@/components/Form/ControlledDialogPrimitives';\nimport { useUiNavigation } from '@/components/Navigation/UiNavigationContext';\n\nimport AppHeader from './AppHeader';\nimport AppSidebar from './AppSidebar';\nimport { ISidebarMenuItem } from './SidebarMenu';\nimport { useSidebarState } from './useSidebarState';\n\nimport '../../styles/sidebar.css';\n\ninterface IAppShellProps {\n items: Array<ISidebarMenuItem>;\n homePath: string;\n logo?: ReactNode;\n toolsSlot?: ReactNode;\n controlsSlot?: ReactNode;\n userMenuSlot?: ReactNode;\n /** Sidebar footer sign-out row; hidden when absent. */\n onLogout?: () => void;\n children: ReactNode;\n}\n\nconst setInert = (element: HTMLElement | null, inert: boolean) => {\n if (!element) return;\n if (inert) element.setAttribute('inert', '');\n else element.removeAttribute('inert');\n};\n\n/**\n * The uMode application chrome: fixed glass header, rail/dock/hover sidebar (pin persisted\n * through uiPreferences, Ctrl+\\ shortcut) and the scroll container every page renders into.\n * Everything app-shaped (menu items, tools, account surface, logout) arrives as props/slots.\n */\nconst AppShell: React.FC<IAppShellProps> = ({\n items,\n homePath,\n logo,\n toolsSlot,\n controlsSlot,\n userMenuSlot,\n onLogout,\n children,\n}) => {\n const { location } = useUiNavigation();\n const scrollRef = useRef<HTMLDivElement>(null);\n const sidebarRef = useRef<HTMLElement>(null);\n const toggleRef = useRef<HTMLElement>(null);\n const wasMobileOpenRef = useRef(false);\n const sidebarId = useId();\n\n const {\n isMobile,\n isVisible,\n isDocked,\n isOverlayOnMobile,\n showRail,\n toggleLabelKey,\n togglePrimary,\n onHoverEnter,\n onHoverLeave,\n closeMobileDrawer,\n } = useSidebarState();\n\n const isSidebarHidden = isMobile && !isVisible;\n const wasSidebarHiddenRef = useRef(isSidebarHidden);\n\n useEffect(() => {\n scrollRef.current?.scrollTo({ top: 0, behavior: 'smooth' });\n }, [location.pathname]);\n\n /* The closed mobile drawer only slides off-screen, so it can animate out: `inert` takes it\n out of the Tab order and the accessibility tree, and the open drawer makes the page behind\n its scrim inert instead. Set through the DOM because React 18 does not type the attribute.\n Focus leaves the sidebar BEFORE it goes inert, and only when it is actually being hidden\n (drawer closing, or a desktop resize down to mobile): a drawer that becomes the visible\n desktop sidebar keeps the focus where it is. */\n useEffect(() => {\n const sidebar = sidebarRef.current;\n const wasOpen = wasMobileOpenRef.current;\n const wasHidden = wasSidebarHiddenRef.current;\n wasMobileOpenRef.current = isOverlayOnMobile;\n wasSidebarHiddenRef.current = isSidebarHidden;\n\n if (sidebar && isSidebarHidden && !wasHidden) {\n const active = document.activeElement;\n const isFocusLost = sidebar.contains(active) || (wasOpen && (!active || active === document.body));\n if (isFocusLost) toggleRef.current?.focus();\n }\n\n setInert(sidebar, isSidebarHidden);\n setInert(scrollRef.current, isOverlayOnMobile);\n\n if (sidebar && isOverlayOnMobile && !wasOpen) getTabbableElements(sidebar)[0]?.focus();\n }, [isSidebarHidden, isOverlayOnMobile]);\n\n useEffect(() => {\n if (!isOverlayOnMobile) return undefined;\n\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key !== 'Escape' || event.defaultPrevented) return;\n const active = document.activeElement;\n const ownsFocus =\n !active || active === document.body || active === toggleRef.current || sidebarRef.current?.contains(active);\n if (!ownsFocus) return;\n event.preventDefault();\n closeMobileDrawer();\n };\n\n document.addEventListener('keydown', handleKeyDown);\n return () => document.removeEventListener('keydown', handleKeyDown);\n }, [isOverlayOnMobile, closeMobileDrawer]);\n\n return (\n <div className=\"h-screen w-full bg-surface\">\n <AppHeader\n toggleSidebar={togglePrimary}\n isSidebarOpen={isDocked}\n toggleLabelKey={toggleLabelKey}\n toggleRef={toggleRef}\n mobileSidebar={isMobile ? { id: sidebarId, open: isOverlayOnMobile } : undefined}\n homePath={homePath}\n logo={logo}\n toolsSlot={toolsSlot}\n controlsSlot={controlsSlot}\n userMenuSlot={userMenuSlot}\n onHoverEnter={onHoverEnter}\n onHoverLeave={onHoverLeave}\n />\n\n <main\n className={classNames('sidebar-content flex h-screen w-full', {\n 'is-rail': !isMobile && !isDocked,\n 'is-mobile': isMobile,\n })}\n >\n {isOverlayOnMobile && (\n <div\n aria-hidden\n className=\"fixed inset-0 z-40 bg-scrim motion-reduce:transition-none\"\n onClick={closeMobileDrawer}\n />\n )}\n\n <AppSidebar\n id={sidebarId}\n sidebarRef={sidebarRef}\n items={items}\n isMobile={isMobile}\n isVisible={isVisible}\n collapsed={showRail}\n isFloating={!isMobile && !showRail && !isDocked}\n onHoverEnter={onHoverEnter}\n onHoverLeave={onHoverLeave}\n onNavigate={isMobile ? closeMobileDrawer : undefined}\n onLogout={onLogout}\n />\n\n <div\n id=\"scrollableDiv\"\n ref={scrollRef}\n className=\"z-30 h-screen w-full px-4 pt-24 tablet:px-6\"\n style={{ overflow: 'auto' }}\n >\n {children}\n </div>\n </main>\n </div>\n );\n};\n\nexport default AppShell;\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"AppShell.js","sources":["../../../src/components/Shell/AppShell.tsx"],"sourcesContent":["import { ReactNode, useEffect, useId, useRef } from 'react';\nimport classNames from 'classnames';\n\nimport { getTabbableElements } from '@/components/Form/ControlledDialogPrimitives';\nimport { useUiNavigation } from '@/components/Navigation/UiNavigationContext';\n\nimport AppHeader from './AppHeader';\nimport AppSidebar from './AppSidebar';\nimport { ISidebarMenuItem } from './SidebarMenu';\nimport { useSidebarState } from './useSidebarState';\n\nimport '../../styles/sidebar.css';\n\ninterface IAppShellProps {\n items: Array<ISidebarMenuItem>;\n homePath: string;\n logo?: ReactNode;\n toolsSlot?: ReactNode;\n controlsSlot?: ReactNode;\n userMenuSlot?: ReactNode;\n /** Sidebar rows pinned to the foot, above the sign-out row (profile, settings, help). */\n footerItems?: Array<ISidebarMenuItem>;\n /** Sidebar footer sign-out row; hidden when absent. */\n onLogout?: () => void;\n children: ReactNode;\n}\n\nconst setInert = (element: HTMLElement | null, inert: boolean) => {\n if (!element) return;\n if (inert) element.setAttribute('inert', '');\n else element.removeAttribute('inert');\n};\n\n/**\n * The uMode application chrome: fixed glass header, rail/dock/hover sidebar (pin persisted\n * through uiPreferences, Ctrl+\\ shortcut) and the scroll container every page renders into.\n * Everything app-shaped (menu items, tools, account surface, logout) arrives as props/slots.\n */\nconst AppShell: React.FC<IAppShellProps> = ({\n items,\n homePath,\n logo,\n toolsSlot,\n controlsSlot,\n userMenuSlot,\n footerItems,\n onLogout,\n children,\n}) => {\n const { location } = useUiNavigation();\n const scrollRef = useRef<HTMLDivElement>(null);\n const sidebarRef = useRef<HTMLElement>(null);\n const toggleRef = useRef<HTMLElement>(null);\n const wasMobileOpenRef = useRef(false);\n const sidebarId = useId();\n\n const {\n isMobile,\n isVisible,\n isDocked,\n isOverlayOnMobile,\n showRail,\n toggleLabelKey,\n togglePrimary,\n onHoverEnter,\n onHoverLeave,\n closeMobileDrawer,\n } = useSidebarState();\n\n const isSidebarHidden = isMobile && !isVisible;\n const wasSidebarHiddenRef = useRef(isSidebarHidden);\n\n useEffect(() => {\n scrollRef.current?.scrollTo({ top: 0, behavior: 'smooth' });\n }, [location.pathname]);\n\n /* The closed mobile drawer only slides off-screen, so it can animate out: `inert` takes it\n out of the Tab order and the accessibility tree, and the open drawer makes the page behind\n its scrim inert instead. Set through the DOM because React 18 does not type the attribute.\n Focus leaves the sidebar BEFORE it goes inert, and only when it is actually being hidden\n (drawer closing, or a desktop resize down to mobile): a drawer that becomes the visible\n desktop sidebar keeps the focus where it is. */\n useEffect(() => {\n const sidebar = sidebarRef.current;\n const wasOpen = wasMobileOpenRef.current;\n const wasHidden = wasSidebarHiddenRef.current;\n wasMobileOpenRef.current = isOverlayOnMobile;\n wasSidebarHiddenRef.current = isSidebarHidden;\n\n if (sidebar && isSidebarHidden && !wasHidden) {\n const active = document.activeElement;\n const isFocusLost = sidebar.contains(active) || (wasOpen && (!active || active === document.body));\n if (isFocusLost) toggleRef.current?.focus();\n }\n\n setInert(sidebar, isSidebarHidden);\n setInert(scrollRef.current, isOverlayOnMobile);\n\n if (sidebar && isOverlayOnMobile && !wasOpen) getTabbableElements(sidebar)[0]?.focus();\n }, [isSidebarHidden, isOverlayOnMobile]);\n\n useEffect(() => {\n if (!isOverlayOnMobile) return undefined;\n\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key !== 'Escape' || event.defaultPrevented) return;\n const active = document.activeElement;\n const ownsFocus =\n !active || active === document.body || active === toggleRef.current || sidebarRef.current?.contains(active);\n if (!ownsFocus) return;\n event.preventDefault();\n closeMobileDrawer();\n };\n\n document.addEventListener('keydown', handleKeyDown);\n return () => document.removeEventListener('keydown', handleKeyDown);\n }, [isOverlayOnMobile, closeMobileDrawer]);\n\n return (\n <div className=\"h-screen w-full bg-surface\">\n <AppHeader\n toggleSidebar={togglePrimary}\n isSidebarOpen={isDocked}\n toggleLabelKey={toggleLabelKey}\n toggleRef={toggleRef}\n mobileSidebar={isMobile ? { id: sidebarId, open: isOverlayOnMobile } : undefined}\n homePath={homePath}\n logo={logo}\n toolsSlot={toolsSlot}\n controlsSlot={controlsSlot}\n userMenuSlot={userMenuSlot}\n onHoverEnter={onHoverEnter}\n onHoverLeave={onHoverLeave}\n />\n\n <main\n className={classNames('sidebar-content flex h-screen w-full', {\n 'is-rail': !isMobile && !isDocked,\n 'is-mobile': isMobile,\n })}\n >\n {isOverlayOnMobile && (\n <div\n aria-hidden\n className=\"fixed inset-0 z-40 bg-scrim motion-reduce:transition-none\"\n onClick={closeMobileDrawer}\n />\n )}\n\n <AppSidebar\n id={sidebarId}\n sidebarRef={sidebarRef}\n items={items}\n isMobile={isMobile}\n isVisible={isVisible}\n collapsed={showRail}\n isFloating={!isMobile && !showRail && !isDocked}\n onHoverEnter={onHoverEnter}\n onHoverLeave={onHoverLeave}\n onNavigate={isMobile ? closeMobileDrawer : undefined}\n footerItems={footerItems}\n onLogout={onLogout}\n />\n\n <div\n id=\"scrollableDiv\"\n ref={scrollRef}\n className=\"z-30 h-screen w-full px-4 pt-24 tablet:px-6\"\n style={{ overflow: 'auto' }}\n >\n {children}\n </div>\n </main>\n </div>\n );\n};\n\nexport default AppShell;\n"],"names":[],"mappings":"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"}
@@ -11,6 +11,8 @@ interface IAppSidebarProps {
11
11
  onHoverEnter: () => void;
12
12
  onHoverLeave: () => void;
13
13
  onNavigate?: () => void;
14
+ /** Rows pinned to the foot of the sidebar, above the sign-out row. */
15
+ footerItems?: Array<ISidebarMenuItem>;
14
16
  /** Hides the footer sign-out row when absent. */
15
17
  onLogout?: () => void;
16
18
  }
@@ -6,6 +6,8 @@ import ControlledButton from "../Form/ControlledButton.js";
6
6
  import { MaterialSymbol } from "../UI/MaterialSymbol/index.js";
7
7
  import { SidebarFavoritesProvider, useSidebarFavorites, buildFavoriteMenuItems } from "./sidebarFavorites.js";
8
8
  import SidebarMenu from "./SidebarMenu.js";
9
+ const ACTIVE_CLASS = "text-brand-emphasis bg-brand-soft";
10
+ const ACTIVE_ICON_CLASS = "text-brand-emphasis";
9
11
  const SidebarMenuWithFavorites = ({ items, collapsed, onNavigate }) => {
10
12
  const { favoriteKeys } = useSidebarFavorites();
11
13
  const menuItems = useMemo(
@@ -17,8 +19,8 @@ const SidebarMenuWithFavorites = ({ items, collapsed, onNavigate }) => {
17
19
  {
18
20
  items: menuItems,
19
21
  collapsed,
20
- activeClass: "text-brand-emphasis bg-brand-soft",
21
- activeIconClass: "text-brand-emphasis",
22
+ activeClass: ACTIVE_CLASS,
23
+ activeIconClass: ACTIVE_ICON_CLASS,
22
24
  onNavigate
23
25
  }
24
26
  );
@@ -34,9 +36,11 @@ const AppSidebar = ({
34
36
  onHoverEnter,
35
37
  onHoverLeave,
36
38
  onNavigate,
39
+ footerItems,
37
40
  onLogout
38
41
  }) => {
39
42
  const { t } = useTranslation("ui");
43
+ const hasFooterItems = Boolean(footerItems?.length);
40
44
  return /* @__PURE__ */ jsx(
41
45
  "aside",
42
46
  {
@@ -55,18 +59,30 @@ const AppSidebar = ({
55
59
  ),
56
60
  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: [
57
61
  /* @__PURE__ */ jsx(SidebarFavoritesProvider, { children: /* @__PURE__ */ jsx(SidebarMenuWithFavorites, { items, collapsed, onNavigate }) }),
58
- onLogout && /* @__PURE__ */ jsx("div", { className: "text-foreground-muted", children: /* @__PURE__ */ jsxs(
59
- ControlledButton,
60
- {
61
- defaultHtmlButton: true,
62
- onClick: onLogout,
63
- 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",
64
- children: [
65
- /* @__PURE__ */ jsx(MaterialSymbol, { icon: "logout", size: 20, weight: 400, className: "shrink-0" }),
66
- /* @__PURE__ */ jsx("span", { className: classNames("sidebar-label text-sm", { "is-collapsed": collapsed }), children: t("sidebar.logout") })
67
- ]
68
- }
69
- ) })
62
+ (hasFooterItems || onLogout) && /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-2 text-foreground-muted", children: [
63
+ hasFooterItems && /* @__PURE__ */ jsx(
64
+ SidebarMenu,
65
+ {
66
+ items: footerItems ?? [],
67
+ collapsed,
68
+ activeClass: ACTIVE_CLASS,
69
+ activeIconClass: ACTIVE_ICON_CLASS,
70
+ onNavigate
71
+ }
72
+ ),
73
+ onLogout && /* @__PURE__ */ jsxs(
74
+ ControlledButton,
75
+ {
76
+ defaultHtmlButton: true,
77
+ onClick: onLogout,
78
+ 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",
79
+ children: [
80
+ /* @__PURE__ */ jsx(MaterialSymbol, { icon: "logout", size: 20, weight: 400, className: "shrink-0" }),
81
+ /* @__PURE__ */ jsx("span", { className: classNames("sidebar-label text-sm", { "is-collapsed": collapsed }), children: t("sidebar.logout") })
82
+ ]
83
+ }
84
+ )
85
+ ] })
70
86
  ] })
71
87
  }
72
88
  );
@@ -1 +1 @@
1
- {"version":3,"file":"AppSidebar.js","sources":["../../../src/components/Shell/AppSidebar.tsx"],"sourcesContent":["import { Ref, 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 id?: string;\n sidebarRef?: Ref<HTMLElement>;\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 id,\n sidebarRef,\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 id={id}\n ref={sidebarRef}\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":";;;;;;;;AA+BA,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;AAAA,EACA;AAAA,EACA;AACF,MAAM;AACJ,QAAM,EAAE,EAAA,IAAM,eAAe,IAAI;AAEjC,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC;AAAA,MACA,KAAK;AAAA,MACL,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
+ {"version":3,"file":"AppSidebar.js","sources":["../../../src/components/Shell/AppSidebar.tsx"],"sourcesContent":["import { Ref, 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 id?: string;\n sidebarRef?: Ref<HTMLElement>;\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 /** Rows pinned to the foot of the sidebar, above the sign-out row. */\n footerItems?: Array<ISidebarMenuItem>;\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 ACTIVE_CLASS = 'text-brand-emphasis bg-brand-soft';\nconst ACTIVE_ICON_CLASS = 'text-brand-emphasis';\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={ACTIVE_CLASS}\n activeIconClass={ACTIVE_ICON_CLASS}\n onNavigate={onNavigate}\n />\n );\n};\n\nconst AppSidebar: React.FC<IAppSidebarProps> = ({\n id,\n sidebarRef,\n items,\n isMobile,\n isVisible,\n collapsed,\n isFloating,\n onHoverEnter,\n onHoverLeave,\n onNavigate,\n footerItems,\n onLogout,\n}) => {\n const { t } = useTranslation('ui');\n const hasFooterItems = Boolean(footerItems?.length);\n\n return (\n <aside\n id={id}\n ref={sidebarRef}\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 {(hasFooterItems || onLogout) && (\n <div className=\"flex flex-col gap-y-2 text-foreground-muted\">\n {/* Outside the favorites provider on purpose: a row that is always in sight takes no star. */}\n {hasFooterItems && (\n <SidebarMenu\n items={footerItems ?? []}\n collapsed={collapsed}\n activeClass={ACTIVE_CLASS}\n activeIconClass={ACTIVE_ICON_CLASS}\n onNavigate={onNavigate}\n />\n )}\n\n {onLogout && (\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 )}\n </div>\n )}\n </nav>\n </aside>\n );\n};\n\nexport default AppSidebar;\n"],"names":[],"mappings":";;;;;;;;AAiCA,MAAM,eAAe;AACrB,MAAM,oBAAoB;AAE1B,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,aAAa;AAAA,MACb,iBAAiB;AAAA,MACjB;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;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MAAM;AACJ,QAAM,EAAE,EAAA,IAAM,eAAe,IAAI;AACjC,QAAM,iBAAiB,QAAQ,aAAa,MAAM;AAElD,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC;AAAA,MACA,KAAK;AAAA,MACL,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,SAEE,kBAAkB,aAClB,qBAAC,OAAA,EAAI,WAAU,+CAEZ,UAAA;AAAA,UAAA,kBACC;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,OAAO,eAAe,CAAA;AAAA,cACtB;AAAA,cACA,aAAa;AAAA,cACb,iBAAiB;AAAA,cACjB;AAAA,YAAA;AAAA,UAAA;AAAA,UAIH,YACC;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,mBAAiB;AAAA,cACjB,SAAS;AAAA,cACT,WAAU;AAAA,cAEV,UAAA;AAAA,gBAAA,oBAAC,gBAAA,EAAe,MAAK,UAAS,MAAM,IAAI,QAAQ,KAAK,WAAU,WAAA,CAAW;AAAA,gBAC1E,oBAAC,QAAA,EAAK,WAAW,WAAW,yBAAyB,EAAE,gBAAgB,UAAA,CAAW,GAC/E,UAAA,EAAE,gBAAgB,EAAA,CACrB;AAAA,cAAA;AAAA,YAAA;AAAA,UAAA;AAAA,QACF,EAAA,CAEJ;AAAA,MAAA,EAAA,CAEJ;AAAA,IAAA;AAAA,EAAA;AAGN;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umodeapporg/ui",
3
- "version": "0.17.2",
3
+ "version": "0.18.0",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "sideEffects": [