@umodeapporg/ui 0.2.0 → 0.3.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 (40) hide show
  1. package/README.md +78 -0
  2. package/dist/components/Form/ControlledSelect.js +1 -1
  3. package/dist/components/Form/ControlledTabs.js +4 -4
  4. package/dist/components/Form/ControlledTabs.js.map +1 -1
  5. package/dist/components/Form/ControlledViewToggle.js +2 -2
  6. package/dist/components/Form/ControlledViewToggle.js.map +1 -1
  7. package/dist/components/Navigation/ControlledBreadcrumb.js +3 -2
  8. package/dist/components/Navigation/ControlledBreadcrumb.js.map +1 -1
  9. package/dist/components/Navigation/PageTitle.js +3 -6
  10. package/dist/components/Navigation/PageTitle.js.map +1 -1
  11. package/dist/components/Navigation/ReactRouterNavigationAdapter.d.ts +7 -0
  12. package/dist/components/Navigation/ReactRouterNavigationAdapter.js +26 -0
  13. package/dist/components/Navigation/ReactRouterNavigationAdapter.js.map +1 -0
  14. package/dist/components/Navigation/UiNavigationContext.d.ts +38 -0
  15. package/dist/components/Navigation/UiNavigationContext.js +43 -0
  16. package/dist/components/Navigation/UiNavigationContext.js.map +1 -0
  17. package/dist/components/Search/filters/useFilters.js +2 -2
  18. package/dist/components/Search/filters/useFilters.js.map +1 -1
  19. package/dist/components/UI/BaseDataCard.js +3 -2
  20. package/dist/components/UI/BaseDataCard.js.map +1 -1
  21. package/dist/components/UI/DataTable/DataTable.js +1 -1
  22. package/dist/components/UI/DataTable/DataTableBulkEditDialog.js +1 -1
  23. package/dist/components/UI/DataTable/DataTableFilters.js +1 -1
  24. package/dist/components/UI/DataTable/DataTableToolbar.js +1 -1
  25. package/dist/components/UI/DataTable/applyDataTableFilters.js +1 -1
  26. package/dist/components/UI/UserMenu.d.ts +48 -0
  27. package/dist/components/UI/UserMenu.js +196 -0
  28. package/dist/components/UI/UserMenu.js.map +1 -0
  29. package/dist/index.d.ts +2 -0
  30. package/dist/index.js +5 -0
  31. package/dist/index.js.map +1 -1
  32. package/dist/locales/en/ui.json.d.ts +7 -0
  33. package/dist/locales/en/ui.json.js +3 -0
  34. package/dist/locales/en/ui.json.js.map +1 -1
  35. package/dist/locales/index.d.ts +14 -0
  36. package/dist/locales/pt/ui.json.d.ts +7 -0
  37. package/dist/locales/pt/ui.json.js +3 -0
  38. package/dist/locales/pt/ui.json.js.map +1 -1
  39. package/package.json +6 -3
  40. package/src/assets/styles/base.css +0 -1
@@ -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":"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"}
@@ -1,7 +1,7 @@
1
1
  import { jsxs, jsx, Fragment } from "react/jsx-runtime";
2
2
  import { useState, useRef } from "react";
3
3
  import classNames from "classnames";
4
- import { Link } from "react-router-dom";
4
+ import { useUiNavigation } from "../Navigation/UiNavigationContext.js";
5
5
  import ControlledTooltip from "./ControlledTooltip.js";
6
6
  import { MaterialSymbol } from "./MaterialSymbol/index.js";
7
7
  import { uiLogger } from "../../helpers/uiLogger.js";
@@ -29,6 +29,7 @@ const BaseDataCard = ({
29
29
  onSecondarySlot,
30
30
  infoTooltip
31
31
  }) => {
32
+ const { Link: NavigationLink } = useUiNavigation();
32
33
  const [isArrowUpward, setIsArrowUpward] = useState(false);
33
34
  const cardRef = useRef(null);
34
35
  const handleActionClick = () => {
@@ -99,7 +100,7 @@ const BaseDataCard = ({
99
100
  ) })
100
101
  ] }) }),
101
102
  /* @__PURE__ */ jsx("div", { children: loading ? /* @__PURE__ */ jsx(SkeletonLoading, { type: "circle", className: "ml-3" }) : /* @__PURE__ */ jsxs("div", { className: "flex items-center space-x-2", children: [
102
- actionHref && /* @__PURE__ */ jsx(Link, { to: actionHref, children: /* @__PURE__ */ jsx(
103
+ actionHref && /* @__PURE__ */ jsx(NavigationLink, { to: actionHref, children: /* @__PURE__ */ jsx(
103
104
  ControlledButton,
104
105
  {
105
106
  defaultHtmlButton: true,
@@ -1 +1 @@
1
- {"version":3,"file":"BaseDataCard.js","sources":["../../../src/components/UI/BaseDataCard.tsx"],"sourcesContent":["import { useRef, useState } from 'react';\nimport * as React from 'react';\nimport classNames from 'classnames';\nimport { Link } from 'react-router-dom';\n\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { MaterialSymbol, SymbolCodepoints } from '@/components/UI/MaterialSymbol';\nimport { uiLogger } from '@/helpers/uiLogger';\n\nimport ControlledButton from '../Form/ControlledButton';\nimport SkeletonLoading from '../UI/SkeletonLoading';\n\nimport CustomCard from './CustomCard';\ninterface IBaseDataCardProps extends React.HTMLAttributes<HTMLDivElement> {\n title?: string;\n smallTitle?: boolean;\n titleIcon?: SymbolCodepoints | null;\n titleClass?: string;\n cardIcon?: SymbolCodepoints;\n iconTooltip?: string;\n iconColor?: string;\n iconSize?: number;\n itsClickable?: boolean;\n actionIcon?: SymbolCodepoints;\n actionHref?: string;\n actionButton?: () => void;\n onAction?: () => void;\n actionButtonLabel?: string;\n showActionButton?: boolean;\n useScroll?: boolean;\n loading?: boolean;\n onSecondarySlot?: React.ReactNode;\n infoTooltip?: string;\n}\n\nconst BaseDataCard: React.FC<IBaseDataCardProps> = ({\n title,\n smallTitle,\n titleIcon,\n titleClass,\n cardIcon,\n iconTooltip,\n itsClickable = false,\n actionIcon = 'edit',\n actionHref,\n className,\n actionButton,\n actionButtonLabel,\n showActionButton = false,\n onAction,\n useScroll = false,\n loading = false,\n children,\n onSecondarySlot,\n infoTooltip,\n}) => {\n const [isArrowUpward, setIsArrowUpward] = useState(false);\n const cardRef = useRef<HTMLDivElement>(null);\n\n const handleActionClick = () => {\n const newArrowUpwardState = !isArrowUpward;\n setIsArrowUpward(newArrowUpwardState);\n if (actionButton) {\n actionButton();\n }\n if (!newArrowUpwardState && cardRef.current && useScroll) {\n setTimeout(() => {\n uiLogger.debug('Scrolling into view');\n cardRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' });\n }, 1);\n }\n };\n\n return (\n <CustomCard\n className={classNames('overflow-y-hidden p-5 transition-colors', {\n [`${className}`]: !!className,\n 'h-[30rem]': useScroll,\n ['hover:transition-custom hover:ease-custom cursor-pointer hover:bg-success-soft hover:duration-slow']:\n itsClickable,\n })}\n customRef={cardRef}\n >\n <div className=\"flex h-full flex-col justify-between\">\n <div className=\"mb-3 flex items-center justify-between\">\n <div className=\"flex w-full items-center justify-between\">\n {loading ? (\n <SkeletonLoading className=\"!h-4\" />\n ) : (\n <div className=\"flex items-center gap-x-1.5\">\n {titleIcon &&\n (iconTooltip ? (\n <ControlledTooltip content={iconTooltip} className=\"max-w-80\">\n <MaterialSymbol\n icon={titleIcon}\n size={smallTitle ? 18 : 20}\n color=\"rgb(var(--color-foreground-subtle))\"\n className=\"!hidden desktop:!block\"\n />\n </ControlledTooltip>\n ) : (\n <MaterialSymbol\n icon={titleIcon}\n size={smallTitle ? 18 : 20}\n color=\"rgb(var(--color-foreground-subtle))\"\n className=\"!hidden desktop:!block\"\n />\n ))}\n\n {title && (\n <span\n className={classNames(\n 'font-semibold text-foreground',\n {\n 'text-xs': smallTitle,\n 'text-lg': !smallTitle,\n },\n titleClass,\n )}\n >\n {title}\n </span>\n )}\n\n {infoTooltip && (\n <ControlledTooltip content={infoTooltip} className=\"max-w-80\">\n <MaterialSymbol\n icon=\"info\"\n size={smallTitle ? 16 : 18}\n color=\"rgb(var(--color-foreground-subtle))\"\n className=\"ml-1 cursor-help\"\n />\n </ControlledTooltip>\n )}\n </div>\n )}\n </div>\n\n <div>\n {loading ? (\n <SkeletonLoading type=\"circle\" className=\"ml-3\" />\n ) : (\n <div className=\"flex items-center space-x-2\">\n {actionHref && (\n <Link to={actionHref}>\n <ControlledButton\n defaultHtmlButton\n onClick={onAction}\n className=\"flex size-9 items-center justify-center rounded-full transition-colors hover:bg-muted\"\n >\n <MaterialSymbol color=\"rgb(var(--color-primary))\" icon={actionIcon} />\n </ControlledButton>\n </Link>\n )}\n\n {onAction && (\n <ControlledButton\n defaultHtmlButton\n onClick={onAction}\n className=\"flex size-9 items-center justify-center rounded-full transition-colors hover:bg-muted\"\n >\n <MaterialSymbol color=\"rgb(var(--color-primary))\" icon={actionIcon} />\n </ControlledButton>\n )}\n\n {onSecondarySlot && <>{onSecondarySlot}</>}\n\n {cardIcon && <MaterialSymbol icon={cardIcon} size={25} color=\"rgb(var(--color-primary))\" />}\n </div>\n )}\n </div>\n </div>\n\n <div\n className={classNames('m-auto', {\n 'w-[90%]': useScroll,\n 'w-full': !useScroll,\n })}\n >\n {children}\n </div>\n </div>\n\n {showActionButton && actionButtonLabel && (\n <div className=\"flex h-20 w-full items-center justify-center rounded-md bg-gradient-to-t from-surface to-transparent\">\n <ControlledButton\n className=\"flex w-full max-w-full items-center justify-center gap-x-3 rounded-full px-4 py-2 text-sm font-semibold capitalize text-white tablet:w-60\"\n onClick={handleActionClick}\n >\n <MaterialSymbol\n icon=\"arrow_downward\"\n size={20}\n color=\"white\"\n className={classNames('transition-transform duration-slow', {\n 'rotate-180': isArrowUpward,\n })}\n />\n\n {actionButtonLabel}\n </ControlledButton>\n </div>\n )}\n </CustomCard>\n );\n};\n\nexport default BaseDataCard;\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"BaseDataCard.js","sources":["../../../src/components/UI/BaseDataCard.tsx"],"sourcesContent":["import { useRef, useState } from 'react';\nimport * as React from 'react';\nimport classNames from 'classnames';\n\nimport { useUiNavigation } from '@/components/Navigation/UiNavigationContext';\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { MaterialSymbol, SymbolCodepoints } from '@/components/UI/MaterialSymbol';\nimport { uiLogger } from '@/helpers/uiLogger';\n\nimport ControlledButton from '../Form/ControlledButton';\nimport SkeletonLoading from '../UI/SkeletonLoading';\n\nimport CustomCard from './CustomCard';\ninterface IBaseDataCardProps extends React.HTMLAttributes<HTMLDivElement> {\n title?: string;\n smallTitle?: boolean;\n titleIcon?: SymbolCodepoints | null;\n titleClass?: string;\n cardIcon?: SymbolCodepoints;\n iconTooltip?: string;\n iconColor?: string;\n iconSize?: number;\n itsClickable?: boolean;\n actionIcon?: SymbolCodepoints;\n actionHref?: string;\n actionButton?: () => void;\n onAction?: () => void;\n actionButtonLabel?: string;\n showActionButton?: boolean;\n useScroll?: boolean;\n loading?: boolean;\n onSecondarySlot?: React.ReactNode;\n infoTooltip?: string;\n}\n\nconst BaseDataCard: React.FC<IBaseDataCardProps> = ({\n title,\n smallTitle,\n titleIcon,\n titleClass,\n cardIcon,\n iconTooltip,\n itsClickable = false,\n actionIcon = 'edit',\n actionHref,\n className,\n actionButton,\n actionButtonLabel,\n showActionButton = false,\n onAction,\n useScroll = false,\n loading = false,\n children,\n onSecondarySlot,\n infoTooltip,\n}) => {\n const { Link: NavigationLink } = useUiNavigation();\n const [isArrowUpward, setIsArrowUpward] = useState(false);\n const cardRef = useRef<HTMLDivElement>(null);\n\n const handleActionClick = () => {\n const newArrowUpwardState = !isArrowUpward;\n setIsArrowUpward(newArrowUpwardState);\n if (actionButton) {\n actionButton();\n }\n if (!newArrowUpwardState && cardRef.current && useScroll) {\n setTimeout(() => {\n uiLogger.debug('Scrolling into view');\n cardRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' });\n }, 1);\n }\n };\n\n return (\n <CustomCard\n className={classNames('overflow-y-hidden p-5 transition-colors', {\n [`${className}`]: !!className,\n 'h-[30rem]': useScroll,\n ['hover:transition-custom hover:ease-custom cursor-pointer hover:bg-success-soft hover:duration-slow']:\n itsClickable,\n })}\n customRef={cardRef}\n >\n <div className=\"flex h-full flex-col justify-between\">\n <div className=\"mb-3 flex items-center justify-between\">\n <div className=\"flex w-full items-center justify-between\">\n {loading ? (\n <SkeletonLoading className=\"!h-4\" />\n ) : (\n <div className=\"flex items-center gap-x-1.5\">\n {titleIcon &&\n (iconTooltip ? (\n <ControlledTooltip content={iconTooltip} className=\"max-w-80\">\n <MaterialSymbol\n icon={titleIcon}\n size={smallTitle ? 18 : 20}\n color=\"rgb(var(--color-foreground-subtle))\"\n className=\"!hidden desktop:!block\"\n />\n </ControlledTooltip>\n ) : (\n <MaterialSymbol\n icon={titleIcon}\n size={smallTitle ? 18 : 20}\n color=\"rgb(var(--color-foreground-subtle))\"\n className=\"!hidden desktop:!block\"\n />\n ))}\n\n {title && (\n <span\n className={classNames(\n 'font-semibold text-foreground',\n {\n 'text-xs': smallTitle,\n 'text-lg': !smallTitle,\n },\n titleClass,\n )}\n >\n {title}\n </span>\n )}\n\n {infoTooltip && (\n <ControlledTooltip content={infoTooltip} className=\"max-w-80\">\n <MaterialSymbol\n icon=\"info\"\n size={smallTitle ? 16 : 18}\n color=\"rgb(var(--color-foreground-subtle))\"\n className=\"ml-1 cursor-help\"\n />\n </ControlledTooltip>\n )}\n </div>\n )}\n </div>\n\n <div>\n {loading ? (\n <SkeletonLoading type=\"circle\" className=\"ml-3\" />\n ) : (\n <div className=\"flex items-center space-x-2\">\n {actionHref && (\n <NavigationLink to={actionHref}>\n <ControlledButton\n defaultHtmlButton\n onClick={onAction}\n className=\"flex size-9 items-center justify-center rounded-full transition-colors hover:bg-muted\"\n >\n <MaterialSymbol color=\"rgb(var(--color-primary))\" icon={actionIcon} />\n </ControlledButton>\n </NavigationLink>\n )}\n\n {onAction && (\n <ControlledButton\n defaultHtmlButton\n onClick={onAction}\n className=\"flex size-9 items-center justify-center rounded-full transition-colors hover:bg-muted\"\n >\n <MaterialSymbol color=\"rgb(var(--color-primary))\" icon={actionIcon} />\n </ControlledButton>\n )}\n\n {onSecondarySlot && <>{onSecondarySlot}</>}\n\n {cardIcon && <MaterialSymbol icon={cardIcon} size={25} color=\"rgb(var(--color-primary))\" />}\n </div>\n )}\n </div>\n </div>\n\n <div\n className={classNames('m-auto', {\n 'w-[90%]': useScroll,\n 'w-full': !useScroll,\n })}\n >\n {children}\n </div>\n </div>\n\n {showActionButton && actionButtonLabel && (\n <div className=\"flex h-20 w-full items-center justify-center rounded-md bg-gradient-to-t from-surface to-transparent\">\n <ControlledButton\n className=\"flex w-full max-w-full items-center justify-center gap-x-3 rounded-full px-4 py-2 text-sm font-semibold capitalize text-white tablet:w-60\"\n onClick={handleActionClick}\n >\n <MaterialSymbol\n icon=\"arrow_downward\"\n size={20}\n color=\"white\"\n className={classNames('transition-transform duration-slow', {\n 'rotate-180': isArrowUpward,\n })}\n />\n\n {actionButtonLabel}\n </ControlledButton>\n </div>\n )}\n </CustomCard>\n );\n};\n\nexport default BaseDataCard;\n"],"names":[],"mappings":"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"}
@@ -27,7 +27,7 @@ import "../NoDataFound/index.js";
27
27
  import "../../Search/filters/FiltersButton.js";
28
28
  import "../../../helpers/utilityHelpers.js";
29
29
  import "../../Form/ControlledDialog.js";
30
- import "react-router-dom";
30
+ import "../../Navigation/UiNavigationContext.js";
31
31
  import { useBreakpoint } from "../../../hooks/useBreakpoint.js";
32
32
  import GridProvider from "./grid/GridProvider.js";
33
33
  import { useGridFillApply } from "./grid/useGridFillApply.js";
@@ -24,7 +24,7 @@ import NoDataFound from "../NoDataFound/index.js";
24
24
  import "../../Search/filters/FiltersButton.js";
25
25
  import "../../../helpers/utilityHelpers.js";
26
26
  import "../ControlledPopover.js";
27
- import "react-router-dom";
27
+ import "../../Navigation/UiNavigationContext.js";
28
28
  import UnifiedSearchBar from "../../Search/UnifiedSearchBar.js";
29
29
  const resolveDefaultRowLabel = (row, columns) => {
30
30
  const label = (columns ?? []).filter((column) => column.type !== "actions" && !!column.accessor).map((column) => column.accessor?.(row)).find((value) => typeof value === "string" || typeof value === "number");
@@ -24,7 +24,7 @@ import "../NoDataFound/index.js";
24
24
  import "../../Search/filters/FiltersButton.js";
25
25
  import "../../../helpers/utilityHelpers.js";
26
26
  import FiltersPanel from "../../Search/filters/FiltersPanel.js";
27
- import "react-router-dom";
27
+ import "../../Navigation/UiNavigationContext.js";
28
28
  const DataTableFilters = ({
29
29
  filters,
30
30
  values,
@@ -26,7 +26,7 @@ import SkeletonLoading from "../SkeletonLoading.js";
26
26
  import "../../Search/filters/FiltersButton.js";
27
27
  import "../../../helpers/utilityHelpers.js";
28
28
  import "../../Form/ControlledDialog.js";
29
- import "react-router-dom";
29
+ import "../../Navigation/UiNavigationContext.js";
30
30
  import UnifiedSearchBar from "../../Search/UnifiedSearchBar.js";
31
31
  import DataTableSegmentedTabs from "./DataTableSegmentedTabs.js";
32
32
  const DataTableToolbar = ({
@@ -25,7 +25,7 @@ import "../NoDataFound/index.js";
25
25
  import "../../Search/filters/FiltersButton.js";
26
26
  import "../../../helpers/utilityHelpers.js";
27
27
  import "../../Form/ControlledDialog.js";
28
- import "react-router-dom";
28
+ import "../../Navigation/UiNavigationContext.js";
29
29
  const isFieldActive = (field, values) => !isFilterValueEmpty(field, values?.[field.key]);
30
30
  const applyDataTableFilters = (rows, filters, values) => {
31
31
  if (!filters) return rows;
@@ -0,0 +1,48 @@
1
+ export interface IUserMenuAccount {
2
+ id: string;
3
+ name: string;
4
+ logoUrl?: string;
5
+ }
6
+ export interface IUserMenuItem {
7
+ key: string;
8
+ label: string;
9
+ /** Internal route, rendered through the navigation port's Link. */
10
+ to?: string;
11
+ onClick?: () => void;
12
+ tone?: 'default' | 'danger';
13
+ }
14
+ interface IUserMenuProps {
15
+ userName?: string;
16
+ userEmail?: string;
17
+ /** The active account/tenant shown in the trigger, beside the user identity. */
18
+ accountName?: string;
19
+ accountLogoUrl?: string;
20
+ /** Role chip in the trigger; omitted when empty. */
21
+ roleLabel?: string;
22
+ /** Account switcher list; omit (undefined) to hide the whole section. */
23
+ accounts?: Array<IUserMenuAccount>;
24
+ activeAccountId?: string;
25
+ /** Account being switched to; disables the list and shows its spinner. */
26
+ switchingAccountId?: string | null;
27
+ loadingAccounts?: boolean;
28
+ onSwitchAccount?: (accountId: string) => void;
29
+ /** Fired when the popover opens/closes, e.g. to lazily fetch the accounts. */
30
+ onOpenChange?: (open: boolean) => void;
31
+ open: boolean;
32
+ /** Extra rows in the footer (profile, settings...), before the logout row. */
33
+ menuItems?: Array<IUserMenuItem>;
34
+ onLogout?: () => void;
35
+ logoutLabel?: string;
36
+ /** Renders only the identity summary, with no popover (e.g. admin mode). */
37
+ readOnly?: boolean;
38
+ className?: string;
39
+ }
40
+ /**
41
+ * The uMode account menu: identity summary as the trigger, account switcher (with search past
42
+ * five entries), custom rows and logout in a popover. Everything app-shaped (who the user is,
43
+ * what switching does, where profile lives) arrives as props, so any host renders the same
44
+ * surface the dashboard does.
45
+ */
46
+ declare const UserMenu: React.FC<IUserMenuProps>;
47
+ export default UserMenu;
48
+ export type { IUserMenuProps };
@@ -0,0 +1,196 @@
1
+ import { jsx, jsxs, Fragment } from "react/jsx-runtime";
2
+ import { useState, useEffect } from "react";
3
+ import classNames from "classnames";
4
+ import { useTranslation } from "react-i18next";
5
+ import ControlledButton from "../Form/ControlledButton.js";
6
+ import ControlledImage from "../Form/ControlledImage/index.js";
7
+ import { useUiNavigation } from "../Navigation/UiNavigationContext.js";
8
+ import UnifiedSearchBar from "../Search/UnifiedSearchBar.js";
9
+ import OutlinedChip from "./Chips/OutlinedChip.js";
10
+ import { ControlledPopoverRoot, ControlledPopoverHandler, ControlledPopoverContent } from "./ControlledPopover.js";
11
+ import LoadingSpinner from "./LoadingSpinner.js";
12
+ import { MaterialSymbol } from "./MaterialSymbol/index.js";
13
+ import NoDataFound from "./NoDataFound/index.js";
14
+ import { getUserAvatarLetters } from "../../helpers/stringHelpers.js";
15
+ const SEARCH_THRESHOLD = 5;
16
+ const AccountRow = ({ account, isActive, switchingAccountId, onSwitchAccount }) => /* @__PURE__ */ jsxs(
17
+ ControlledButton,
18
+ {
19
+ defaultHtmlButton: true,
20
+ type: "button",
21
+ disabled: !!switchingAccountId,
22
+ onClick: () => onSwitchAccount?.(account.id),
23
+ className: classNames("flex w-full items-center gap-x-3 rounded-lg px-2 py-2.5 transition-colors", {
24
+ "bg-brand-soft": isActive,
25
+ "hover:bg-muted": !isActive,
26
+ "disabled:cursor-not-allowed disabled:opacity-60": !!switchingAccountId
27
+ }),
28
+ children: [
29
+ account.logoUrl ? /* @__PURE__ */ jsx(
30
+ ControlledImage,
31
+ {
32
+ src: account.logoUrl,
33
+ className: "size-8 shrink-0 rounded-full border border-brand-border bg-surface object-cover"
34
+ }
35
+ ) : /* @__PURE__ */ jsx("div", { className: "flex size-8 shrink-0 items-center justify-center rounded-full bg-brand-hover text-xs font-bold text-brand-fg", children: getUserAvatarLetters(account.name) }),
36
+ /* @__PURE__ */ jsx("div", { className: "flex min-w-0 flex-1 flex-col items-start", children: /* @__PURE__ */ jsx("span", { className: "w-full truncate text-left text-sm text-foreground", children: account.name }) }),
37
+ switchingAccountId === account.id && /* @__PURE__ */ jsx(LoadingSpinner, { size: 4, color: "foreground-faint" }),
38
+ isActive && !switchingAccountId && /* @__PURE__ */ jsx(MaterialSymbol, { icon: "check", size: 18, color: "rgb(var(--color-primary))" })
39
+ ]
40
+ }
41
+ );
42
+ const UserMenu = ({
43
+ userName,
44
+ userEmail,
45
+ accountName,
46
+ accountLogoUrl,
47
+ roleLabel,
48
+ accounts,
49
+ activeAccountId,
50
+ switchingAccountId,
51
+ loadingAccounts = false,
52
+ onSwitchAccount,
53
+ onOpenChange,
54
+ open,
55
+ menuItems,
56
+ onLogout,
57
+ logoutLabel,
58
+ readOnly = false,
59
+ className
60
+ }) => {
61
+ const { t } = useTranslation("ui");
62
+ const { Link: NavigationLink } = useUiNavigation();
63
+ const userInitials = userName ? getUserAvatarLetters(userName) : "??";
64
+ const showAccounts = Array.isArray(accounts);
65
+ const safeAccounts = accounts ?? [];
66
+ const [filtered, setFiltered] = useState(safeAccounts);
67
+ useEffect(() => {
68
+ setFiltered(accounts ?? []);
69
+ }, [accounts, open]);
70
+ const accountSummary = /* @__PURE__ */ jsxs(Fragment, { children: [
71
+ /* @__PURE__ */ jsx("div", { className: "flex size-10 shrink-0 items-center justify-center rounded-full bg-gradient-to-tl from-brand to-brand", children: accountLogoUrl && /* @__PURE__ */ jsx(ControlledImage, { src: accountLogoUrl, className: "size-9 rounded-full" }) }),
72
+ /* @__PURE__ */ jsxs("div", { className: "hidden min-w-0 flex-1 flex-col items-start gap-y-0.5 tablet:flex", children: [
73
+ /* @__PURE__ */ jsx("span", { className: "w-full truncate text-left text-sm font-semibold text-foreground", children: userName }),
74
+ /* @__PURE__ */ jsxs("div", { className: "flex w-full items-center justify-between gap-x-2", children: [
75
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-left text-xs text-foreground-subtle", children: accountName }),
76
+ roleLabel && /* @__PURE__ */ jsx(
77
+ OutlinedChip,
78
+ {
79
+ label: roleLabel,
80
+ chipIcon: "verified_user",
81
+ truncate: true,
82
+ tooltip: roleLabel,
83
+ className: "max-w-[15rem] shrink-0 whitespace-nowrap border border-brand-border bg-brand-soft text-brand-emphasis"
84
+ }
85
+ )
86
+ ] })
87
+ ] })
88
+ ] });
89
+ if (readOnly) {
90
+ return /* @__PURE__ */ jsx("div", { className: classNames("flex min-w-0 items-center gap-x-2.5 px-2.5 py-1.5 tablet:min-w-[300px]", className), children: accountSummary });
91
+ }
92
+ const footerRow = (item) => {
93
+ const rowClass = classNames(
94
+ "rounded-lg px-2 py-2.5 text-left text-sm transition-colors",
95
+ item.tone === "danger" ? "text-danger-emphasis hover:bg-danger-soft" : "text-foreground-muted hover:bg-muted"
96
+ );
97
+ if (item.to) {
98
+ return /* @__PURE__ */ jsx(NavigationLink, { to: item.to, onClick: () => onOpenChange?.(false), className: rowClass, children: item.label }, item.key);
99
+ }
100
+ return /* @__PURE__ */ jsx(
101
+ ControlledButton,
102
+ {
103
+ defaultHtmlButton: true,
104
+ type: "button",
105
+ onClick: () => {
106
+ onOpenChange?.(false);
107
+ item.onClick?.();
108
+ },
109
+ className: classNames("cursor-pointer", rowClass),
110
+ children: item.label
111
+ },
112
+ item.key
113
+ );
114
+ };
115
+ return /* @__PURE__ */ jsxs(ControlledPopoverRoot, { placement: "bottom-end", open, handler: (next) => onOpenChange?.(next), children: [
116
+ /* @__PURE__ */ jsx(ControlledPopoverHandler, { children: /* @__PURE__ */ jsxs(
117
+ ControlledButton,
118
+ {
119
+ defaultHtmlButton: true,
120
+ type: "button",
121
+ className: classNames(
122
+ "flex min-w-0 cursor-pointer items-center gap-x-2.5 px-2.5 py-1.5 tablet:min-w-[300px]",
123
+ className
124
+ ),
125
+ children: [
126
+ accountSummary,
127
+ /* @__PURE__ */ jsx(
128
+ MaterialSymbol,
129
+ {
130
+ icon: open ? "expand_less" : "expand_more",
131
+ size: 20,
132
+ color: "rgb(var(--color-foreground-subtle))"
133
+ }
134
+ )
135
+ ]
136
+ }
137
+ ) }),
138
+ /* @__PURE__ */ jsxs(ControlledPopoverContent, { className: "z-[9999] w-72 max-w-[calc(100vw-1.5rem)] rounded-xl border border-border p-0 shadow-xl", children: [
139
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-x-3 px-5 pb-4 pt-5", children: [
140
+ /* @__PURE__ */ jsx("div", { className: "flex size-11 shrink-0 items-center justify-center rounded-full bg-gradient-to-tl from-brand to-brand text-sm font-bold text-white", children: userInitials }),
141
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col overflow-hidden", children: [
142
+ /* @__PURE__ */ jsx("span", { className: "truncate text-sm font-semibold text-foreground", children: userName || t("userMenu.loading") }),
143
+ /* @__PURE__ */ jsx("span", { className: "truncate text-xs text-foreground-faint", children: userEmail || "" })
144
+ ] })
145
+ ] }),
146
+ /* @__PURE__ */ jsx("hr", { className: "border-border-subtle" }),
147
+ showAccounts && /* @__PURE__ */ jsxs(Fragment, { children: [
148
+ /* @__PURE__ */ jsx("div", { className: "px-5 pb-2 pt-4", children: /* @__PURE__ */ jsx("span", { className: "text-xxs font-semibold uppercase tracking-wider text-foreground-faint", children: t("userMenu.yourAccounts") }) }),
149
+ safeAccounts.length > SEARCH_THRESHOLD && /* @__PURE__ */ jsx("div", { className: "px-3 pb-2", children: /* @__PURE__ */ jsx(
150
+ UnifiedSearchBar,
151
+ {
152
+ mode: "default",
153
+ searchableItems: safeAccounts,
154
+ searchKeys: ["name"],
155
+ setFilteredItems: setFiltered,
156
+ label: t("userMenu.searchAccount"),
157
+ debounceTime: 300,
158
+ minSearchLength: 1,
159
+ containerClassName: "relative flex gap-x-2"
160
+ }
161
+ ) }),
162
+ /* @__PURE__ */ jsx("div", { className: "max-h-48 overflow-y-auto px-3 pb-2", children: loadingAccounts ? /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center py-4", children: /* @__PURE__ */ jsx(LoadingSpinner, { size: 5, color: "foreground-faint" }) }) : filtered.length === 0 ? /* @__PURE__ */ jsx(NoDataFound, { compact: true, message: t("userMenu.noAccounts") }) : filtered.map((account) => /* @__PURE__ */ jsx(
163
+ AccountRow,
164
+ {
165
+ account,
166
+ isActive: account.id === activeAccountId,
167
+ switchingAccountId,
168
+ onSwitchAccount
169
+ },
170
+ account.id
171
+ )) }),
172
+ /* @__PURE__ */ jsx("hr", { className: "border-border-subtle" })
173
+ ] }),
174
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col px-3 py-2", children: [
175
+ (menuItems ?? []).map(footerRow),
176
+ onLogout && /* @__PURE__ */ jsx(
177
+ ControlledButton,
178
+ {
179
+ defaultHtmlButton: true,
180
+ type: "button",
181
+ onClick: () => {
182
+ onOpenChange?.(false);
183
+ onLogout();
184
+ },
185
+ className: "cursor-pointer rounded-lg px-2 py-2.5 text-left text-sm text-danger-emphasis transition-colors hover:bg-danger-soft",
186
+ children: logoutLabel || t("userMenu.logout")
187
+ }
188
+ )
189
+ ] })
190
+ ] })
191
+ ] });
192
+ };
193
+ export {
194
+ UserMenu as default
195
+ };
196
+ //# sourceMappingURL=UserMenu.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"UserMenu.js","sources":["../../../src/components/UI/UserMenu.tsx"],"sourcesContent":["import { ReactNode, useEffect, useState } from 'react';\nimport classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\n\nimport ControlledButton from '@/components/Form/ControlledButton';\nimport ControlledImage from '@/components/Form/ControlledImage';\nimport { useUiNavigation } from '@/components/Navigation/UiNavigationContext';\nimport UnifiedSearchBar from '@/components/Search/UnifiedSearchBar';\nimport OutlinedChip from '@/components/UI/Chips/OutlinedChip';\nimport {\n ControlledPopoverContent,\n ControlledPopoverHandler,\n ControlledPopoverRoot,\n} from '@/components/UI/ControlledPopover';\nimport LoadingSpinner from '@/components/UI/LoadingSpinner';\nimport { MaterialSymbol } from '@/components/UI/MaterialSymbol';\nimport NoDataFound from '@/components/UI/NoDataFound';\nimport { getUserAvatarLetters } from '@/helpers/stringHelpers';\n\nconst SEARCH_THRESHOLD = 5;\n\nexport interface IUserMenuAccount {\n id: string;\n name: string;\n logoUrl?: string;\n}\n\nexport interface IUserMenuItem {\n key: string;\n label: string;\n /** Internal route, rendered through the navigation port's Link. */\n to?: string;\n onClick?: () => void;\n tone?: 'default' | 'danger';\n}\n\ninterface IUserMenuProps {\n userName?: string;\n userEmail?: string;\n /** The active account/tenant shown in the trigger, beside the user identity. */\n accountName?: string;\n accountLogoUrl?: string;\n /** Role chip in the trigger; omitted when empty. */\n roleLabel?: string;\n /** Account switcher list; omit (undefined) to hide the whole section. */\n accounts?: Array<IUserMenuAccount>;\n activeAccountId?: string;\n /** Account being switched to; disables the list and shows its spinner. */\n switchingAccountId?: string | null;\n loadingAccounts?: boolean;\n onSwitchAccount?: (accountId: string) => void;\n /** Fired when the popover opens/closes, e.g. to lazily fetch the accounts. */\n onOpenChange?: (open: boolean) => void;\n open: boolean;\n /** Extra rows in the footer (profile, settings...), before the logout row. */\n menuItems?: Array<IUserMenuItem>;\n onLogout?: () => void;\n logoutLabel?: string;\n /** Renders only the identity summary, with no popover (e.g. admin mode). */\n readOnly?: boolean;\n className?: string;\n}\n\nconst AccountRow: React.FC<{\n account: IUserMenuAccount;\n isActive: boolean;\n switchingAccountId?: string | null;\n onSwitchAccount?: (accountId: string) => void;\n}> = ({ account, isActive, switchingAccountId, onSwitchAccount }) => (\n <ControlledButton\n defaultHtmlButton\n type=\"button\"\n disabled={!!switchingAccountId}\n onClick={() => onSwitchAccount?.(account.id)}\n className={classNames('flex w-full items-center gap-x-3 rounded-lg px-2 py-2.5 transition-colors', {\n 'bg-brand-soft': isActive,\n 'hover:bg-muted': !isActive,\n 'disabled:cursor-not-allowed disabled:opacity-60': !!switchingAccountId,\n })}\n >\n {account.logoUrl ? (\n <ControlledImage\n src={account.logoUrl}\n className=\"size-8 shrink-0 rounded-full border border-brand-border bg-surface object-cover\"\n />\n ) : (\n <div className=\"flex size-8 shrink-0 items-center justify-center rounded-full bg-brand-hover text-xs font-bold text-brand-fg\">\n {getUserAvatarLetters(account.name)}\n </div>\n )}\n\n <div className=\"flex min-w-0 flex-1 flex-col items-start\">\n <span className=\"w-full truncate text-left text-sm text-foreground\">{account.name}</span>\n </div>\n\n {switchingAccountId === account.id && <LoadingSpinner size={4} color=\"foreground-faint\" />}\n {isActive && !switchingAccountId && <MaterialSymbol icon=\"check\" size={18} color=\"rgb(var(--color-primary))\" />}\n </ControlledButton>\n);\n\n/**\n * The uMode account menu: identity summary as the trigger, account switcher (with search past\n * five entries), custom rows and logout in a popover. Everything app-shaped (who the user is,\n * what switching does, where profile lives) arrives as props, so any host renders the same\n * surface the dashboard does.\n */\nconst UserMenu: React.FC<IUserMenuProps> = ({\n userName,\n userEmail,\n accountName,\n accountLogoUrl,\n roleLabel,\n accounts,\n activeAccountId,\n switchingAccountId,\n loadingAccounts = false,\n onSwitchAccount,\n onOpenChange,\n open,\n menuItems,\n onLogout,\n logoutLabel,\n readOnly = false,\n className,\n}) => {\n const { t } = useTranslation('ui');\n const { Link: NavigationLink } = useUiNavigation();\n\n const userInitials = userName ? getUserAvatarLetters(userName) : '??';\n const showAccounts = Array.isArray(accounts);\n const safeAccounts = accounts ?? [];\n\n const [filtered, setFiltered] = useState<Array<IUserMenuAccount>>(safeAccounts);\n\n useEffect(() => {\n setFiltered(accounts ?? []);\n }, [accounts, open]);\n\n const accountSummary = (\n <>\n <div className=\"flex size-10 shrink-0 items-center justify-center rounded-full bg-gradient-to-tl from-brand to-brand\">\n {accountLogoUrl && <ControlledImage src={accountLogoUrl} className=\"size-9 rounded-full\" />}\n </div>\n {/* Below the tablet edge the header only has room for the avatar; the same identity is\n one tap away inside the popover, so nothing is lost. */}\n <div className=\"hidden min-w-0 flex-1 flex-col items-start gap-y-0.5 tablet:flex\">\n <span className=\"w-full truncate text-left text-sm font-semibold text-foreground\">{userName}</span>\n <div className=\"flex w-full items-center justify-between gap-x-2\">\n <span className=\"min-w-0 flex-1 truncate text-left text-xs text-foreground-subtle\">{accountName}</span>\n {roleLabel && (\n <OutlinedChip\n label={roleLabel}\n chipIcon=\"verified_user\"\n truncate\n tooltip={roleLabel}\n className=\"max-w-[15rem] shrink-0 whitespace-nowrap border border-brand-border bg-brand-soft text-brand-emphasis\"\n />\n )}\n </div>\n </div>\n </>\n );\n\n if (readOnly) {\n return (\n <div className={classNames('flex min-w-0 items-center gap-x-2.5 px-2.5 py-1.5 tablet:min-w-[300px]', className)}>\n {accountSummary}\n </div>\n );\n }\n\n const footerRow = (item: IUserMenuItem): ReactNode => {\n const rowClass = classNames(\n 'rounded-lg px-2 py-2.5 text-left text-sm transition-colors',\n item.tone === 'danger' ? 'text-danger-emphasis hover:bg-danger-soft' : 'text-foreground-muted hover:bg-muted',\n );\n\n if (item.to) {\n return (\n <NavigationLink key={item.key} to={item.to} onClick={() => onOpenChange?.(false)} className={rowClass}>\n {item.label}\n </NavigationLink>\n );\n }\n return (\n <ControlledButton\n defaultHtmlButton\n key={item.key}\n type=\"button\"\n onClick={() => {\n onOpenChange?.(false);\n item.onClick?.();\n }}\n className={classNames('cursor-pointer', rowClass)}\n >\n {item.label}\n </ControlledButton>\n );\n };\n\n return (\n <ControlledPopoverRoot placement=\"bottom-end\" open={open} handler={(next) => onOpenChange?.(next)}>\n <ControlledPopoverHandler>\n <ControlledButton\n defaultHtmlButton\n type=\"button\"\n className={classNames(\n 'flex min-w-0 cursor-pointer items-center gap-x-2.5 px-2.5 py-1.5 tablet:min-w-[300px]',\n className,\n )}\n >\n {accountSummary}\n <MaterialSymbol\n icon={open ? 'expand_less' : 'expand_more'}\n size={20}\n color=\"rgb(var(--color-foreground-subtle))\"\n />\n </ControlledButton>\n </ControlledPopoverHandler>\n\n <ControlledPopoverContent className=\"z-[9999] w-72 max-w-[calc(100vw-1.5rem)] rounded-xl border border-border p-0 shadow-xl\">\n <div className=\"flex items-center gap-x-3 px-5 pb-4 pt-5\">\n <div className=\"flex size-11 shrink-0 items-center justify-center rounded-full bg-gradient-to-tl from-brand to-brand text-sm font-bold text-white\">\n {userInitials}\n </div>\n <div className=\"flex min-w-0 flex-1 flex-col overflow-hidden\">\n <span className=\"truncate text-sm font-semibold text-foreground\">{userName || t('userMenu.loading')}</span>\n <span className=\"truncate text-xs text-foreground-faint\">{userEmail || ''}</span>\n </div>\n </div>\n\n <hr className=\"border-border-subtle\" />\n\n {showAccounts && (\n <>\n <div className=\"px-5 pb-2 pt-4\">\n <span className=\"text-xxs font-semibold uppercase tracking-wider text-foreground-faint\">\n {t('userMenu.yourAccounts')}\n </span>\n </div>\n\n {safeAccounts.length > SEARCH_THRESHOLD && (\n <div className=\"px-3 pb-2\">\n <UnifiedSearchBar\n mode=\"default\"\n searchableItems={safeAccounts}\n searchKeys={['name']}\n setFilteredItems={setFiltered}\n label={t('userMenu.searchAccount')}\n debounceTime={300}\n minSearchLength={1}\n containerClassName=\"relative flex gap-x-2\"\n />\n </div>\n )}\n\n <div className=\"max-h-48 overflow-y-auto px-3 pb-2\">\n {loadingAccounts ? (\n <div className=\"flex items-center justify-center py-4\">\n <LoadingSpinner size={5} color=\"foreground-faint\" />\n </div>\n ) : filtered.length === 0 ? (\n <NoDataFound compact message={t('userMenu.noAccounts')} />\n ) : (\n filtered.map((account) => (\n <AccountRow\n key={account.id}\n account={account}\n isActive={account.id === activeAccountId}\n switchingAccountId={switchingAccountId}\n onSwitchAccount={onSwitchAccount}\n />\n ))\n )}\n </div>\n\n <hr className=\"border-border-subtle\" />\n </>\n )}\n\n <div className=\"flex flex-col px-3 py-2\">\n {(menuItems ?? []).map(footerRow)}\n\n {onLogout && (\n <ControlledButton\n defaultHtmlButton\n type=\"button\"\n onClick={() => {\n onOpenChange?.(false);\n onLogout();\n }}\n className=\"cursor-pointer rounded-lg px-2 py-2.5 text-left text-sm text-danger-emphasis transition-colors hover:bg-danger-soft\"\n >\n {logoutLabel || t('userMenu.logout')}\n </ControlledButton>\n )}\n </div>\n </ControlledPopoverContent>\n </ControlledPopoverRoot>\n );\n};\n\nexport default UserMenu;\nexport type { IUserMenuProps };\n"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -14,6 +14,7 @@ export { default as ControlledBreadcrumb } from './components/Navigation/Control
14
14
  export { default as ErrorPage } from './components/Navigation/ErrorPage';
15
15
  export { default as PageTitle } from './components/Navigation/PageTitle';
16
16
  export * from './components/Navigation/PageTitleNavigationContext';
17
+ export * from './components/Navigation/UiNavigationContext';
17
18
  export { default as UnifiedSearchBar } from './components/Search/UnifiedSearchBar';
18
19
  export * from './components/UI/access/UiAccessContext';
19
20
  export { default as BaseDataCard } from './components/UI/BaseDataCard';
@@ -34,6 +35,7 @@ export { default as LoadingSpinner } from './components/UI/LoadingSpinner';
34
35
  export * from './components/UI/MaterialSymbol/index';
35
36
  export { default as NoDataFound } from './components/UI/NoDataFound/index';
36
37
  export { default as SkeletonLoading } from './components/UI/SkeletonLoading';
38
+ export { default as UserMenu } from './components/UI/UserMenu';
37
39
  export * from './helpers/composeRefs';
38
40
  export * from './helpers/layeredSurfaces';
39
41
  export * from './helpers/themeTokens';