@umodeapporg/ui 0.2.0 → 0.4.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.
Files changed (65) hide show
  1. package/README.md +78 -0
  2. package/dist/assets/umode-ui.css +148 -0
  3. package/dist/components/Form/ControlledSelect.js +1 -1
  4. package/dist/components/Form/ControlledTabs.js +4 -4
  5. package/dist/components/Form/ControlledTabs.js.map +1 -1
  6. package/dist/components/Form/ControlledViewToggle.js +2 -2
  7. package/dist/components/Form/ControlledViewToggle.js.map +1 -1
  8. package/dist/components/Navigation/ControlledBreadcrumb.js +3 -2
  9. package/dist/components/Navigation/ControlledBreadcrumb.js.map +1 -1
  10. package/dist/components/Navigation/PageTitle.js +3 -6
  11. package/dist/components/Navigation/PageTitle.js.map +1 -1
  12. package/dist/components/Navigation/ReactRouterNavigationAdapter.d.ts +7 -0
  13. package/dist/components/Navigation/ReactRouterNavigationAdapter.js +26 -0
  14. package/dist/components/Navigation/ReactRouterNavigationAdapter.js.map +1 -0
  15. package/dist/components/Navigation/UiNavigationContext.d.ts +38 -0
  16. package/dist/components/Navigation/UiNavigationContext.js +43 -0
  17. package/dist/components/Navigation/UiNavigationContext.js.map +1 -0
  18. package/dist/components/Search/filters/useFilters.js +2 -2
  19. package/dist/components/Search/filters/useFilters.js.map +1 -1
  20. package/dist/components/Shell/AppHeader.d.ts +19 -0
  21. package/dist/components/Shell/AppHeader.js +64 -0
  22. package/dist/components/Shell/AppHeader.js.map +1 -0
  23. package/dist/components/Shell/AppShell.d.ts +20 -0
  24. package/dist/components/Shell/AppShell.js +100 -0
  25. package/dist/components/Shell/AppShell.js.map +1 -0
  26. package/dist/components/Shell/AppSidebar.d.ts +15 -0
  27. package/dist/components/Shell/AppSidebar.js +63 -0
  28. package/dist/components/Shell/AppSidebar.js.map +1 -0
  29. package/dist/components/Shell/LanguageToggle.d.ts +8 -0
  30. package/dist/components/Shell/LanguageToggle.js +59 -0
  31. package/dist/components/Shell/LanguageToggle.js.map +1 -0
  32. package/dist/components/Shell/SidebarMenu.d.ts +24 -0
  33. package/dist/components/Shell/SidebarMenu.js +204 -0
  34. package/dist/components/Shell/SidebarMenu.js.map +1 -0
  35. package/dist/components/Shell/ThemeToggle.d.ts +11 -0
  36. package/dist/components/Shell/ThemeToggle.js +55 -0
  37. package/dist/components/Shell/ThemeToggle.js.map +1 -0
  38. package/dist/components/Shell/headerControlStyles.d.ts +10 -0
  39. package/dist/components/Shell/headerControlStyles.js +11 -0
  40. package/dist/components/Shell/headerControlStyles.js.map +1 -0
  41. package/dist/components/Shell/useSidebarState.d.ts +14 -0
  42. package/dist/components/Shell/useSidebarState.js +106 -0
  43. package/dist/components/Shell/useSidebarState.js.map +1 -0
  44. package/dist/components/UI/BaseDataCard.js +3 -2
  45. package/dist/components/UI/BaseDataCard.js.map +1 -1
  46. package/dist/components/UI/DataTable/DataTable.js +1 -1
  47. package/dist/components/UI/DataTable/DataTableBulkEditDialog.js +1 -1
  48. package/dist/components/UI/DataTable/DataTableFilters.js +1 -1
  49. package/dist/components/UI/DataTable/DataTableToolbar.js +1 -1
  50. package/dist/components/UI/DataTable/applyDataTableFilters.js +1 -1
  51. package/dist/components/UI/UserMenu.d.ts +48 -0
  52. package/dist/components/UI/UserMenu.js +196 -0
  53. package/dist/components/UI/UserMenu.js.map +1 -0
  54. package/dist/index.d.ts +6 -0
  55. package/dist/index.js +47 -34
  56. package/dist/index.js.map +1 -1
  57. package/dist/locales/en/ui.json.d.ts +28 -0
  58. package/dist/locales/en/ui.json.js +12 -0
  59. package/dist/locales/en/ui.json.js.map +1 -1
  60. package/dist/locales/index.d.ts +56 -0
  61. package/dist/locales/pt/ui.json.d.ts +28 -0
  62. package/dist/locales/pt/ui.json.js +12 -0
  63. package/dist/locales/pt/ui.json.js.map +1 -1
  64. package/package.json +6 -3
  65. package/src/assets/styles/base.css +0 -1
@@ -2,13 +2,13 @@ import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { useMemo } from "react";
3
3
  import classNames from "classnames";
4
4
  import { useTranslation } from "react-i18next";
5
- import { useNavigate, useLocation } from "react-router-dom";
6
5
  import ControlledButton from "../Form/ControlledButton.js";
7
6
  import ControlledTooltip from "../UI/ControlledTooltip.js";
8
7
  import { capitalizeAllLetters } from "../../helpers/stringHelpers.js";
9
8
  import { MaterialSymbol } from "../UI/MaterialSymbol/index.js";
10
9
  import ControlledBreadcrumb from "./ControlledBreadcrumb.js";
11
10
  import { usePageTitleNavigation } from "./PageTitleNavigationContext.js";
11
+ import { useUiNavigation } from "./UiNavigationContext.js";
12
12
  const PageTitle = ({
13
13
  canBack = false,
14
14
  canFloat = false,
@@ -35,8 +35,7 @@ const PageTitle = ({
35
35
  disableBack = false
36
36
  }) => {
37
37
  const { t } = useTranslation("ui");
38
- const navigate = useNavigate();
39
- const location = useLocation();
38
+ const { navigate, goBack, location } = useUiNavigation();
40
39
  const isFirefox = () => typeof navigator !== "undefined" && navigator.userAgent.toLowerCase().includes("firefox");
41
40
  const handleBack = () => {
42
41
  if (disableBack) return;
@@ -45,9 +44,7 @@ const PageTitle = ({
45
44
  } else if (canBackTo) {
46
45
  navigate(canBackTo);
47
46
  } else {
48
- navigate(-1, {
49
- state: { ignoreActiveTab: true }
50
- });
47
+ goBack();
51
48
  }
52
49
  };
53
50
  const { resolveTrail } = usePageTitleNavigation();
@@ -1 +1 @@
1
- {"version":3,"file":"PageTitle.js","sources":["../../../src/components/Navigation/PageTitle.tsx"],"sourcesContent":["import { ReactNode, useMemo } from 'react';\nimport classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\nimport { useLocation, useNavigate } from 'react-router-dom';\n\nimport ControlledButton from '@/components/Form/ControlledButton';\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { capitalizeAllLetters } from '@/helpers/stringHelpers';\n\nimport { MaterialSymbol, SymbolCodepoints } from '../UI/MaterialSymbol';\n\nimport ControlledBreadcrumb from './ControlledBreadcrumb';\nimport { usePageTitleNavigation } from './PageTitleNavigationContext';\n\ninterface IPageTitleProps {\n title?: string;\n /** Substitui o span do título por conteúdo rico (ex.: nome editável inline da ficha técnica). */\n titleContent?: ReactNode;\n /** Rótulo pequeno em caixa alta acima do título (ex.: \"FICHA TÉCNICA\"). */\n eyebrow?: string;\n /** Conteúdo abaixo da linha do título, dentro da coluna de texto (tags, steppers). */\n subtitleSlot?: ReactNode;\n slot?: ReactNode;\n /** Conteúdo renderizado à ESQUERDA, imediatamente ao lado do título (o `slot` fica à direita). */\n titleSlot?: ReactNode;\n /** Conteúdo renderizado ANTES do ícone/título (ex.: botão voltar customizado, alvo de drop). */\n leadingSlot?: ReactNode;\n /** Trilha custom acima do título (use `ControlledBreadcrumb`). Sem ela, a trilha é derivada do menu. */\n breadcrumb?: ReactNode;\n /** Deriva a trilha automaticamente da árvore de navegação quando `breadcrumb` não é passado. */\n autoBreadcrumb?: boolean;\n /** Ícone em chip à esquerda do título. Sem ele, herda o ícone do item de menu da rota atual. */\n titleIcon?: SymbolCodepoints;\n titleIconClassName?: string;\n /** Deriva o ícone automaticamente do menu quando `titleIcon` não é passado. */\n autoIcon?: boolean;\n /** Variante compacta: título menor, para headers de navegação interna (ex.: pastas). */\n compact?: boolean;\n /** Desligue para títulos digitados pelo usuário (nomes de pastas etc.), que não podem ter a caixa alterada. */\n capitalizeTitle?: boolean;\n /** Desligue quando o header é seguido imediatamente por tabs ou outra divisão própria. */\n showDivider?: boolean;\n canBack?: boolean;\n canBackTo?: string;\n canFloat?: boolean;\n headline?: string;\n className?: string;\n titleClassName?: string;\n onBackClick?: () => void;\n disableBack?: boolean;\n}\n\nconst PageTitle: React.FC<IPageTitleProps> = ({\n canBack = false,\n canFloat = false,\n canBackTo,\n title = '',\n titleContent,\n eyebrow,\n subtitleSlot,\n headline,\n slot,\n titleSlot,\n leadingSlot,\n breadcrumb,\n autoBreadcrumb = true,\n titleIcon,\n titleIconClassName,\n autoIcon = true,\n compact = false,\n capitalizeTitle = true,\n showDivider = true,\n className,\n titleClassName,\n onBackClick,\n disableBack = false,\n}) => {\n const { t } = useTranslation('ui');\n const navigate = useNavigate();\n const location = useLocation();\n\n const isFirefox = () => typeof navigator !== 'undefined' && navigator.userAgent.toLowerCase().includes('firefox');\n\n const handleBack = () => {\n if (disableBack) return;\n\n if (onBackClick) {\n onBackClick();\n } else if (canBackTo) {\n navigate(canBackTo);\n } else {\n navigate(-1 as unknown as string, {\n state: { ignoreActiveTab: true },\n });\n }\n };\n\n const { resolveTrail } = usePageTitleNavigation();\n\n const menuTrail = useMemo(() => {\n const needsTrail = (autoBreadcrumb && !breadcrumb && !canFloat) || (autoIcon && !titleIcon);\n return needsTrail ? resolveTrail(location.pathname) : [];\n }, [autoBreadcrumb, breadcrumb, canFloat, autoIcon, titleIcon, location.pathname, resolveTrail]);\n\n const resolvedBreadcrumb =\n breadcrumb ??\n (autoBreadcrumb && !canFloat && menuTrail.length >= 2 ? (\n <ControlledBreadcrumb\n ariaLabel={t('pageTitle.breadcrumbLabel')}\n items={menuTrail.map((item, index) => ({\n key: item.key,\n label: item.label,\n to: index < menuTrail.length - 1 ? item.to : undefined,\n }))}\n />\n ) : undefined);\n\n const resolvedTitleIcon = titleIcon ?? (autoIcon ? menuTrail[menuTrail.length - 1]?.icon : undefined);\n\n return (\n <div\n className={classNames('flex flex-col', {\n 'zIndexMax fixed top-16 mr-7': canFloat,\n className,\n })}\n style={{\n width: canFloat ? (isFirefox() ? '-moz-available' : '-webkit-fill-available') : '100%',\n }}\n >\n {resolvedBreadcrumb && <div className=\"mb-2 shrink-0\">{resolvedBreadcrumb}</div>}\n\n <div\n className={classNames('flex w-full flex-wrap items-center justify-between gap-x-4 gap-y-2', {\n 'bg-surface py-5': canFloat,\n })}\n >\n <div className={classNames('flex min-w-0 flex-1 basis-60 items-center', compact ? 'gap-2' : 'gap-3')}>\n {leadingSlot}\n\n {canBack && (\n <ControlledTooltip content={t('actions.back')}>\n <ControlledButton\n defaultHtmlButton\n onClick={handleBack}\n disabled={disableBack}\n className={classNames(\n 'flex shrink-0 items-center justify-center rounded-full text-foreground-subtle transition-interactive duration-base ease-enter hover:bg-muted-hover hover:text-foreground-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-border active:scale-90 disabled:pointer-events-none disabled:text-foreground-faint',\n compact ? 'size-8' : 'size-9',\n )}\n >\n <MaterialSymbol icon=\"arrow_back\" size={compact ? 18 : 20} />\n </ControlledButton>\n </ControlledTooltip>\n )}\n\n {resolvedTitleIcon && (\n <span\n className={classNames(\n 'flex shrink-0 items-center justify-center',\n compact ? 'size-8 rounded-lg' : 'size-10 rounded-xl',\n titleIconClassName ?? 'bg-brand-soft text-brand-emphasis',\n )}\n >\n <MaterialSymbol icon={resolvedTitleIcon} size={compact ? 18 : 22} fill className=\"text-inherit\" />\n </span>\n )}\n\n <div className=\"flex min-w-0 flex-1 flex-col\">\n {eyebrow && (\n <span className=\"mb-0.5 text-xs font-semibold uppercase tracking-widest text-foreground-faint\">\n {eyebrow}\n </span>\n )}\n\n <div className=\"flex min-w-0 items-center gap-2\">\n {titleContent ?? (\n <span\n className={classNames(\n 'min-w-0 truncate',\n compact\n ? 'text-sm font-semibold text-foreground'\n : 'text-lg font-bold tracking-tight text-foreground laptop:text-xl',\n titleClassName,\n )}\n >\n {capitalizeTitle ? capitalizeAllLetters(title) : title}\n </span>\n )}\n\n {titleSlot}\n </div>\n\n {headline && <span className=\"mt-0.5 text-xs text-foreground-subtle\">{headline}</span>}\n\n {subtitleSlot && <div className=\"mt-1 flex min-w-0 flex-col gap-1\">{subtitleSlot}</div>}\n </div>\n </div>\n\n {slot && <div className=\"ml-auto flex shrink-0 flex-wrap items-center justify-end gap-2\">{slot}</div>}\n </div>\n {showDivider && !canFloat && (title.length > 0 || Boolean(titleContent)) && (\n <hr className={classNames('border-border-subtle', compact ? 'mb-2 mt-2' : 'mb-2 mt-3')} />\n )}\n </div>\n );\n};\n\nexport default PageTitle;\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"PageTitle.js","sources":["../../../src/components/Navigation/PageTitle.tsx"],"sourcesContent":["import { ReactNode, useMemo } from 'react';\nimport classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\n\nimport ControlledButton from '@/components/Form/ControlledButton';\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { capitalizeAllLetters } from '@/helpers/stringHelpers';\n\nimport { MaterialSymbol, SymbolCodepoints } from '../UI/MaterialSymbol';\n\nimport ControlledBreadcrumb from './ControlledBreadcrumb';\nimport { usePageTitleNavigation } from './PageTitleNavigationContext';\nimport { useUiNavigation } from './UiNavigationContext';\n\ninterface IPageTitleProps {\n title?: string;\n /** Substitui o span do título por conteúdo rico (ex.: nome editável inline da ficha técnica). */\n titleContent?: ReactNode;\n /** Rótulo pequeno em caixa alta acima do título (ex.: \"FICHA TÉCNICA\"). */\n eyebrow?: string;\n /** Conteúdo abaixo da linha do título, dentro da coluna de texto (tags, steppers). */\n subtitleSlot?: ReactNode;\n slot?: ReactNode;\n /** Conteúdo renderizado à ESQUERDA, imediatamente ao lado do título (o `slot` fica à direita). */\n titleSlot?: ReactNode;\n /** Conteúdo renderizado ANTES do ícone/título (ex.: botão voltar customizado, alvo de drop). */\n leadingSlot?: ReactNode;\n /** Trilha custom acima do título (use `ControlledBreadcrumb`). Sem ela, a trilha é derivada do menu. */\n breadcrumb?: ReactNode;\n /** Deriva a trilha automaticamente da árvore de navegação quando `breadcrumb` não é passado. */\n autoBreadcrumb?: boolean;\n /** Ícone em chip à esquerda do título. Sem ele, herda o ícone do item de menu da rota atual. */\n titleIcon?: SymbolCodepoints;\n titleIconClassName?: string;\n /** Deriva o ícone automaticamente do menu quando `titleIcon` não é passado. */\n autoIcon?: boolean;\n /** Variante compacta: título menor, para headers de navegação interna (ex.: pastas). */\n compact?: boolean;\n /** Desligue para títulos digitados pelo usuário (nomes de pastas etc.), que não podem ter a caixa alterada. */\n capitalizeTitle?: boolean;\n /** Desligue quando o header é seguido imediatamente por tabs ou outra divisão própria. */\n showDivider?: boolean;\n canBack?: boolean;\n canBackTo?: string;\n canFloat?: boolean;\n headline?: string;\n className?: string;\n titleClassName?: string;\n onBackClick?: () => void;\n disableBack?: boolean;\n}\n\nconst PageTitle: React.FC<IPageTitleProps> = ({\n canBack = false,\n canFloat = false,\n canBackTo,\n title = '',\n titleContent,\n eyebrow,\n subtitleSlot,\n headline,\n slot,\n titleSlot,\n leadingSlot,\n breadcrumb,\n autoBreadcrumb = true,\n titleIcon,\n titleIconClassName,\n autoIcon = true,\n compact = false,\n capitalizeTitle = true,\n showDivider = true,\n className,\n titleClassName,\n onBackClick,\n disableBack = false,\n}) => {\n const { t } = useTranslation('ui');\n const { navigate, goBack, location } = useUiNavigation();\n\n const isFirefox = () => typeof navigator !== 'undefined' && navigator.userAgent.toLowerCase().includes('firefox');\n\n const handleBack = () => {\n if (disableBack) return;\n\n if (onBackClick) {\n onBackClick();\n } else if (canBackTo) {\n navigate(canBackTo);\n } else {\n goBack();\n }\n };\n\n const { resolveTrail } = usePageTitleNavigation();\n\n const menuTrail = useMemo(() => {\n const needsTrail = (autoBreadcrumb && !breadcrumb && !canFloat) || (autoIcon && !titleIcon);\n return needsTrail ? resolveTrail(location.pathname) : [];\n }, [autoBreadcrumb, breadcrumb, canFloat, autoIcon, titleIcon, location.pathname, resolveTrail]);\n\n const resolvedBreadcrumb =\n breadcrumb ??\n (autoBreadcrumb && !canFloat && menuTrail.length >= 2 ? (\n <ControlledBreadcrumb\n ariaLabel={t('pageTitle.breadcrumbLabel')}\n items={menuTrail.map((item, index) => ({\n key: item.key,\n label: item.label,\n to: index < menuTrail.length - 1 ? item.to : undefined,\n }))}\n />\n ) : undefined);\n\n const resolvedTitleIcon = titleIcon ?? (autoIcon ? menuTrail[menuTrail.length - 1]?.icon : undefined);\n\n return (\n <div\n className={classNames('flex flex-col', {\n 'zIndexMax fixed top-16 mr-7': canFloat,\n className,\n })}\n style={{\n width: canFloat ? (isFirefox() ? '-moz-available' : '-webkit-fill-available') : '100%',\n }}\n >\n {resolvedBreadcrumb && <div className=\"mb-2 shrink-0\">{resolvedBreadcrumb}</div>}\n\n <div\n className={classNames('flex w-full flex-wrap items-center justify-between gap-x-4 gap-y-2', {\n 'bg-surface py-5': canFloat,\n })}\n >\n <div className={classNames('flex min-w-0 flex-1 basis-60 items-center', compact ? 'gap-2' : 'gap-3')}>\n {leadingSlot}\n\n {canBack && (\n <ControlledTooltip content={t('actions.back')}>\n <ControlledButton\n defaultHtmlButton\n onClick={handleBack}\n disabled={disableBack}\n className={classNames(\n 'flex shrink-0 items-center justify-center rounded-full text-foreground-subtle transition-interactive duration-base ease-enter hover:bg-muted-hover hover:text-foreground-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-border active:scale-90 disabled:pointer-events-none disabled:text-foreground-faint',\n compact ? 'size-8' : 'size-9',\n )}\n >\n <MaterialSymbol icon=\"arrow_back\" size={compact ? 18 : 20} />\n </ControlledButton>\n </ControlledTooltip>\n )}\n\n {resolvedTitleIcon && (\n <span\n className={classNames(\n 'flex shrink-0 items-center justify-center',\n compact ? 'size-8 rounded-lg' : 'size-10 rounded-xl',\n titleIconClassName ?? 'bg-brand-soft text-brand-emphasis',\n )}\n >\n <MaterialSymbol icon={resolvedTitleIcon} size={compact ? 18 : 22} fill className=\"text-inherit\" />\n </span>\n )}\n\n <div className=\"flex min-w-0 flex-1 flex-col\">\n {eyebrow && (\n <span className=\"mb-0.5 text-xs font-semibold uppercase tracking-widest text-foreground-faint\">\n {eyebrow}\n </span>\n )}\n\n <div className=\"flex min-w-0 items-center gap-2\">\n {titleContent ?? (\n <span\n className={classNames(\n 'min-w-0 truncate',\n compact\n ? 'text-sm font-semibold text-foreground'\n : 'text-lg font-bold tracking-tight text-foreground laptop:text-xl',\n titleClassName,\n )}\n >\n {capitalizeTitle ? capitalizeAllLetters(title) : title}\n </span>\n )}\n\n {titleSlot}\n </div>\n\n {headline && <span className=\"mt-0.5 text-xs text-foreground-subtle\">{headline}</span>}\n\n {subtitleSlot && <div className=\"mt-1 flex min-w-0 flex-col gap-1\">{subtitleSlot}</div>}\n </div>\n </div>\n\n {slot && <div className=\"ml-auto flex shrink-0 flex-wrap items-center justify-end gap-2\">{slot}</div>}\n </div>\n {showDivider && !canFloat && (title.length > 0 || Boolean(titleContent)) && (\n <hr className={classNames('border-border-subtle', compact ? 'mb-2 mt-2' : 'mb-2 mt-3')} />\n )}\n </div>\n );\n};\n\nexport default PageTitle;\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Binds the navigation port to react-router. The ONLY module in the package that imports
3
+ * `react-router-dom` (an optional peer): hosts without it simply never import this file.
4
+ */
5
+ export declare const UiReactRouterNavigationProvider: React.FC<{
6
+ children: React.ReactNode;
7
+ }>;
@@ -0,0 +1,26 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { useMemo } from "react";
3
+ import { useLocation, useNavigate, useSearchParams, Link } from "react-router-dom";
4
+ import { UiNavigationProvider } from "./UiNavigationContext.js";
5
+ const RouterLink = ({ to, children, ...rest }) => /* @__PURE__ */ jsx(Link, { to, ...rest, children });
6
+ const UiReactRouterNavigationProvider = ({ children }) => {
7
+ const location = useLocation();
8
+ const navigate = useNavigate();
9
+ const [searchParams, setSearchParams] = useSearchParams();
10
+ const value = useMemo(
11
+ () => ({
12
+ location: { pathname: location.pathname, search: location.search, state: location.state },
13
+ searchParams,
14
+ navigate: (to, options) => navigate(to, options),
15
+ goBack: () => navigate(-1),
16
+ setSearchParams: (updater, options) => setSearchParams(updater, options),
17
+ Link: RouterLink
18
+ }),
19
+ [location.pathname, location.search, location.state, searchParams, navigate, setSearchParams]
20
+ );
21
+ return /* @__PURE__ */ jsx(UiNavigationProvider, { value, children });
22
+ };
23
+ export {
24
+ UiReactRouterNavigationProvider
25
+ };
26
+ //# sourceMappingURL=ReactRouterNavigationAdapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ReactRouterNavigationAdapter.js","sources":["../../../src/components/Navigation/ReactRouterNavigationAdapter.tsx"],"sourcesContent":["import { useMemo } from 'react';\nimport { Link as ReactRouterDomLink, useLocation, useNavigate, useSearchParams } from 'react-router-dom';\n\nimport { IUiLinkProps, IUiNavigationApi, UiNavigationProvider } from './UiNavigationContext';\n\nconst RouterLink: React.FC<IUiLinkProps> = ({ to, children, ...rest }) => (\n <ReactRouterDomLink to={to} {...rest}>\n {children}\n </ReactRouterDomLink>\n);\n\n/**\n * Binds the navigation port to react-router. The ONLY module in the package that imports\n * `react-router-dom` (an optional peer): hosts without it simply never import this file.\n */\nexport const UiReactRouterNavigationProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {\n const location = useLocation();\n const navigate = useNavigate();\n const [searchParams, setSearchParams] = useSearchParams();\n\n const value = useMemo<IUiNavigationApi>(\n () => ({\n location: { pathname: location.pathname, search: location.search, state: location.state },\n searchParams,\n navigate: (to, options) => navigate(to, options),\n goBack: () => navigate(-1),\n setSearchParams: (updater, options) => setSearchParams(updater, options),\n Link: RouterLink,\n }),\n [location.pathname, location.search, location.state, searchParams, navigate, setSearchParams],\n );\n\n return <UiNavigationProvider value={value}>{children}</UiNavigationProvider>;\n};\n"],"names":["ReactRouterDomLink"],"mappings":";;;;AAKA,MAAM,aAAqC,CAAC,EAAE,IAAI,UAAU,GAAG,KAAA,MAC7D,oBAACA,MAAA,EAAmB,IAAS,GAAG,MAC7B,SAAA,CACH;AAOK,MAAM,kCAA2E,CAAC,EAAE,eAAe;AACxG,QAAM,WAAW,YAAA;AACjB,QAAM,WAAW,YAAA;AACjB,QAAM,CAAC,cAAc,eAAe,IAAI,gBAAA;AAExC,QAAM,QAAQ;AAAA,IACZ,OAAO;AAAA,MACL,UAAU,EAAE,UAAU,SAAS,UAAU,QAAQ,SAAS,QAAQ,OAAO,SAAS,MAAA;AAAA,MAClF;AAAA,MACA,UAAU,CAAC,IAAI,YAAY,SAAS,IAAI,OAAO;AAAA,MAC/C,QAAQ,MAAM,SAAS,EAAE;AAAA,MACzB,iBAAiB,CAAC,SAAS,YAAY,gBAAgB,SAAS,OAAO;AAAA,MACvE,MAAM;AAAA,IAAA;AAAA,IAER,CAAC,SAAS,UAAU,SAAS,QAAQ,SAAS,OAAO,cAAc,UAAU,eAAe;AAAA,EAAA;AAG9F,SAAO,oBAAC,sBAAA,EAAqB,OAAe,SAAA,CAAS;AACvD;"}
@@ -0,0 +1,38 @@
1
+ import { AnchorHTMLAttributes, ReactNode } from 'react';
2
+ export interface IUiNavigationLocation {
3
+ pathname: string;
4
+ search: string;
5
+ /** Ephemeral navigation state (react-router). Hosts without it always expose undefined. */
6
+ state?: unknown;
7
+ }
8
+ export interface IUiLinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
9
+ to: string;
10
+ children?: ReactNode;
11
+ }
12
+ export type TUiSearchParamsSetter = (updater: (previousParams: URLSearchParams) => URLSearchParams, options?: {
13
+ replace?: boolean;
14
+ }) => void;
15
+ /**
16
+ * The navigation port: everything the components need from a router, and nothing else.
17
+ * The host decides who fulfils it: `UiReactRouterNavigationProvider` (react-router apps),
18
+ * a hand-rolled provider on `next/navigation` (Next.js), or nothing at all, in which case
19
+ * a window-based fallback keeps links and URL sync working with full page loads.
20
+ */
21
+ export interface IUiNavigationApi {
22
+ location: IUiNavigationLocation;
23
+ searchParams: URLSearchParams;
24
+ navigate: (to: string, options?: {
25
+ replace?: boolean;
26
+ state?: unknown;
27
+ }) => void;
28
+ goBack: () => void;
29
+ setSearchParams: TUiSearchParamsSetter;
30
+ Link: React.ComponentType<IUiLinkProps>;
31
+ }
32
+ interface IUiNavigationProviderProps {
33
+ value: IUiNavigationApi;
34
+ children: ReactNode;
35
+ }
36
+ export declare const UiNavigationProvider: React.FC<IUiNavigationProviderProps>;
37
+ export declare const useUiNavigation: () => IUiNavigationApi;
38
+ export {};
@@ -0,0 +1,43 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext, useMemo } from "react";
3
+ const FallbackLink = ({ to, children, ...rest }) => /* @__PURE__ */ jsx("a", { href: to, ...rest, children });
4
+ const createFallbackNavigation = () => {
5
+ const hasWindow = typeof window !== "undefined";
6
+ const currentSearch = hasWindow ? window.location.search : "";
7
+ return {
8
+ location: {
9
+ pathname: hasWindow ? window.location.pathname : "/",
10
+ search: currentSearch,
11
+ state: void 0
12
+ },
13
+ searchParams: new URLSearchParams(currentSearch),
14
+ navigate: (to, options) => {
15
+ if (!hasWindow) return;
16
+ if (options?.replace) window.location.replace(to);
17
+ else window.location.assign(to);
18
+ },
19
+ goBack: () => {
20
+ if (hasWindow) window.history.back();
21
+ },
22
+ setSearchParams: (updater, options) => {
23
+ if (!hasWindow) return;
24
+ const nextParams = updater(new URLSearchParams(window.location.search));
25
+ const query = nextParams.toString();
26
+ const nextUrl = `${window.location.pathname}${query ? `?${query}` : ""}${window.location.hash}`;
27
+ if (options?.replace) window.history.replaceState(window.history.state, "", nextUrl);
28
+ else window.history.pushState(window.history.state, "", nextUrl);
29
+ },
30
+ Link: FallbackLink
31
+ };
32
+ };
33
+ const UiNavigationContext = createContext(null);
34
+ const UiNavigationProvider = ({ value, children }) => /* @__PURE__ */ jsx(UiNavigationContext.Provider, { value, children });
35
+ const useUiNavigation = () => {
36
+ const fromContext = useContext(UiNavigationContext);
37
+ return useMemo(() => fromContext ?? createFallbackNavigation(), [fromContext]);
38
+ };
39
+ export {
40
+ UiNavigationProvider,
41
+ useUiNavigation
42
+ };
43
+ //# sourceMappingURL=UiNavigationContext.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"UiNavigationContext.js","sources":["../../../src/components/Navigation/UiNavigationContext.tsx"],"sourcesContent":["import { AnchorHTMLAttributes, createContext, ReactNode, useContext, useMemo } from 'react';\n\nexport interface IUiNavigationLocation {\n pathname: string;\n search: string;\n /** Ephemeral navigation state (react-router). Hosts without it always expose undefined. */\n state?: unknown;\n}\n\nexport interface IUiLinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {\n to: string;\n children?: ReactNode;\n}\n\nexport type TUiSearchParamsSetter = (\n updater: (previousParams: URLSearchParams) => URLSearchParams,\n options?: { replace?: boolean },\n) => void;\n\n/**\n * The navigation port: everything the components need from a router, and nothing else.\n * The host decides who fulfils it: `UiReactRouterNavigationProvider` (react-router apps),\n * a hand-rolled provider on `next/navigation` (Next.js), or nothing at all, in which case\n * a window-based fallback keeps links and URL sync working with full page loads.\n */\nexport interface IUiNavigationApi {\n location: IUiNavigationLocation;\n searchParams: URLSearchParams;\n navigate: (to: string, options?: { replace?: boolean; state?: unknown }) => void;\n goBack: () => void;\n setSearchParams: TUiSearchParamsSetter;\n Link: React.ComponentType<IUiLinkProps>;\n}\n\nconst FallbackLink: React.FC<IUiLinkProps> = ({ to, children, ...rest }) => (\n <a href={to} {...rest}>\n {children}\n </a>\n);\n\nconst createFallbackNavigation = (): IUiNavigationApi => {\n const hasWindow = typeof window !== 'undefined';\n const currentSearch = hasWindow ? window.location.search : '';\n\n return {\n location: {\n pathname: hasWindow ? window.location.pathname : '/',\n search: currentSearch,\n state: undefined,\n },\n searchParams: new URLSearchParams(currentSearch),\n navigate: (to, options) => {\n if (!hasWindow) return;\n if (options?.replace) window.location.replace(to);\n else window.location.assign(to);\n },\n goBack: () => {\n if (hasWindow) window.history.back();\n },\n setSearchParams: (updater, options) => {\n if (!hasWindow) return;\n const nextParams = updater(new URLSearchParams(window.location.search));\n const query = nextParams.toString();\n const nextUrl = `${window.location.pathname}${query ? `?${query}` : ''}${window.location.hash}`;\n if (options?.replace) window.history.replaceState(window.history.state, '', nextUrl);\n else window.history.pushState(window.history.state, '', nextUrl);\n },\n Link: FallbackLink,\n };\n};\n\nconst UiNavigationContext = createContext<IUiNavigationApi | null>(null);\n\ninterface IUiNavigationProviderProps {\n value: IUiNavigationApi;\n children: ReactNode;\n}\n\nexport const UiNavigationProvider: React.FC<IUiNavigationProviderProps> = ({ value, children }) => (\n <UiNavigationContext.Provider value={value}>{children}</UiNavigationContext.Provider>\n);\n\nexport const useUiNavigation = (): IUiNavigationApi => {\n const fromContext = useContext(UiNavigationContext);\n return useMemo(() => fromContext ?? createFallbackNavigation(), [fromContext]);\n};\n"],"names":[],"mappings":";;AAkCA,MAAM,eAAuC,CAAC,EAAE,IAAI,UAAU,GAAG,KAAA,MAC/D,oBAAC,KAAA,EAAE,MAAM,IAAK,GAAG,MACd,SAAA,CACH;AAGF,MAAM,2BAA2B,MAAwB;AACvD,QAAM,YAAY,OAAO,WAAW;AACpC,QAAM,gBAAgB,YAAY,OAAO,SAAS,SAAS;AAE3D,SAAO;AAAA,IACL,UAAU;AAAA,MACR,UAAU,YAAY,OAAO,SAAS,WAAW;AAAA,MACjD,QAAQ;AAAA,MACR,OAAO;AAAA,IAAA;AAAA,IAET,cAAc,IAAI,gBAAgB,aAAa;AAAA,IAC/C,UAAU,CAAC,IAAI,YAAY;AACzB,UAAI,CAAC,UAAW;AAChB,UAAI,SAAS,QAAS,QAAO,SAAS,QAAQ,EAAE;AAAA,UAC3C,QAAO,SAAS,OAAO,EAAE;AAAA,IAChC;AAAA,IACA,QAAQ,MAAM;AACZ,UAAI,UAAW,QAAO,QAAQ,KAAA;AAAA,IAChC;AAAA,IACA,iBAAiB,CAAC,SAAS,YAAY;AACrC,UAAI,CAAC,UAAW;AAChB,YAAM,aAAa,QAAQ,IAAI,gBAAgB,OAAO,SAAS,MAAM,CAAC;AACtE,YAAM,QAAQ,WAAW,SAAA;AACzB,YAAM,UAAU,GAAG,OAAO,SAAS,QAAQ,GAAG,QAAQ,IAAI,KAAK,KAAK,EAAE,GAAG,OAAO,SAAS,IAAI;AAC7F,UAAI,SAAS,QAAS,QAAO,QAAQ,aAAa,OAAO,QAAQ,OAAO,IAAI,OAAO;AAAA,kBACvE,QAAQ,UAAU,OAAO,QAAQ,OAAO,IAAI,OAAO;AAAA,IACjE;AAAA,IACA,MAAM;AAAA,EAAA;AAEV;AAEA,MAAM,sBAAsB,cAAuC,IAAI;AAOhE,MAAM,uBAA6D,CAAC,EAAE,OAAO,SAAA,MAClF,oBAAC,oBAAoB,UAApB,EAA6B,OAAe,SAAA,CAAS;AAGjD,MAAM,kBAAkB,MAAwB;AACrD,QAAM,cAAc,WAAW,mBAAmB;AAClD,SAAO,QAAQ,MAAM,eAAe,4BAA4B,CAAC,WAAW,CAAC;AAC/E;"}
@@ -1,5 +1,5 @@
1
1
  import { useMemo, useCallback, useState, useRef, useEffect } from "react";
2
- import { useSearchParams } from "react-router-dom";
2
+ import { useUiNavigation } from "../../Navigation/UiNavigationContext.js";
3
3
  import { buildEmptyFilterValues, clearDependentValues, emptyValueForKind, getActiveFilterFields, areFilterValuesEqual, countActiveFilters } from "./filterValues.js";
4
4
  const URL_PREFIX = "f.";
5
5
  const serializeValue = (value) => {
@@ -46,7 +46,7 @@ const useFilters = ({
46
46
  urlKey
47
47
  }) => {
48
48
  const safeFields = useMemo(() => Array.isArray(fields) ? fields : [], [fields]);
49
- const [searchParams, setSearchParams] = useSearchParams();
49
+ const { searchParams, setSearchParams } = useUiNavigation();
50
50
  const readFromUrl = useCallback(() => {
51
51
  if (!urlKey) return null;
52
52
  const fromUrl = {};
@@ -1 +1 @@
1
- {"version":3,"file":"useFilters.js","sources":["../../../../src/components/Search/filters/useFilters.ts"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { useSearchParams } from 'react-router-dom';\n\nimport { TFilterField, TFilterValues } from './filterTypes';\nimport {\n areFilterValuesEqual,\n buildEmptyFilterValues,\n clearDependentValues,\n countActiveFilters,\n emptyValueForKind,\n getActiveFilterFields,\n} from './filterValues';\n\nconst URL_PREFIX = 'f.';\n\nconst serializeValue = (value: unknown): string | null => {\n if (value === null || value === undefined) return null;\n if (Array.isArray(value)) return value.length > 0 ? value.map(String).join(',') : null;\n if (typeof value === 'boolean') return value ? '1' : null;\n if (typeof value === 'object') {\n const entries = Object.entries(value as Record<string, unknown>).filter(\n ([, entry]) => entry !== undefined && entry !== null && entry !== '',\n );\n return entries.length > 0 ? JSON.stringify(Object.fromEntries(entries)) : null;\n }\n const asString = String(value);\n\n return asString.trim().length > 0 ? asString : null;\n};\n\nconst deserializeValue = (field: TFilterField, raw: string): unknown => {\n switch (field.kind) {\n case 'multiselect':\n case 'checkbox-group':\n case 'tags':\n return raw.split(',').filter(Boolean);\n case 'boolean':\n return raw === '1' || raw === 'true';\n case 'number':\n return raw;\n case 'date-range':\n case 'range':\n try {\n return JSON.parse(raw);\n } catch {\n return emptyValueForKind(field.kind);\n }\n case 'async-select':\n return field.multiple ? raw.split(',').filter(Boolean) : raw;\n default:\n return raw;\n }\n};\n\nexport interface IUseFiltersParams {\n fields: Array<TFilterField>;\n /** Values owned by the host. When given, the hook is controlled. */\n value?: TFilterValues;\n onChange?: (values: TFilterValues) => void;\n initialValues?: TFilterValues;\n /**\n * Namespace for URL persistence. Without it, filters live only in memory\n * (current behaviour everywhere in the app).\n */\n urlKey?: string;\n}\n\nexport interface IUseFiltersResult {\n /** Values currently in effect on the list. */\n applied: TFilterValues;\n /** Values being edited inside the drawer. */\n draft: TFilterValues;\n isDirty: boolean;\n activeCount: number;\n activeFields: Array<TFilterField>;\n setDraftValue: (key: string, value: unknown) => void;\n setDraft: (values: TFilterValues) => void;\n resetDraft: () => void;\n apply: () => TFilterValues;\n clear: () => TFilterValues;\n removeApplied: (key: string) => TFilterValues;\n}\n\n/**\n * Single owner of filter state: draft while the panel is open, applied once\n * confirmed. Every consumer used to rebuild this by hand, each one differently.\n */\nexport const useFilters = ({\n fields,\n value,\n onChange,\n initialValues,\n urlKey,\n}: IUseFiltersParams): IUseFiltersResult => {\n const safeFields = useMemo(() => (Array.isArray(fields) ? fields : []), [fields]);\n const [searchParams, setSearchParams] = useSearchParams();\n\n const readFromUrl = useCallback((): TFilterValues | null => {\n if (!urlKey) return null;\n const fromUrl: TFilterValues = {};\n let found = false;\n safeFields.forEach((field) => {\n const raw = searchParams.get(`${URL_PREFIX}${urlKey}.${field.key}`);\n if (raw === null) return;\n found = true;\n fromUrl[field.key] = deserializeValue(field, raw);\n });\n\n return found ? fromUrl : null;\n }, [urlKey, safeFields, searchParams]);\n\n const buildInitial = useCallback(\n (): TFilterValues => ({\n ...buildEmptyFilterValues(safeFields),\n ...(initialValues ?? {}),\n ...(readFromUrl() ?? {}),\n }),\n // `readFromUrl` is only meant to seed the first render; re-reading it on\n // every change would fight the user typing in the drawer.\n [safeFields],\n );\n\n const [internalApplied, setInternalApplied] = useState<TFilterValues>(buildInitial);\n const applied = value ?? internalApplied;\n\n const [draft, setDraftState] = useState<TFilterValues>(applied);\n const draftRef = useRef<TFilterValues>(draft);\n\n const commitDraft = useCallback((next: TFilterValues) => {\n draftRef.current = next;\n setDraftState(next);\n }, []);\n\n const appliedKey = JSON.stringify(applied);\n\n // The host can apply values from outside (deep link, a freshly created\n // record, a preset). The draft must follow, or the panel would reopen\n // showing stale values.\n useEffect(() => {\n commitDraft(applied);\n }, [appliedKey, commitDraft]);\n\n const writeToUrl = useCallback(\n (next: TFilterValues) => {\n if (!urlKey) return;\n setSearchParams(\n (current) => {\n const params = new URLSearchParams(current);\n safeFields.forEach((field) => {\n const paramKey = `${URL_PREFIX}${urlKey}.${field.key}`;\n const serialized = serializeValue(next[field.key]);\n if (serialized === null) {\n params.delete(paramKey);\n } else {\n params.set(paramKey, serialized);\n }\n });\n\n return params;\n },\n { replace: true },\n );\n },\n [urlKey, safeFields, setSearchParams],\n );\n\n const publish = useCallback(\n (next: TFilterValues) => {\n if (value === undefined) setInternalApplied(next);\n writeToUrl(next);\n onChange?.(next);\n\n return next;\n },\n [value, writeToUrl, onChange],\n );\n\n const setDraftValue = useCallback(\n (key: string, nextValue: unknown) => {\n const withValue = { ...draftRef.current, [key]: nextValue };\n commitDraft(clearDependentValues(safeFields, key, withValue));\n },\n [safeFields, commitDraft],\n );\n\n const setDraft = useCallback((values: TFilterValues) => commitDraft(values), [commitDraft]);\n\n const resetDraft = useCallback(() => commitDraft(applied), [applied, commitDraft]);\n\n const apply = useCallback(() => publish({ ...draftRef.current }), [publish]);\n\n const clear = useCallback(() => {\n const empty = buildEmptyFilterValues(safeFields);\n commitDraft(empty);\n\n return publish(empty);\n }, [safeFields, commitDraft, publish]);\n\n const removeApplied = useCallback(\n (key: string) => {\n const field = safeFields.find((item) => item.key === key);\n if (!field) return applied;\n const next = clearDependentValues(safeFields, key, {\n ...applied,\n [key]: emptyValueForKind(field.kind),\n });\n commitDraft(next);\n\n return publish(next);\n },\n [safeFields, applied, commitDraft, publish],\n );\n\n const activeFields = useMemo(() => getActiveFilterFields(safeFields, applied), [safeFields, applied]);\n\n const isDirty = useMemo(() => !areFilterValuesEqual(draft, applied), [draft, applied]);\n\n return {\n applied,\n draft,\n isDirty,\n activeCount: countActiveFilters(safeFields, applied),\n activeFields,\n setDraftValue,\n setDraft,\n resetDraft,\n apply,\n clear,\n removeApplied,\n };\n};\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"useFilters.js","sources":["../../../../src/components/Search/filters/useFilters.ts"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from 'react';\n\nimport { useUiNavigation } from '@/components/Navigation/UiNavigationContext';\n\nimport { TFilterField, TFilterValues } from './filterTypes';\nimport {\n areFilterValuesEqual,\n buildEmptyFilterValues,\n clearDependentValues,\n countActiveFilters,\n emptyValueForKind,\n getActiveFilterFields,\n} from './filterValues';\n\nconst URL_PREFIX = 'f.';\n\nconst serializeValue = (value: unknown): string | null => {\n if (value === null || value === undefined) return null;\n if (Array.isArray(value)) return value.length > 0 ? value.map(String).join(',') : null;\n if (typeof value === 'boolean') return value ? '1' : null;\n if (typeof value === 'object') {\n const entries = Object.entries(value as Record<string, unknown>).filter(\n ([, entry]) => entry !== undefined && entry !== null && entry !== '',\n );\n return entries.length > 0 ? JSON.stringify(Object.fromEntries(entries)) : null;\n }\n const asString = String(value);\n\n return asString.trim().length > 0 ? asString : null;\n};\n\nconst deserializeValue = (field: TFilterField, raw: string): unknown => {\n switch (field.kind) {\n case 'multiselect':\n case 'checkbox-group':\n case 'tags':\n return raw.split(',').filter(Boolean);\n case 'boolean':\n return raw === '1' || raw === 'true';\n case 'number':\n return raw;\n case 'date-range':\n case 'range':\n try {\n return JSON.parse(raw);\n } catch {\n return emptyValueForKind(field.kind);\n }\n case 'async-select':\n return field.multiple ? raw.split(',').filter(Boolean) : raw;\n default:\n return raw;\n }\n};\n\nexport interface IUseFiltersParams {\n fields: Array<TFilterField>;\n /** Values owned by the host. When given, the hook is controlled. */\n value?: TFilterValues;\n onChange?: (values: TFilterValues) => void;\n initialValues?: TFilterValues;\n /**\n * Namespace for URL persistence. Without it, filters live only in memory\n * (current behaviour everywhere in the app).\n */\n urlKey?: string;\n}\n\nexport interface IUseFiltersResult {\n /** Values currently in effect on the list. */\n applied: TFilterValues;\n /** Values being edited inside the drawer. */\n draft: TFilterValues;\n isDirty: boolean;\n activeCount: number;\n activeFields: Array<TFilterField>;\n setDraftValue: (key: string, value: unknown) => void;\n setDraft: (values: TFilterValues) => void;\n resetDraft: () => void;\n apply: () => TFilterValues;\n clear: () => TFilterValues;\n removeApplied: (key: string) => TFilterValues;\n}\n\n/**\n * Single owner of filter state: draft while the panel is open, applied once\n * confirmed. Every consumer used to rebuild this by hand, each one differently.\n */\nexport const useFilters = ({\n fields,\n value,\n onChange,\n initialValues,\n urlKey,\n}: IUseFiltersParams): IUseFiltersResult => {\n const safeFields = useMemo(() => (Array.isArray(fields) ? fields : []), [fields]);\n const { searchParams, setSearchParams } = useUiNavigation();\n\n const readFromUrl = useCallback((): TFilterValues | null => {\n if (!urlKey) return null;\n const fromUrl: TFilterValues = {};\n let found = false;\n safeFields.forEach((field) => {\n const raw = searchParams.get(`${URL_PREFIX}${urlKey}.${field.key}`);\n if (raw === null) return;\n found = true;\n fromUrl[field.key] = deserializeValue(field, raw);\n });\n\n return found ? fromUrl : null;\n }, [urlKey, safeFields, searchParams]);\n\n const buildInitial = useCallback(\n (): TFilterValues => ({\n ...buildEmptyFilterValues(safeFields),\n ...(initialValues ?? {}),\n ...(readFromUrl() ?? {}),\n }),\n // `readFromUrl` is only meant to seed the first render; re-reading it on\n // every change would fight the user typing in the drawer.\n [safeFields],\n );\n\n const [internalApplied, setInternalApplied] = useState<TFilterValues>(buildInitial);\n const applied = value ?? internalApplied;\n\n const [draft, setDraftState] = useState<TFilterValues>(applied);\n const draftRef = useRef<TFilterValues>(draft);\n\n const commitDraft = useCallback((next: TFilterValues) => {\n draftRef.current = next;\n setDraftState(next);\n }, []);\n\n const appliedKey = JSON.stringify(applied);\n\n // The host can apply values from outside (deep link, a freshly created\n // record, a preset). The draft must follow, or the panel would reopen\n // showing stale values.\n useEffect(() => {\n commitDraft(applied);\n }, [appliedKey, commitDraft]);\n\n const writeToUrl = useCallback(\n (next: TFilterValues) => {\n if (!urlKey) return;\n setSearchParams(\n (current) => {\n const params = new URLSearchParams(current);\n safeFields.forEach((field) => {\n const paramKey = `${URL_PREFIX}${urlKey}.${field.key}`;\n const serialized = serializeValue(next[field.key]);\n if (serialized === null) {\n params.delete(paramKey);\n } else {\n params.set(paramKey, serialized);\n }\n });\n\n return params;\n },\n { replace: true },\n );\n },\n [urlKey, safeFields, setSearchParams],\n );\n\n const publish = useCallback(\n (next: TFilterValues) => {\n if (value === undefined) setInternalApplied(next);\n writeToUrl(next);\n onChange?.(next);\n\n return next;\n },\n [value, writeToUrl, onChange],\n );\n\n const setDraftValue = useCallback(\n (key: string, nextValue: unknown) => {\n const withValue = { ...draftRef.current, [key]: nextValue };\n commitDraft(clearDependentValues(safeFields, key, withValue));\n },\n [safeFields, commitDraft],\n );\n\n const setDraft = useCallback((values: TFilterValues) => commitDraft(values), [commitDraft]);\n\n const resetDraft = useCallback(() => commitDraft(applied), [applied, commitDraft]);\n\n const apply = useCallback(() => publish({ ...draftRef.current }), [publish]);\n\n const clear = useCallback(() => {\n const empty = buildEmptyFilterValues(safeFields);\n commitDraft(empty);\n\n return publish(empty);\n }, [safeFields, commitDraft, publish]);\n\n const removeApplied = useCallback(\n (key: string) => {\n const field = safeFields.find((item) => item.key === key);\n if (!field) return applied;\n const next = clearDependentValues(safeFields, key, {\n ...applied,\n [key]: emptyValueForKind(field.kind),\n });\n commitDraft(next);\n\n return publish(next);\n },\n [safeFields, applied, commitDraft, publish],\n );\n\n const activeFields = useMemo(() => getActiveFilterFields(safeFields, applied), [safeFields, applied]);\n\n const isDirty = useMemo(() => !areFilterValuesEqual(draft, applied), [draft, applied]);\n\n return {\n applied,\n draft,\n isDirty,\n activeCount: countActiveFilters(safeFields, applied),\n activeFields,\n setDraftValue,\n setDraft,\n resetDraft,\n apply,\n clear,\n removeApplied,\n };\n};\n"],"names":[],"mappings":";;;AAcA,MAAM,aAAa;AAEnB,MAAM,iBAAiB,CAAC,UAAkC;AACxD,MAAI,UAAU,QAAQ,UAAU,OAAW,QAAO;AAClD,MAAI,MAAM,QAAQ,KAAK,UAAU,MAAM,SAAS,IAAI,MAAM,IAAI,MAAM,EAAE,KAAK,GAAG,IAAI;AAClF,MAAI,OAAO,UAAU,UAAW,QAAO,QAAQ,MAAM;AACrD,MAAI,OAAO,UAAU,UAAU;AAC7B,UAAM,UAAU,OAAO,QAAQ,KAAgC,EAAE;AAAA,MAC/D,CAAC,CAAA,EAAG,KAAK,MAAM,UAAU,UAAa,UAAU,QAAQ,UAAU;AAAA,IAAA;AAEpE,WAAO,QAAQ,SAAS,IAAI,KAAK,UAAU,OAAO,YAAY,OAAO,CAAC,IAAI;AAAA,EAC5E;AACA,QAAM,WAAW,OAAO,KAAK;AAE7B,SAAO,SAAS,KAAA,EAAO,SAAS,IAAI,WAAW;AACjD;AAEA,MAAM,mBAAmB,CAAC,OAAqB,QAAyB;AACtE,UAAQ,MAAM,MAAA;AAAA,IACZ,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AACH,aAAO,IAAI,MAAM,GAAG,EAAE,OAAO,OAAO;AAAA,IACtC,KAAK;AACH,aAAO,QAAQ,OAAO,QAAQ;AAAA,IAChC,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AAAA,IACL,KAAK;AACH,UAAI;AACF,eAAO,KAAK,MAAM,GAAG;AAAA,MACvB,QAAQ;AACN,eAAO,kBAAkB,MAAM,IAAI;AAAA,MACrC;AAAA,IACF,KAAK;AACH,aAAO,MAAM,WAAW,IAAI,MAAM,GAAG,EAAE,OAAO,OAAO,IAAI;AAAA,IAC3D;AACE,aAAO;AAAA,EAAA;AAEb;AAmCO,MAAM,aAAa,CAAC;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MAA4C;AAC1C,QAAM,aAAa,QAAQ,MAAO,MAAM,QAAQ,MAAM,IAAI,SAAS,CAAA,GAAK,CAAC,MAAM,CAAC;AAChF,QAAM,EAAE,cAAc,gBAAA,IAAoB,gBAAA;AAE1C,QAAM,cAAc,YAAY,MAA4B;AAC1D,QAAI,CAAC,OAAQ,QAAO;AACpB,UAAM,UAAyB,CAAA;AAC/B,QAAI,QAAQ;AACZ,eAAW,QAAQ,CAAC,UAAU;AAC5B,YAAM,MAAM,aAAa,IAAI,GAAG,UAAU,GAAG,MAAM,IAAI,MAAM,GAAG,EAAE;AAClE,UAAI,QAAQ,KAAM;AAClB,cAAQ;AACR,cAAQ,MAAM,GAAG,IAAI,iBAAiB,OAAO,GAAG;AAAA,IAClD,CAAC;AAED,WAAO,QAAQ,UAAU;AAAA,EAC3B,GAAG,CAAC,QAAQ,YAAY,YAAY,CAAC;AAErC,QAAM,eAAe;AAAA,IACnB,OAAsB;AAAA,MACpB,GAAG,uBAAuB,UAAU;AAAA,MACpC,GAAI,iBAAiB,CAAA;AAAA,MACrB,GAAI,YAAA,KAAiB,CAAA;AAAA,IAAC;AAAA;AAAA;AAAA,IAIxB,CAAC,UAAU;AAAA,EAAA;AAGb,QAAM,CAAC,iBAAiB,kBAAkB,IAAI,SAAwB,YAAY;AAClF,QAAM,UAAU,SAAS;AAEzB,QAAM,CAAC,OAAO,aAAa,IAAI,SAAwB,OAAO;AAC9D,QAAM,WAAW,OAAsB,KAAK;AAE5C,QAAM,cAAc,YAAY,CAAC,SAAwB;AACvD,aAAS,UAAU;AACnB,kBAAc,IAAI;AAAA,EACpB,GAAG,CAAA,CAAE;AAEL,QAAM,aAAa,KAAK,UAAU,OAAO;AAKzC,YAAU,MAAM;AACd,gBAAY,OAAO;AAAA,EACrB,GAAG,CAAC,YAAY,WAAW,CAAC;AAE5B,QAAM,aAAa;AAAA,IACjB,CAAC,SAAwB;AACvB,UAAI,CAAC,OAAQ;AACb;AAAA,QACE,CAAC,YAAY;AACX,gBAAM,SAAS,IAAI,gBAAgB,OAAO;AAC1C,qBAAW,QAAQ,CAAC,UAAU;AAC5B,kBAAM,WAAW,GAAG,UAAU,GAAG,MAAM,IAAI,MAAM,GAAG;AACpD,kBAAM,aAAa,eAAe,KAAK,MAAM,GAAG,CAAC;AACjD,gBAAI,eAAe,MAAM;AACvB,qBAAO,OAAO,QAAQ;AAAA,YACxB,OAAO;AACL,qBAAO,IAAI,UAAU,UAAU;AAAA,YACjC;AAAA,UACF,CAAC;AAED,iBAAO;AAAA,QACT;AAAA,QACA,EAAE,SAAS,KAAA;AAAA,MAAK;AAAA,IAEpB;AAAA,IACA,CAAC,QAAQ,YAAY,eAAe;AAAA,EAAA;AAGtC,QAAM,UAAU;AAAA,IACd,CAAC,SAAwB;AACvB,UAAI,UAAU,OAAW,oBAAmB,IAAI;AAChD,iBAAW,IAAI;AACf,iBAAW,IAAI;AAEf,aAAO;AAAA,IACT;AAAA,IACA,CAAC,OAAO,YAAY,QAAQ;AAAA,EAAA;AAG9B,QAAM,gBAAgB;AAAA,IACpB,CAAC,KAAa,cAAuB;AACnC,YAAM,YAAY,EAAE,GAAG,SAAS,SAAS,CAAC,GAAG,GAAG,UAAA;AAChD,kBAAY,qBAAqB,YAAY,KAAK,SAAS,CAAC;AAAA,IAC9D;AAAA,IACA,CAAC,YAAY,WAAW;AAAA,EAAA;AAG1B,QAAM,WAAW,YAAY,CAAC,WAA0B,YAAY,MAAM,GAAG,CAAC,WAAW,CAAC;AAE1F,QAAM,aAAa,YAAY,MAAM,YAAY,OAAO,GAAG,CAAC,SAAS,WAAW,CAAC;AAEjF,QAAM,QAAQ,YAAY,MAAM,QAAQ,EAAE,GAAG,SAAS,QAAA,CAAS,GAAG,CAAC,OAAO,CAAC;AAE3E,QAAM,QAAQ,YAAY,MAAM;AAC9B,UAAM,QAAQ,uBAAuB,UAAU;AAC/C,gBAAY,KAAK;AAEjB,WAAO,QAAQ,KAAK;AAAA,EACtB,GAAG,CAAC,YAAY,aAAa,OAAO,CAAC;AAErC,QAAM,gBAAgB;AAAA,IACpB,CAAC,QAAgB;AACf,YAAM,QAAQ,WAAW,KAAK,CAAC,SAAS,KAAK,QAAQ,GAAG;AACxD,UAAI,CAAC,MAAO,QAAO;AACnB,YAAM,OAAO,qBAAqB,YAAY,KAAK;AAAA,QACjD,GAAG;AAAA,QACH,CAAC,GAAG,GAAG,kBAAkB,MAAM,IAAI;AAAA,MAAA,CACpC;AACD,kBAAY,IAAI;AAEhB,aAAO,QAAQ,IAAI;AAAA,IACrB;AAAA,IACA,CAAC,YAAY,SAAS,aAAa,OAAO;AAAA,EAAA;AAG5C,QAAM,eAAe,QAAQ,MAAM,sBAAsB,YAAY,OAAO,GAAG,CAAC,YAAY,OAAO,CAAC;AAEpG,QAAM,UAAU,QAAQ,MAAM,CAAC,qBAAqB,OAAO,OAAO,GAAG,CAAC,OAAO,OAAO,CAAC;AAErF,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA,aAAa,mBAAmB,YAAY,OAAO;AAAA,IACnD;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAEJ;"}
@@ -0,0 +1,19 @@
1
+ import { ReactNode } from 'react';
2
+ interface IAppHeaderProps {
3
+ toggleSidebar: () => void;
4
+ isSidebarOpen: boolean;
5
+ toggleLabelKey: string;
6
+ homePath: string;
7
+ /** Replaces the uMode logo; the header stays the same. */
8
+ logo?: ReactNode;
9
+ /** App tools rendered inside the pill shell (workspace, notifications...). */
10
+ toolsSlot?: ReactNode;
11
+ /** Theme/language knobs (usually the lib's ThemeToggle + LanguageToggle wired by the host). */
12
+ controlsSlot?: ReactNode;
13
+ /** The account surface (usually the lib's UserMenu wired by the host). */
14
+ userMenuSlot?: ReactNode;
15
+ onHoverEnter: () => void;
16
+ onHoverLeave: () => void;
17
+ }
18
+ declare const AppHeader: React.FC<IAppHeaderProps>;
19
+ export default AppHeader;
@@ -0,0 +1,64 @@
1
+ import { jsxs, jsx, Fragment } from "react/jsx-runtime";
2
+ import classNames from "classnames";
3
+ import { useTranslation } from "react-i18next";
4
+ import { useUiNavigation } from "../Navigation/UiNavigationContext.js";
5
+ import ControlledTooltip from "../UI/ControlledTooltip.js";
6
+ import { MaterialSymbol } from "../UI/MaterialSymbol/index.js";
7
+ import UmodeLogo from "../UI/UmodeLogo.js";
8
+ import { HEADER_CONTROL_SHELL_CLASS } from "./headerControlStyles.js";
9
+ const AppHeader = ({
10
+ toggleSidebar,
11
+ isSidebarOpen,
12
+ toggleLabelKey,
13
+ homePath,
14
+ logo,
15
+ toolsSlot,
16
+ controlsSlot,
17
+ userMenuSlot,
18
+ onHoverEnter,
19
+ onHoverLeave
20
+ }) => {
21
+ const { t } = useTranslation("ui");
22
+ const { Link: NavigationLink } = useUiNavigation();
23
+ return /* @__PURE__ */ jsxs("header", { className: "fixed top-0 z-50 flex h-16 w-full select-none items-center justify-between border-x-0 border-t-0 py-2 pr-4 glass-subtle", children: [
24
+ /* @__PURE__ */ jsxs("div", { className: "flex cursor-pointer items-center gap-x-2", children: [
25
+ /* @__PURE__ */ jsx("div", { className: "sidebar-rail-slot flex shrink-0 items-center justify-center", children: /* @__PURE__ */ jsx(
26
+ MaterialSymbol,
27
+ {
28
+ icon: isSidebarOpen ? "menu_open" : "menu",
29
+ size: 22,
30
+ weight: 400,
31
+ onClick: toggleSidebar,
32
+ onMouseEnter: onHoverEnter,
33
+ onMouseLeave: onHoverLeave,
34
+ "aria-label": t(toggleLabelKey),
35
+ className: classNames(
36
+ "rounded-lg p-1 transition-colors duration-base",
37
+ isSidebarOpen ? "text-brand-emphasis hover:bg-brand-soft" : "text-foreground-muted hover:bg-muted-hover hover:text-foreground"
38
+ )
39
+ }
40
+ ) }),
41
+ /* @__PURE__ */ jsx(ControlledTooltip, { content: t("sidebar.home"), children: /* @__PURE__ */ jsx(
42
+ NavigationLink,
43
+ {
44
+ to: homePath,
45
+ "aria-label": t("sidebar.home"),
46
+ className: "interactive hidden shrink-0 rounded-lg px-1 text-foreground hover:opacity-70 tablet:block",
47
+ children: logo ?? /* @__PURE__ */ jsx(UmodeLogo, { className: "h-5 w-auto" })
48
+ }
49
+ ) })
50
+ ] }),
51
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-x-2", children: [
52
+ toolsSlot && /* @__PURE__ */ jsx("div", { className: HEADER_CONTROL_SHELL_CLASS, children: toolsSlot }),
53
+ controlsSlot,
54
+ userMenuSlot && /* @__PURE__ */ jsxs(Fragment, { children: [
55
+ /* @__PURE__ */ jsx("div", { className: "mx-1 h-6 w-px shrink-0 bg-border", "aria-hidden": true }),
56
+ userMenuSlot
57
+ ] })
58
+ ] })
59
+ ] });
60
+ };
61
+ export {
62
+ AppHeader as default
63
+ };
64
+ //# sourceMappingURL=AppHeader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AppHeader.js","sources":["../../../src/components/Shell/AppHeader.tsx"],"sourcesContent":["import { ReactNode } from 'react';\nimport classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\n\nimport { useUiNavigation } from '@/components/Navigation/UiNavigationContext';\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { MaterialSymbol } from '@/components/UI/MaterialSymbol';\nimport UmodeLogo from '@/components/UI/UmodeLogo';\n\nimport { HEADER_CONTROL_SHELL_CLASS } from './headerControlStyles';\n\ninterface IAppHeaderProps {\n toggleSidebar: () => void;\n isSidebarOpen: boolean;\n toggleLabelKey: string;\n homePath: string;\n /** Replaces the uMode logo; the header stays the same. */\n logo?: ReactNode;\n /** App tools rendered inside the pill shell (workspace, notifications...). */\n toolsSlot?: ReactNode;\n /** Theme/language knobs (usually the lib's ThemeToggle + LanguageToggle wired by the host). */\n controlsSlot?: ReactNode;\n /** The account surface (usually the lib's UserMenu wired by the host). */\n userMenuSlot?: ReactNode;\n onHoverEnter: () => void;\n onHoverLeave: () => void;\n}\n\nconst AppHeader: React.FC<IAppHeaderProps> = ({\n toggleSidebar,\n isSidebarOpen,\n toggleLabelKey,\n homePath,\n logo,\n toolsSlot,\n controlsSlot,\n userMenuSlot,\n onHoverEnter,\n onHoverLeave,\n}) => {\n const { t } = useTranslation('ui');\n const { Link: NavigationLink } = useUiNavigation();\n\n return (\n <header className=\"fixed top-0 z-50 flex h-16 w-full select-none items-center justify-between border-x-0 border-t-0 py-2 pr-4 glass-subtle\">\n <div className=\"flex cursor-pointer items-center gap-x-2\">\n <div className=\"sidebar-rail-slot flex shrink-0 items-center justify-center\">\n <MaterialSymbol\n icon={isSidebarOpen ? 'menu_open' : 'menu'}\n size={22}\n weight={400}\n onClick={toggleSidebar}\n onMouseEnter={onHoverEnter}\n onMouseLeave={onHoverLeave}\n aria-label={t(toggleLabelKey)}\n className={classNames(\n 'rounded-lg p-1 transition-colors duration-base',\n isSidebarOpen\n ? 'text-brand-emphasis hover:bg-brand-soft'\n : 'text-foreground-muted hover:bg-muted-hover hover:text-foreground',\n )}\n />\n </div>\n\n <ControlledTooltip content={t('sidebar.home')}>\n <NavigationLink\n to={homePath}\n aria-label={t('sidebar.home')}\n className=\"interactive hidden shrink-0 rounded-lg px-1 text-foreground hover:opacity-70 tablet:block\"\n >\n {logo ?? <UmodeLogo className=\"h-5 w-auto\" />}\n </NavigationLink>\n </ControlledTooltip>\n </div>\n\n <div className=\"flex items-center gap-x-2\">\n {toolsSlot && <div className={HEADER_CONTROL_SHELL_CLASS}>{toolsSlot}</div>}\n {controlsSlot}\n\n {userMenuSlot && (\n <>\n <div className=\"mx-1 h-6 w-px shrink-0 bg-border\" aria-hidden />\n {userMenuSlot}\n </>\n )}\n </div>\n </header>\n );\n};\n\nexport default AppHeader;\n"],"names":[],"mappings":";;;;;;;;AA4BA,MAAM,YAAuC,CAAC;AAAA,EAC5C;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,EAAE,MAAM,eAAA,IAAmB,gBAAA;AAEjC,SACE,qBAAC,UAAA,EAAO,WAAU,2HAChB,UAAA;AAAA,IAAA,qBAAC,OAAA,EAAI,WAAU,4CACb,UAAA;AAAA,MAAA,oBAAC,OAAA,EAAI,WAAU,+DACb,UAAA;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,MAAM,gBAAgB,cAAc;AAAA,UACpC,MAAM;AAAA,UACN,QAAQ;AAAA,UACR,SAAS;AAAA,UACT,cAAc;AAAA,UACd,cAAc;AAAA,UACd,cAAY,EAAE,cAAc;AAAA,UAC5B,WAAW;AAAA,YACT;AAAA,YACA,gBACI,4CACA;AAAA,UAAA;AAAA,QACN;AAAA,MAAA,GAEJ;AAAA,MAEA,oBAAC,mBAAA,EAAkB,SAAS,EAAE,cAAc,GAC1C,UAAA;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,IAAI;AAAA,UACJ,cAAY,EAAE,cAAc;AAAA,UAC5B,WAAU;AAAA,UAET,UAAA,QAAQ,oBAAC,WAAA,EAAU,WAAU,aAAA,CAAa;AAAA,QAAA;AAAA,MAAA,EAC7C,CACF;AAAA,IAAA,GACF;AAAA,IAEA,qBAAC,OAAA,EAAI,WAAU,6BACZ,UAAA;AAAA,MAAA,aAAa,oBAAC,OAAA,EAAI,WAAW,4BAA6B,UAAA,WAAU;AAAA,MACpE;AAAA,MAEA,gBACC,qBAAA,UAAA,EACE,UAAA;AAAA,QAAA,oBAAC,OAAA,EAAI,WAAU,oCAAmC,eAAW,MAAC;AAAA,QAC7D;AAAA,MAAA,EAAA,CACH;AAAA,IAAA,EAAA,CAEJ;AAAA,EAAA,GACF;AAEJ;"}
@@ -0,0 +1,20 @@
1
+ import { ReactNode } from 'react';
2
+ import { ISidebarMenuItem } from './SidebarMenu';
3
+ interface IAppShellProps {
4
+ items: Array<ISidebarMenuItem>;
5
+ homePath: string;
6
+ logo?: ReactNode;
7
+ toolsSlot?: ReactNode;
8
+ controlsSlot?: ReactNode;
9
+ userMenuSlot?: ReactNode;
10
+ /** Sidebar footer sign-out row; hidden when absent. */
11
+ onLogout?: () => void;
12
+ children: ReactNode;
13
+ }
14
+ /**
15
+ * The uMode application chrome: fixed glass header, rail/dock/hover sidebar (pin persisted
16
+ * through uiPreferences, Ctrl+\ shortcut) and the scroll container every page renders into.
17
+ * Everything app-shaped (menu items, tools, account surface, logout) arrives as props/slots.
18
+ */
19
+ declare const AppShell: React.FC<IAppShellProps>;
20
+ export default AppShell;
@@ -0,0 +1,100 @@
1
+ import { jsxs, jsx } from "react/jsx-runtime";
2
+ import { useRef, useEffect } from "react";
3
+ import classNames from "classnames";
4
+ import { useUiNavigation } from "../Navigation/UiNavigationContext.js";
5
+ import AppHeader from "./AppHeader.js";
6
+ import AppSidebar from "./AppSidebar.js";
7
+ import { useSidebarState } from "./useSidebarState.js";
8
+ /* empty css */
9
+ const AppShell = ({
10
+ items,
11
+ homePath,
12
+ logo,
13
+ toolsSlot,
14
+ controlsSlot,
15
+ userMenuSlot,
16
+ onLogout,
17
+ children
18
+ }) => {
19
+ const { location } = useUiNavigation();
20
+ const scrollRef = useRef(null);
21
+ const {
22
+ isMobile,
23
+ isVisible,
24
+ isDocked,
25
+ isOverlayOnMobile,
26
+ showRail,
27
+ toggleLabelKey,
28
+ togglePrimary,
29
+ onHoverEnter,
30
+ onHoverLeave,
31
+ closeMobileDrawer
32
+ } = useSidebarState();
33
+ useEffect(() => {
34
+ scrollRef.current?.scrollTo({ top: 0, behavior: "smooth" });
35
+ }, [location.pathname]);
36
+ return /* @__PURE__ */ jsxs("div", { className: "h-screen w-full bg-surface", children: [
37
+ /* @__PURE__ */ jsx(
38
+ AppHeader,
39
+ {
40
+ toggleSidebar: togglePrimary,
41
+ isSidebarOpen: isDocked,
42
+ toggleLabelKey,
43
+ homePath,
44
+ logo,
45
+ toolsSlot,
46
+ controlsSlot,
47
+ userMenuSlot,
48
+ onHoverEnter,
49
+ onHoverLeave
50
+ }
51
+ ),
52
+ /* @__PURE__ */ jsxs(
53
+ "main",
54
+ {
55
+ className: classNames("sidebar-content flex h-screen w-full", {
56
+ "is-rail": !isMobile && !isDocked,
57
+ "is-mobile": isMobile
58
+ }),
59
+ children: [
60
+ isOverlayOnMobile && /* @__PURE__ */ jsx(
61
+ "div",
62
+ {
63
+ "aria-hidden": true,
64
+ className: "fixed inset-0 z-40 bg-scrim motion-reduce:transition-none",
65
+ onClick: closeMobileDrawer
66
+ }
67
+ ),
68
+ /* @__PURE__ */ jsx(
69
+ AppSidebar,
70
+ {
71
+ items,
72
+ isMobile,
73
+ isVisible,
74
+ collapsed: showRail,
75
+ isFloating: !isMobile && !showRail && !isDocked,
76
+ onHoverEnter,
77
+ onHoverLeave,
78
+ onNavigate: isMobile ? closeMobileDrawer : void 0,
79
+ onLogout
80
+ }
81
+ ),
82
+ /* @__PURE__ */ jsx(
83
+ "div",
84
+ {
85
+ id: "scrollableDiv",
86
+ ref: scrollRef,
87
+ className: "z-30 h-screen w-full px-4 pt-24 tablet:px-6",
88
+ style: { overflow: "auto" },
89
+ children
90
+ }
91
+ )
92
+ ]
93
+ }
94
+ )
95
+ ] });
96
+ };
97
+ export {
98
+ AppShell as default
99
+ };
100
+ //# sourceMappingURL=AppShell.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AppShell.js","sources":["../../../src/components/Shell/AppShell.tsx"],"sourcesContent":["import { ReactNode, useEffect, useRef } from 'react';\nimport classNames from 'classnames';\n\nimport { useUiNavigation } from '@/components/Navigation/UiNavigationContext';\n\nimport AppHeader from './AppHeader';\nimport AppSidebar from './AppSidebar';\nimport { ISidebarMenuItem } from './SidebarMenu';\nimport { useSidebarState } from './useSidebarState';\n\nimport '../../styles/sidebar.css';\n\ninterface IAppShellProps {\n items: Array<ISidebarMenuItem>;\n homePath: string;\n logo?: ReactNode;\n toolsSlot?: ReactNode;\n controlsSlot?: ReactNode;\n userMenuSlot?: ReactNode;\n /** Sidebar footer sign-out row; hidden when absent. */\n onLogout?: () => void;\n children: ReactNode;\n}\n\n/**\n * The uMode application chrome: fixed glass header, rail/dock/hover sidebar (pin persisted\n * through uiPreferences, Ctrl+\\ shortcut) and the scroll container every page renders into.\n * Everything app-shaped (menu items, tools, account surface, logout) arrives as props/slots.\n */\nconst AppShell: React.FC<IAppShellProps> = ({\n items,\n homePath,\n logo,\n toolsSlot,\n controlsSlot,\n userMenuSlot,\n onLogout,\n children,\n}) => {\n const { location } = useUiNavigation();\n const scrollRef = useRef<HTMLDivElement>(null);\n\n const {\n isMobile,\n isVisible,\n isDocked,\n isOverlayOnMobile,\n showRail,\n toggleLabelKey,\n togglePrimary,\n onHoverEnter,\n onHoverLeave,\n closeMobileDrawer,\n } = useSidebarState();\n\n useEffect(() => {\n scrollRef.current?.scrollTo({ top: 0, behavior: 'smooth' });\n }, [location.pathname]);\n\n return (\n <div className=\"h-screen w-full bg-surface\">\n <AppHeader\n toggleSidebar={togglePrimary}\n isSidebarOpen={isDocked}\n toggleLabelKey={toggleLabelKey}\n homePath={homePath}\n logo={logo}\n toolsSlot={toolsSlot}\n controlsSlot={controlsSlot}\n userMenuSlot={userMenuSlot}\n onHoverEnter={onHoverEnter}\n onHoverLeave={onHoverLeave}\n />\n\n <main\n className={classNames('sidebar-content flex h-screen w-full', {\n 'is-rail': !isMobile && !isDocked,\n 'is-mobile': isMobile,\n })}\n >\n {isOverlayOnMobile && (\n <div\n aria-hidden\n className=\"fixed inset-0 z-40 bg-scrim motion-reduce:transition-none\"\n onClick={closeMobileDrawer}\n />\n )}\n\n <AppSidebar\n items={items}\n isMobile={isMobile}\n isVisible={isVisible}\n collapsed={showRail}\n isFloating={!isMobile && !showRail && !isDocked}\n onHoverEnter={onHoverEnter}\n onHoverLeave={onHoverLeave}\n onNavigate={isMobile ? closeMobileDrawer : undefined}\n onLogout={onLogout}\n />\n\n <div\n id=\"scrollableDiv\"\n ref={scrollRef}\n className=\"z-30 h-screen w-full px-4 pt-24 tablet:px-6\"\n style={{ overflow: 'auto' }}\n >\n {children}\n </div>\n </main>\n </div>\n );\n};\n\nexport default AppShell;\n"],"names":[],"mappings":";;;;;;;;AA6BA,MAAM,WAAqC,CAAC;AAAA,EAC1C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MAAM;AACJ,QAAM,EAAE,SAAA,IAAa,gBAAA;AACrB,QAAM,YAAY,OAAuB,IAAI;AAE7C,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA,IACE,gBAAA;AAEJ,YAAU,MAAM;AACd,cAAU,SAAS,SAAS,EAAE,KAAK,GAAG,UAAU,UAAU;AAAA,EAC5D,GAAG,CAAC,SAAS,QAAQ,CAAC;AAEtB,SACE,qBAAC,OAAA,EAAI,WAAU,8BACb,UAAA;AAAA,IAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,eAAe;AAAA,QACf,eAAe;AAAA,QACf;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MAAA;AAAA,IAAA;AAAA,IAGF;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,WAAW,WAAW,wCAAwC;AAAA,UAC5D,WAAW,CAAC,YAAY,CAAC;AAAA,UACzB,aAAa;AAAA,QAAA,CACd;AAAA,QAEA,UAAA;AAAA,UAAA,qBACC;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,eAAW;AAAA,cACX,WAAU;AAAA,cACV,SAAS;AAAA,YAAA;AAAA,UAAA;AAAA,UAIb;AAAA,YAAC;AAAA,YAAA;AAAA,cACC;AAAA,cACA;AAAA,cACA;AAAA,cACA,WAAW;AAAA,cACX,YAAY,CAAC,YAAY,CAAC,YAAY,CAAC;AAAA,cACvC;AAAA,cACA;AAAA,cACA,YAAY,WAAW,oBAAoB;AAAA,cAC3C;AAAA,YAAA;AAAA,UAAA;AAAA,UAGF;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,IAAG;AAAA,cACH,KAAK;AAAA,cACL,WAAU;AAAA,cACV,OAAO,EAAE,UAAU,OAAA;AAAA,cAElB;AAAA,YAAA;AAAA,UAAA;AAAA,QACH;AAAA,MAAA;AAAA,IAAA;AAAA,EACF,GACF;AAEJ;"}
@@ -0,0 +1,15 @@
1
+ import { ISidebarMenuItem } from './SidebarMenu';
2
+ interface IAppSidebarProps {
3
+ items: Array<ISidebarMenuItem>;
4
+ isMobile: boolean;
5
+ isVisible: boolean;
6
+ collapsed: boolean;
7
+ isFloating: boolean;
8
+ onHoverEnter: () => void;
9
+ onHoverLeave: () => void;
10
+ onNavigate?: () => void;
11
+ /** Hides the footer sign-out row when absent. */
12
+ onLogout?: () => void;
13
+ }
14
+ declare const AppSidebar: React.FC<IAppSidebarProps>;
15
+ export default AppSidebar;
@@ -0,0 +1,63 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import classNames from "classnames";
3
+ import { useTranslation } from "react-i18next";
4
+ import ControlledButton from "../Form/ControlledButton.js";
5
+ import { MaterialSymbol } from "../UI/MaterialSymbol/index.js";
6
+ import SidebarMenu from "./SidebarMenu.js";
7
+ const AppSidebar = ({
8
+ items,
9
+ isMobile,
10
+ isVisible,
11
+ collapsed,
12
+ isFloating,
13
+ onHoverEnter,
14
+ onHoverLeave,
15
+ onNavigate,
16
+ onLogout
17
+ }) => {
18
+ const { t } = useTranslation("ui");
19
+ return /* @__PURE__ */ jsx(
20
+ "aside",
21
+ {
22
+ onMouseEnter: onHoverEnter,
23
+ onMouseLeave: onHoverLeave,
24
+ className: classNames(
25
+ "sidebar-aside fixed left-0 top-0 z-40 h-full overflow-hidden border-r border-border bg-muted",
26
+ {
27
+ "is-rail": !isMobile && collapsed,
28
+ "is-mobile": isMobile,
29
+ "is-floating": isFloating,
30
+ "is-hidden pointer-events-none": isMobile && !isVisible
31
+ }
32
+ ),
33
+ 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
+ ),
44
+ onLogout && /* @__PURE__ */ jsx("div", { className: "text-foreground-muted", children: /* @__PURE__ */ jsxs(
45
+ ControlledButton,
46
+ {
47
+ defaultHtmlButton: true,
48
+ onClick: onLogout,
49
+ 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",
50
+ children: [
51
+ /* @__PURE__ */ jsx(MaterialSymbol, { icon: "logout", size: 20, weight: 400, className: "shrink-0" }),
52
+ /* @__PURE__ */ jsx("span", { className: classNames("sidebar-label text-sm", { "is-collapsed": collapsed }), children: t("sidebar.logout") })
53
+ ]
54
+ }
55
+ ) })
56
+ ] })
57
+ }
58
+ );
59
+ };
60
+ export {
61
+ AppSidebar as default
62
+ };
63
+ //# sourceMappingURL=AppSidebar.js.map
@@ -0,0 +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;"}
@@ -0,0 +1,8 @@
1
+ export type TUiLanguage = 'pt' | 'en';
2
+ interface ILanguageToggleProps {
3
+ language: TUiLanguage;
4
+ onChange: (language: TUiLanguage) => void;
5
+ }
6
+ /** PT/EN knob; the host owns the i18n instance and persistence. */
7
+ declare const LanguageToggle: React.FC<ILanguageToggleProps>;
8
+ export default LanguageToggle;