@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.
- package/README.md +78 -0
- package/dist/assets/umode-ui.css +148 -0
- package/dist/components/Form/ControlledSelect.js +1 -1
- package/dist/components/Form/ControlledTabs.js +4 -4
- package/dist/components/Form/ControlledTabs.js.map +1 -1
- package/dist/components/Form/ControlledViewToggle.js +2 -2
- package/dist/components/Form/ControlledViewToggle.js.map +1 -1
- package/dist/components/Navigation/ControlledBreadcrumb.js +3 -2
- package/dist/components/Navigation/ControlledBreadcrumb.js.map +1 -1
- package/dist/components/Navigation/PageTitle.js +3 -6
- package/dist/components/Navigation/PageTitle.js.map +1 -1
- package/dist/components/Navigation/ReactRouterNavigationAdapter.d.ts +7 -0
- package/dist/components/Navigation/ReactRouterNavigationAdapter.js +26 -0
- package/dist/components/Navigation/ReactRouterNavigationAdapter.js.map +1 -0
- package/dist/components/Navigation/UiNavigationContext.d.ts +38 -0
- package/dist/components/Navigation/UiNavigationContext.js +43 -0
- package/dist/components/Navigation/UiNavigationContext.js.map +1 -0
- package/dist/components/Search/filters/useFilters.js +2 -2
- package/dist/components/Search/filters/useFilters.js.map +1 -1
- package/dist/components/Shell/AppHeader.d.ts +19 -0
- package/dist/components/Shell/AppHeader.js +64 -0
- package/dist/components/Shell/AppHeader.js.map +1 -0
- package/dist/components/Shell/AppShell.d.ts +20 -0
- package/dist/components/Shell/AppShell.js +100 -0
- package/dist/components/Shell/AppShell.js.map +1 -0
- package/dist/components/Shell/AppSidebar.d.ts +15 -0
- package/dist/components/Shell/AppSidebar.js +63 -0
- package/dist/components/Shell/AppSidebar.js.map +1 -0
- package/dist/components/Shell/LanguageToggle.d.ts +8 -0
- package/dist/components/Shell/LanguageToggle.js +59 -0
- package/dist/components/Shell/LanguageToggle.js.map +1 -0
- package/dist/components/Shell/SidebarMenu.d.ts +24 -0
- package/dist/components/Shell/SidebarMenu.js +204 -0
- package/dist/components/Shell/SidebarMenu.js.map +1 -0
- package/dist/components/Shell/ThemeToggle.d.ts +11 -0
- package/dist/components/Shell/ThemeToggle.js +55 -0
- package/dist/components/Shell/ThemeToggle.js.map +1 -0
- package/dist/components/Shell/headerControlStyles.d.ts +10 -0
- package/dist/components/Shell/headerControlStyles.js +11 -0
- package/dist/components/Shell/headerControlStyles.js.map +1 -0
- package/dist/components/Shell/useSidebarState.d.ts +14 -0
- package/dist/components/Shell/useSidebarState.js +106 -0
- package/dist/components/Shell/useSidebarState.js.map +1 -0
- package/dist/components/UI/BaseDataCard.js +3 -2
- package/dist/components/UI/BaseDataCard.js.map +1 -1
- package/dist/components/UI/DataTable/DataTable.js +1 -1
- package/dist/components/UI/DataTable/DataTableBulkEditDialog.js +1 -1
- package/dist/components/UI/DataTable/DataTableFilters.js +1 -1
- package/dist/components/UI/DataTable/DataTableToolbar.js +1 -1
- package/dist/components/UI/DataTable/applyDataTableFilters.js +1 -1
- package/dist/components/UI/UserMenu.d.ts +48 -0
- package/dist/components/UI/UserMenu.js +196 -0
- package/dist/components/UI/UserMenu.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +47 -34
- package/dist/index.js.map +1 -1
- package/dist/locales/en/ui.json.d.ts +28 -0
- package/dist/locales/en/ui.json.js +12 -0
- package/dist/locales/en/ui.json.js.map +1 -1
- package/dist/locales/index.d.ts +56 -0
- package/dist/locales/pt/ui.json.d.ts +28 -0
- package/dist/locales/pt/ui.json.js +12 -0
- package/dist/locales/pt/ui.json.js.map +1 -1
- package/package.json +6 -3
- package/src/assets/styles/base.css +0 -1
|
@@ -0,0 +1,59 @@
|
|
|
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 ControlledTooltip from "../UI/ControlledTooltip.js";
|
|
6
|
+
import { MaterialSymbol } from "../UI/MaterialSymbol/index.js";
|
|
7
|
+
import { HEADER_CONTROL_SIZE_REM, HEADER_CONTROL_SHELL_CLASS } from "./headerControlStyles.js";
|
|
8
|
+
const OPTION_WIDTH_REM = 2.25;
|
|
9
|
+
const LanguageToggle = ({ language, onChange }) => {
|
|
10
|
+
const { t } = useTranslation("ui");
|
|
11
|
+
const isPT = language === "pt";
|
|
12
|
+
const optionClass = (isActive) => classNames(
|
|
13
|
+
"relative z-10 flex shrink-0 items-center justify-center rounded-full text-[0.6875rem] font-semibold leading-none tracking-wide transition-colors duration-base",
|
|
14
|
+
isActive ? "text-brand-emphasis" : "text-foreground-faint hover:text-foreground-muted"
|
|
15
|
+
);
|
|
16
|
+
return /* @__PURE__ */ jsx(ControlledTooltip, { content: t("language.toggle"), children: /* @__PURE__ */ jsxs("div", { className: classNames(HEADER_CONTROL_SHELL_CLASS, "gap-x-1.5 pl-2.5"), children: [
|
|
17
|
+
/* @__PURE__ */ jsx(MaterialSymbol, { icon: "language", size: 15, className: "shrink-0 text-foreground-faint" }),
|
|
18
|
+
/* @__PURE__ */ jsxs("div", { className: "relative flex items-center", children: [
|
|
19
|
+
/* @__PURE__ */ jsx(
|
|
20
|
+
"div",
|
|
21
|
+
{
|
|
22
|
+
"aria-hidden": true,
|
|
23
|
+
className: "pointer-events-none absolute left-0 top-0 rounded-full bg-surface shadow-elevation1 ring-1 ring-border transition-transform duration-base ease-enter",
|
|
24
|
+
style: {
|
|
25
|
+
width: `${OPTION_WIDTH_REM}rem`,
|
|
26
|
+
height: `${HEADER_CONTROL_SIZE_REM}rem`,
|
|
27
|
+
transform: `translateX(${isPT ? 0 : OPTION_WIDTH_REM}rem)`
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
),
|
|
31
|
+
/* @__PURE__ */ jsx(
|
|
32
|
+
ControlledButton,
|
|
33
|
+
{
|
|
34
|
+
defaultHtmlButton: true,
|
|
35
|
+
"aria-pressed": isPT,
|
|
36
|
+
onClick: () => onChange("pt"),
|
|
37
|
+
className: optionClass(isPT),
|
|
38
|
+
style: { width: `${OPTION_WIDTH_REM}rem`, height: `${HEADER_CONTROL_SIZE_REM}rem` },
|
|
39
|
+
children: t("language.pt_short")
|
|
40
|
+
}
|
|
41
|
+
),
|
|
42
|
+
/* @__PURE__ */ jsx(
|
|
43
|
+
ControlledButton,
|
|
44
|
+
{
|
|
45
|
+
defaultHtmlButton: true,
|
|
46
|
+
"aria-pressed": !isPT,
|
|
47
|
+
onClick: () => onChange("en"),
|
|
48
|
+
className: optionClass(!isPT),
|
|
49
|
+
style: { width: `${OPTION_WIDTH_REM}rem`, height: `${HEADER_CONTROL_SIZE_REM}rem` },
|
|
50
|
+
children: t("language.en_short")
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
] })
|
|
54
|
+
] }) });
|
|
55
|
+
};
|
|
56
|
+
export {
|
|
57
|
+
LanguageToggle as default
|
|
58
|
+
};
|
|
59
|
+
//# sourceMappingURL=LanguageToggle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LanguageToggle.js","sources":["../../../src/components/Shell/LanguageToggle.tsx"],"sourcesContent":["import classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\n\nimport ControlledButton from '@/components/Form/ControlledButton';\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { MaterialSymbol } from '@/components/UI/MaterialSymbol';\n\nimport { HEADER_CONTROL_SHELL_CLASS, HEADER_CONTROL_SIZE_REM } from './headerControlStyles';\n\nconst OPTION_WIDTH_REM = 2.25;\n\nexport type TUiLanguage = 'pt' | 'en';\n\ninterface ILanguageToggleProps {\n language: TUiLanguage;\n onChange: (language: TUiLanguage) => void;\n}\n\n/** PT/EN knob; the host owns the i18n instance and persistence. */\nconst LanguageToggle: React.FC<ILanguageToggleProps> = ({ language, onChange }) => {\n const { t } = useTranslation('ui');\n\n const isPT = language === 'pt';\n\n const optionClass = (isActive: boolean) =>\n classNames(\n 'relative z-10 flex shrink-0 items-center justify-center rounded-full text-[0.6875rem] font-semibold leading-none tracking-wide transition-colors duration-base',\n isActive ? 'text-brand-emphasis' : 'text-foreground-faint hover:text-foreground-muted',\n );\n\n return (\n <ControlledTooltip content={t('language.toggle')}>\n <div className={classNames(HEADER_CONTROL_SHELL_CLASS, 'gap-x-1.5 pl-2.5')}>\n <MaterialSymbol icon=\"language\" size={15} className=\"shrink-0 text-foreground-faint\" />\n\n <div className=\"relative flex items-center\">\n <div\n aria-hidden\n className=\"pointer-events-none absolute left-0 top-0 rounded-full bg-surface shadow-elevation1 ring-1 ring-border transition-transform duration-base ease-enter\"\n style={{\n width: `${OPTION_WIDTH_REM}rem`,\n height: `${HEADER_CONTROL_SIZE_REM}rem`,\n transform: `translateX(${isPT ? 0 : OPTION_WIDTH_REM}rem)`,\n }}\n />\n\n <ControlledButton\n defaultHtmlButton\n aria-pressed={isPT}\n onClick={() => onChange('pt')}\n className={optionClass(isPT)}\n style={{ width: `${OPTION_WIDTH_REM}rem`, height: `${HEADER_CONTROL_SIZE_REM}rem` }}\n >\n {t('language.pt_short')}\n </ControlledButton>\n\n <ControlledButton\n defaultHtmlButton\n aria-pressed={!isPT}\n onClick={() => onChange('en')}\n className={optionClass(!isPT)}\n style={{ width: `${OPTION_WIDTH_REM}rem`, height: `${HEADER_CONTROL_SIZE_REM}rem` }}\n >\n {t('language.en_short')}\n </ControlledButton>\n </div>\n </div>\n </ControlledTooltip>\n );\n};\n\nexport default LanguageToggle;\n"],"names":[],"mappings":";;;;;;;AASA,MAAM,mBAAmB;AAUzB,MAAM,iBAAiD,CAAC,EAAE,UAAU,eAAe;AACjF,QAAM,EAAE,EAAA,IAAM,eAAe,IAAI;AAEjC,QAAM,OAAO,aAAa;AAE1B,QAAM,cAAc,CAAC,aACnB;AAAA,IACE;AAAA,IACA,WAAW,wBAAwB;AAAA,EAAA;AAGvC,SACE,oBAAC,mBAAA,EAAkB,SAAS,EAAE,iBAAiB,GAC7C,UAAA,qBAAC,OAAA,EAAI,WAAW,WAAW,4BAA4B,kBAAkB,GACvE,UAAA;AAAA,IAAA,oBAAC,kBAAe,MAAK,YAAW,MAAM,IAAI,WAAU,kCAAiC;AAAA,IAErF,qBAAC,OAAA,EAAI,WAAU,8BACb,UAAA;AAAA,MAAA;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,eAAW;AAAA,UACX,WAAU;AAAA,UACV,OAAO;AAAA,YACL,OAAO,GAAG,gBAAgB;AAAA,YAC1B,QAAQ,GAAG,uBAAuB;AAAA,YAClC,WAAW,cAAc,OAAO,IAAI,gBAAgB;AAAA,UAAA;AAAA,QACtD;AAAA,MAAA;AAAA,MAGF;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,mBAAiB;AAAA,UACjB,gBAAc;AAAA,UACd,SAAS,MAAM,SAAS,IAAI;AAAA,UAC5B,WAAW,YAAY,IAAI;AAAA,UAC3B,OAAO,EAAE,OAAO,GAAG,gBAAgB,OAAO,QAAQ,GAAG,uBAAuB,MAAA;AAAA,UAE3E,YAAE,mBAAmB;AAAA,QAAA;AAAA,MAAA;AAAA,MAGxB;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,mBAAiB;AAAA,UACjB,gBAAc,CAAC;AAAA,UACf,SAAS,MAAM,SAAS,IAAI;AAAA,UAC5B,WAAW,YAAY,CAAC,IAAI;AAAA,UAC5B,OAAO,EAAE,OAAO,GAAG,gBAAgB,OAAO,QAAQ,GAAG,uBAAuB,MAAA;AAAA,UAE3E,YAAE,mBAAmB;AAAA,QAAA;AAAA,MAAA;AAAA,IACxB,EAAA,CACF;AAAA,EAAA,EAAA,CACF,EAAA,CACF;AAEJ;"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { MaterialSymbolWeight, SymbolCodepoints } from '../UI/MaterialSymbol';
|
|
2
|
+
export type TUiSidebarSection = 'management' | 'registration';
|
|
3
|
+
export interface ISidebarMenuItem {
|
|
4
|
+
id?: string;
|
|
5
|
+
weight?: MaterialSymbolWeight;
|
|
6
|
+
icon: SymbolCodepoints;
|
|
7
|
+
text: string;
|
|
8
|
+
to: string;
|
|
9
|
+
externalUrl?: string;
|
|
10
|
+
end?: boolean;
|
|
11
|
+
section?: TUiSidebarSection;
|
|
12
|
+
children?: Array<ISidebarMenuItem>;
|
|
13
|
+
}
|
|
14
|
+
export declare const resolveSidebarItemId: (item: ISidebarMenuItem) => string;
|
|
15
|
+
interface ISidebarMenuProps {
|
|
16
|
+
items: Array<ISidebarMenuItem>;
|
|
17
|
+
activeClass: string;
|
|
18
|
+
collapsed: boolean;
|
|
19
|
+
activeIconClass?: string;
|
|
20
|
+
onNavigate?: () => void;
|
|
21
|
+
level?: number;
|
|
22
|
+
}
|
|
23
|
+
declare const SidebarMenu: React.FC<ISidebarMenuProps>;
|
|
24
|
+
export default SidebarMenu;
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useRef, useEffect } from "react";
|
|
3
|
+
import classNames from "classnames";
|
|
4
|
+
import { useTranslation } from "react-i18next";
|
|
5
|
+
import ControlledButton from "../Form/ControlledButton.js";
|
|
6
|
+
import { useUiNavigation } from "../Navigation/UiNavigationContext.js";
|
|
7
|
+
import { MaterialSymbol } from "../UI/MaterialSymbol/index.js";
|
|
8
|
+
const resolveSidebarItemId = (item) => item.id ?? `${item.to}:${item.text}`;
|
|
9
|
+
const matchesRoute = (location, target, end = false) => {
|
|
10
|
+
if (!target || target === "#") return false;
|
|
11
|
+
if (target.includes("?")) {
|
|
12
|
+
const [pathPart, queryPart] = target.split("?");
|
|
13
|
+
const samePath = end ? location.pathname === pathPart : location.pathname === pathPart || location.pathname.startsWith(`${pathPart}/`);
|
|
14
|
+
if (!samePath) return false;
|
|
15
|
+
const currentParams = new URLSearchParams(location.search);
|
|
16
|
+
const expectedParams = new URLSearchParams(queryPart);
|
|
17
|
+
return Array.from(expectedParams.entries()).every(([key, value]) => currentParams.get(key) === value);
|
|
18
|
+
}
|
|
19
|
+
return end ? location.pathname === target : location.pathname === target || location.pathname.startsWith(`${target}/`);
|
|
20
|
+
};
|
|
21
|
+
const containsActiveRoute = (item, location) => item.children?.length ? item.children.some((child) => containsActiveRoute(child, location)) : matchesRoute(location, item.to, item.end);
|
|
22
|
+
const SidebarMenuRow = ({
|
|
23
|
+
item,
|
|
24
|
+
isOpen,
|
|
25
|
+
collapsed,
|
|
26
|
+
onToggle,
|
|
27
|
+
activeClass,
|
|
28
|
+
activeIconClass,
|
|
29
|
+
onNavigate,
|
|
30
|
+
level
|
|
31
|
+
}) => {
|
|
32
|
+
const { location, Link: NavigationLink } = useUiNavigation();
|
|
33
|
+
const contentRef = useRef(null);
|
|
34
|
+
const [maxHeight, setMaxHeight] = useState("0px");
|
|
35
|
+
const hasChildren = Boolean(item.children?.length);
|
|
36
|
+
const isActive = hasChildren ? containsActiveRoute(item, location) : matchesRoute(location, item.to, item.end);
|
|
37
|
+
const submenuOpen = isOpen && !collapsed;
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
const content = contentRef.current;
|
|
40
|
+
if (!content) return;
|
|
41
|
+
if (!submenuOpen) {
|
|
42
|
+
setMaxHeight("0px");
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const updateHeight = () => setMaxHeight(`${content.scrollHeight}px`);
|
|
46
|
+
updateHeight();
|
|
47
|
+
const observer = new ResizeObserver(updateHeight);
|
|
48
|
+
observer.observe(content);
|
|
49
|
+
return () => observer.disconnect();
|
|
50
|
+
}, [submenuOpen]);
|
|
51
|
+
const rowClassName = classNames(
|
|
52
|
+
"relative flex h-9 w-full items-center gap-x-3 rounded-lg px-3 text-left transition-colors duration-base",
|
|
53
|
+
isActive ? activeClass : "text-foreground-muted hover:bg-muted-hover hover:text-foreground"
|
|
54
|
+
);
|
|
55
|
+
const iconClassName = classNames(
|
|
56
|
+
"shrink-0",
|
|
57
|
+
isActive ? activeIconClass ?? "text-brand-emphasis" : "text-foreground-muted"
|
|
58
|
+
);
|
|
59
|
+
const activeBar = isActive ? /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "absolute left-0 top-1/2 h-5 w-0.5 -translate-y-1/2 rounded-full bg-brand" }) : null;
|
|
60
|
+
const icon = /* @__PURE__ */ jsx(MaterialSymbol, { icon: item.icon, size: 20, weight: item.weight || 400, className: iconClassName });
|
|
61
|
+
const label = /* @__PURE__ */ jsx("span", { className: classNames("sidebar-label text-sm", { "is-collapsed": collapsed }), children: item.text });
|
|
62
|
+
if (item.externalUrl) {
|
|
63
|
+
return /* @__PURE__ */ jsx("li", { className: "flex flex-col", children: /* @__PURE__ */ jsxs(
|
|
64
|
+
"a",
|
|
65
|
+
{
|
|
66
|
+
href: item.externalUrl,
|
|
67
|
+
target: "_blank",
|
|
68
|
+
rel: "noopener noreferrer",
|
|
69
|
+
className: rowClassName,
|
|
70
|
+
onClick: onNavigate,
|
|
71
|
+
children: [
|
|
72
|
+
activeBar,
|
|
73
|
+
icon,
|
|
74
|
+
label
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
) });
|
|
78
|
+
}
|
|
79
|
+
if (hasChildren) {
|
|
80
|
+
return /* @__PURE__ */ jsxs("li", { className: "flex flex-col", children: [
|
|
81
|
+
/* @__PURE__ */ jsxs(ControlledButton, { defaultHtmlButton: true, onClick: onToggle, "aria-expanded": submenuOpen, className: rowClassName, children: [
|
|
82
|
+
activeBar,
|
|
83
|
+
icon,
|
|
84
|
+
label,
|
|
85
|
+
/* @__PURE__ */ jsx(
|
|
86
|
+
MaterialSymbol,
|
|
87
|
+
{
|
|
88
|
+
icon: "chevron_right",
|
|
89
|
+
size: 18,
|
|
90
|
+
className: classNames("sidebar-chevron ml-auto shrink-0 text-foreground-subtle", {
|
|
91
|
+
"is-collapsed": collapsed,
|
|
92
|
+
"is-open": submenuOpen
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
)
|
|
96
|
+
] }),
|
|
97
|
+
/* @__PURE__ */ jsx("div", { style: { maxHeight }, className: classNames("sidebar-submenu", { "mt-1": submenuOpen }), children: /* @__PURE__ */ jsx("div", { ref: contentRef, className: "pl-5", children: /* @__PURE__ */ jsx(
|
|
98
|
+
SidebarMenu,
|
|
99
|
+
{
|
|
100
|
+
items: item.children ?? [],
|
|
101
|
+
collapsed,
|
|
102
|
+
activeClass,
|
|
103
|
+
activeIconClass,
|
|
104
|
+
onNavigate,
|
|
105
|
+
level: level + 1
|
|
106
|
+
}
|
|
107
|
+
) }) })
|
|
108
|
+
] });
|
|
109
|
+
}
|
|
110
|
+
return /* @__PURE__ */ jsx("li", { className: "flex flex-col", children: /* @__PURE__ */ jsxs(NavigationLink, { to: item.to, className: rowClassName, onClick: onNavigate, children: [
|
|
111
|
+
activeBar,
|
|
112
|
+
icon,
|
|
113
|
+
label
|
|
114
|
+
] }) });
|
|
115
|
+
};
|
|
116
|
+
const SECTION_LABEL_KEYS = {
|
|
117
|
+
management: "sidebar.sections.management",
|
|
118
|
+
registration: "sidebar.sections.registration"
|
|
119
|
+
};
|
|
120
|
+
const SidebarSectionHeader = ({ section, collapsed, withDivider }) => {
|
|
121
|
+
const { t } = useTranslation("ui");
|
|
122
|
+
if (collapsed) {
|
|
123
|
+
if (!withDivider) return null;
|
|
124
|
+
return /* @__PURE__ */ jsx("li", { "aria-hidden": true, className: "flex flex-col px-3", children: /* @__PURE__ */ jsx("span", { className: "border-t border-border/70" }) });
|
|
125
|
+
}
|
|
126
|
+
return /* @__PURE__ */ jsxs("li", { className: classNames("flex flex-col", withDivider ? "mt-2" : "mt-1"), children: [
|
|
127
|
+
withDivider && /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "mx-3 mb-2 border-t border-border/70" }),
|
|
128
|
+
/* @__PURE__ */ jsx("span", { className: "px-3 pb-0.5 text-xxs font-semibold uppercase tracking-wide text-foreground-subtle", children: t(SECTION_LABEL_KEYS[section]) })
|
|
129
|
+
] });
|
|
130
|
+
};
|
|
131
|
+
const SECTION_ORDER = ["management", "registration"];
|
|
132
|
+
const groupItemsBySection = (items) => {
|
|
133
|
+
const itemsWithoutSection = (items ?? []).filter((item) => !item.section);
|
|
134
|
+
const sectionGroups = SECTION_ORDER.map((section) => ({
|
|
135
|
+
section,
|
|
136
|
+
items: (items ?? []).filter((item) => item.section === section)
|
|
137
|
+
})).filter((group) => group.items.length > 0);
|
|
138
|
+
return itemsWithoutSection.length ? [{ section: null, items: itemsWithoutSection }, ...sectionGroups] : sectionGroups;
|
|
139
|
+
};
|
|
140
|
+
const SidebarMenu = ({
|
|
141
|
+
items,
|
|
142
|
+
activeClass,
|
|
143
|
+
collapsed,
|
|
144
|
+
activeIconClass,
|
|
145
|
+
onNavigate,
|
|
146
|
+
level = 0
|
|
147
|
+
}) => {
|
|
148
|
+
const { location } = useUiNavigation();
|
|
149
|
+
const activeKey = items.filter((item) => Boolean(item.children?.length) && containsActiveRoute(item, location)).map(resolveSidebarItemId)[0] ?? null;
|
|
150
|
+
const [openKey, setOpenKey] = useState(activeKey);
|
|
151
|
+
const previousActiveKey = useRef(activeKey);
|
|
152
|
+
useEffect(() => {
|
|
153
|
+
if (activeKey && activeKey !== previousActiveKey.current) {
|
|
154
|
+
setOpenKey(activeKey);
|
|
155
|
+
}
|
|
156
|
+
previousActiveKey.current = activeKey;
|
|
157
|
+
}, [activeKey]);
|
|
158
|
+
const handleToggle = (key) => {
|
|
159
|
+
setOpenKey((current) => current === key ? null : key);
|
|
160
|
+
};
|
|
161
|
+
const showSections = level === 0;
|
|
162
|
+
const groups = showSections ? groupItemsBySection(items) : [{ section: null, items: items ?? [] }];
|
|
163
|
+
return /* @__PURE__ */ jsx("ul", { className: classNames("flex flex-col", level === 0 ? "gap-y-2" : "gap-y-1"), children: groups.flatMap((group, groupIndex) => {
|
|
164
|
+
const rows = [];
|
|
165
|
+
if (group.section) {
|
|
166
|
+
rows.push(
|
|
167
|
+
/* @__PURE__ */ jsx(
|
|
168
|
+
SidebarSectionHeader,
|
|
169
|
+
{
|
|
170
|
+
section: group.section,
|
|
171
|
+
collapsed,
|
|
172
|
+
withDivider: groupIndex > 0
|
|
173
|
+
},
|
|
174
|
+
`section-${group.section}`
|
|
175
|
+
)
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
group.items.forEach((item) => {
|
|
179
|
+
const itemId = resolveSidebarItemId(item);
|
|
180
|
+
rows.push(
|
|
181
|
+
/* @__PURE__ */ jsx(
|
|
182
|
+
SidebarMenuRow,
|
|
183
|
+
{
|
|
184
|
+
item,
|
|
185
|
+
isOpen: openKey === itemId,
|
|
186
|
+
collapsed,
|
|
187
|
+
onToggle: () => handleToggle(itemId),
|
|
188
|
+
activeClass,
|
|
189
|
+
activeIconClass,
|
|
190
|
+
onNavigate,
|
|
191
|
+
level
|
|
192
|
+
},
|
|
193
|
+
itemId
|
|
194
|
+
)
|
|
195
|
+
);
|
|
196
|
+
});
|
|
197
|
+
return rows;
|
|
198
|
+
}) });
|
|
199
|
+
};
|
|
200
|
+
export {
|
|
201
|
+
SidebarMenu as default,
|
|
202
|
+
resolveSidebarItemId
|
|
203
|
+
};
|
|
204
|
+
//# sourceMappingURL=SidebarMenu.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SidebarMenu.js","sources":["../../../src/components/Shell/SidebarMenu.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from 'react';\nimport classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\n\nimport ControlledButton from '@/components/Form/ControlledButton';\nimport { useUiNavigation } from '@/components/Navigation/UiNavigationContext';\nimport { MaterialSymbol, MaterialSymbolWeight, SymbolCodepoints } from '@/components/UI/MaterialSymbol';\n\nexport type TUiSidebarSection = 'management' | 'registration';\n\nexport interface ISidebarMenuItem {\n id?: string;\n weight?: MaterialSymbolWeight;\n icon: SymbolCodepoints;\n text: string;\n to: string;\n externalUrl?: string;\n end?: boolean;\n section?: TUiSidebarSection;\n children?: Array<ISidebarMenuItem>;\n}\n\nexport const resolveSidebarItemId = (item: ISidebarMenuItem): string => item.id ?? `${item.to}:${item.text}`;\n\ninterface ISidebarLocation {\n pathname: string;\n search: string;\n}\n\ninterface ISidebarMenuProps {\n items: Array<ISidebarMenuItem>;\n activeClass: string;\n collapsed: boolean;\n activeIconClass?: string;\n onNavigate?: () => void;\n level?: number;\n}\n\nconst matchesRoute = (location: ISidebarLocation, target: string, end = false): boolean => {\n if (!target || target === '#') return false;\n\n if (target.includes('?')) {\n const [pathPart, queryPart] = target.split('?');\n const samePath = end\n ? location.pathname === pathPart\n : location.pathname === pathPart || location.pathname.startsWith(`${pathPart}/`);\n if (!samePath) return false;\n\n const currentParams = new URLSearchParams(location.search);\n const expectedParams = new URLSearchParams(queryPart);\n\n return Array.from(expectedParams.entries()).every(([key, value]) => currentParams.get(key) === value);\n }\n\n return end\n ? location.pathname === target\n : location.pathname === target || location.pathname.startsWith(`${target}/`);\n};\n\nconst containsActiveRoute = (item: ISidebarMenuItem, location: ISidebarLocation): boolean =>\n item.children?.length\n ? item.children.some((child) => containsActiveRoute(child, location))\n : matchesRoute(location, item.to, item.end);\n\ninterface ISidebarMenuRowProps {\n item: ISidebarMenuItem;\n isOpen: boolean;\n collapsed: boolean;\n onToggle: () => void;\n activeClass: string;\n activeIconClass?: string;\n onNavigate?: () => void;\n level: number;\n}\n\nconst SidebarMenuRow: React.FC<ISidebarMenuRowProps> = ({\n item,\n isOpen,\n collapsed,\n onToggle,\n activeClass,\n activeIconClass,\n onNavigate,\n level,\n}) => {\n const { location, Link: NavigationLink } = useUiNavigation();\n const contentRef = useRef<HTMLDivElement>(null);\n const [maxHeight, setMaxHeight] = useState('0px');\n\n const hasChildren = Boolean(item.children?.length);\n const isActive = hasChildren ? containsActiveRoute(item, location) : matchesRoute(location, item.to, item.end);\n const submenuOpen = isOpen && !collapsed;\n\n useEffect(() => {\n const content = contentRef.current;\n if (!content) return;\n\n if (!submenuOpen) {\n setMaxHeight('0px');\n return;\n }\n\n const updateHeight = () => setMaxHeight(`${content.scrollHeight}px`);\n updateHeight();\n\n const observer = new ResizeObserver(updateHeight);\n observer.observe(content);\n\n return () => observer.disconnect();\n }, [submenuOpen]);\n\n const rowClassName = classNames(\n 'relative flex h-9 w-full items-center gap-x-3 rounded-lg px-3 text-left transition-colors duration-base',\n isActive ? activeClass : 'text-foreground-muted hover:bg-muted-hover hover:text-foreground',\n );\n\n const iconClassName = classNames(\n 'shrink-0',\n isActive ? (activeIconClass ?? 'text-brand-emphasis') : 'text-foreground-muted',\n );\n\n const activeBar = isActive ? (\n <span aria-hidden className=\"absolute left-0 top-1/2 h-5 w-0.5 -translate-y-1/2 rounded-full bg-brand\" />\n ) : null;\n\n const icon = <MaterialSymbol icon={item.icon} size={20} weight={item.weight || 400} className={iconClassName} />;\n const label = <span className={classNames('sidebar-label text-sm', { 'is-collapsed': collapsed })}>{item.text}</span>;\n\n if (item.externalUrl) {\n return (\n <li className=\"flex flex-col\">\n <a\n href={item.externalUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className={rowClassName}\n onClick={onNavigate}\n >\n {activeBar}\n {icon}\n {label}\n </a>\n </li>\n );\n }\n\n if (hasChildren) {\n return (\n <li className=\"flex flex-col\">\n <ControlledButton defaultHtmlButton onClick={onToggle} aria-expanded={submenuOpen} className={rowClassName}>\n {activeBar}\n {icon}\n {label}\n <MaterialSymbol\n icon=\"chevron_right\"\n size={18}\n className={classNames('sidebar-chevron ml-auto shrink-0 text-foreground-subtle', {\n 'is-collapsed': collapsed,\n 'is-open': submenuOpen,\n })}\n />\n </ControlledButton>\n\n <div style={{ maxHeight }} className={classNames('sidebar-submenu', { 'mt-1': submenuOpen })}>\n <div ref={contentRef} className=\"pl-5\">\n <SidebarMenu\n items={item.children ?? []}\n collapsed={collapsed}\n activeClass={activeClass}\n activeIconClass={activeIconClass}\n onNavigate={onNavigate}\n level={level + 1}\n />\n </div>\n </div>\n </li>\n );\n }\n\n return (\n <li className=\"flex flex-col\">\n <NavigationLink to={item.to} className={rowClassName} onClick={onNavigate}>\n {activeBar}\n {icon}\n {label}\n </NavigationLink>\n </li>\n );\n};\n\nconst SECTION_LABEL_KEYS: Record<TUiSidebarSection, string> = {\n management: 'sidebar.sections.management',\n registration: 'sidebar.sections.registration',\n};\n\ninterface ISidebarSectionHeaderProps {\n section: TUiSidebarSection;\n collapsed: boolean;\n withDivider: boolean;\n}\n\nconst SidebarSectionHeader: React.FC<ISidebarSectionHeaderProps> = ({ section, collapsed, withDivider }) => {\n const { t } = useTranslation('ui');\n\n if (collapsed) {\n if (!withDivider) return null;\n return (\n <li aria-hidden className=\"flex flex-col px-3\">\n <span className=\"border-t border-border/70\" />\n </li>\n );\n }\n\n return (\n <li className={classNames('flex flex-col', withDivider ? 'mt-2' : 'mt-1')}>\n {withDivider && <span aria-hidden className=\"mx-3 mb-2 border-t border-border/70\" />}\n <span className=\"px-3 pb-0.5 text-xxs font-semibold uppercase tracking-wide text-foreground-subtle\">\n {t(SECTION_LABEL_KEYS[section])}\n </span>\n </li>\n );\n};\n\nconst SECTION_ORDER: Array<TUiSidebarSection> = ['management', 'registration'];\n\ninterface ISidebarMenuGroup {\n section: TUiSidebarSection | null;\n items: Array<ISidebarMenuItem>;\n}\n\nconst groupItemsBySection = (items: Array<ISidebarMenuItem>): Array<ISidebarMenuGroup> => {\n const itemsWithoutSection = (items ?? []).filter((item) => !item.section);\n const sectionGroups = SECTION_ORDER.map((section) => ({\n section,\n items: (items ?? []).filter((item) => item.section === section),\n })).filter((group) => group.items.length > 0);\n\n return itemsWithoutSection.length ? [{ section: null, items: itemsWithoutSection }, ...sectionGroups] : sectionGroups;\n};\n\nconst SidebarMenu: React.FC<ISidebarMenuProps> = ({\n items,\n activeClass,\n collapsed,\n activeIconClass,\n onNavigate,\n level = 0,\n}) => {\n const { location } = useUiNavigation();\n\n const activeKey =\n items\n .filter((item) => Boolean(item.children?.length) && containsActiveRoute(item, location))\n .map(resolveSidebarItemId)[0] ?? null;\n\n const [openKey, setOpenKey] = useState<string | null>(activeKey);\n const previousActiveKey = useRef(activeKey);\n\n useEffect(() => {\n if (activeKey && activeKey !== previousActiveKey.current) {\n setOpenKey(activeKey);\n }\n previousActiveKey.current = activeKey;\n }, [activeKey]);\n\n const handleToggle = (key: string) => {\n setOpenKey((current) => (current === key ? null : key));\n };\n\n const showSections = level === 0;\n const groups = showSections ? groupItemsBySection(items) : [{ section: null, items: items ?? [] }];\n\n return (\n <ul className={classNames('flex flex-col', level === 0 ? 'gap-y-2' : 'gap-y-1')}>\n {groups.flatMap((group, groupIndex) => {\n const rows: Array<React.ReactElement> = [];\n\n if (group.section) {\n rows.push(\n <SidebarSectionHeader\n key={`section-${group.section}`}\n section={group.section}\n collapsed={collapsed}\n withDivider={groupIndex > 0}\n />,\n );\n }\n\n group.items.forEach((item) => {\n const itemId = resolveSidebarItemId(item);\n\n rows.push(\n <SidebarMenuRow\n key={itemId}\n item={item}\n isOpen={openKey === itemId}\n collapsed={collapsed}\n onToggle={() => handleToggle(itemId)}\n activeClass={activeClass}\n activeIconClass={activeIconClass}\n onNavigate={onNavigate}\n level={level}\n />,\n );\n });\n\n return rows;\n })}\n </ul>\n );\n};\n\nexport default SidebarMenu;\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type TUiThemePreference = 'light' | 'system' | 'dark';
|
|
2
|
+
interface IThemeToggleProps {
|
|
3
|
+
preference: TUiThemePreference;
|
|
4
|
+
onChange: (preference: TUiThemePreference) => void;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Three-state control: an explicit light/dark choice, or `system` to follow the OS. The host
|
|
8
|
+
* owns the preference (persistence + writing `data-theme` on <html>); this renders the knob.
|
|
9
|
+
*/
|
|
10
|
+
declare const ThemeToggle: React.FC<IThemeToggleProps>;
|
|
11
|
+
export default ThemeToggle;
|
|
@@ -0,0 +1,55 @@
|
|
|
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 ControlledTooltip from "../UI/ControlledTooltip.js";
|
|
6
|
+
import { MaterialSymbol } from "../UI/MaterialSymbol/index.js";
|
|
7
|
+
import { HEADER_CONTROL_SHELL_CLASS, HEADER_CONTROL_SIZE_REM } from "./headerControlStyles.js";
|
|
8
|
+
const OPTION_SIZE_REM = HEADER_CONTROL_SIZE_REM;
|
|
9
|
+
const OPTIONS = [
|
|
10
|
+
{ value: "light", icon: "light_mode", labelKey: "theme.light" },
|
|
11
|
+
{ value: "system", icon: "computer", labelKey: "theme.system" },
|
|
12
|
+
{ value: "dark", icon: "dark_mode", labelKey: "theme.dark" }
|
|
13
|
+
];
|
|
14
|
+
const ThemeToggle = ({ preference, onChange }) => {
|
|
15
|
+
const { t } = useTranslation("ui");
|
|
16
|
+
const activeIndex = Math.max(
|
|
17
|
+
0,
|
|
18
|
+
OPTIONS.findIndex((option) => option.value === preference)
|
|
19
|
+
);
|
|
20
|
+
return /* @__PURE__ */ jsx("div", { role: "group", "aria-label": t("theme.toggle"), className: HEADER_CONTROL_SHELL_CLASS, children: /* @__PURE__ */ jsxs("div", { className: "relative flex items-center", children: [
|
|
21
|
+
/* @__PURE__ */ jsx(
|
|
22
|
+
"div",
|
|
23
|
+
{
|
|
24
|
+
"aria-hidden": true,
|
|
25
|
+
className: "pointer-events-none absolute left-0 top-0 rounded-full bg-surface shadow-elevation1 ring-1 ring-border transition-transform duration-base ease-enter",
|
|
26
|
+
style: {
|
|
27
|
+
width: `${OPTION_SIZE_REM}rem`,
|
|
28
|
+
height: `${OPTION_SIZE_REM}rem`,
|
|
29
|
+
transform: `translateX(${activeIndex * OPTION_SIZE_REM}rem)`
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
),
|
|
33
|
+
OPTIONS.map((option) => {
|
|
34
|
+
const isActive = option.value === preference;
|
|
35
|
+
return /* @__PURE__ */ jsx(ControlledTooltip, { content: t(option.labelKey), children: /* @__PURE__ */ jsx(
|
|
36
|
+
ControlledButton,
|
|
37
|
+
{
|
|
38
|
+
defaultHtmlButton: true,
|
|
39
|
+
"aria-pressed": isActive,
|
|
40
|
+
onClick: () => onChange(option.value),
|
|
41
|
+
className: classNames(
|
|
42
|
+
"relative z-10 flex shrink-0 items-center justify-center rounded-full leading-none transition-colors duration-base",
|
|
43
|
+
isActive ? "text-brand-emphasis" : "text-foreground-faint hover:text-foreground-muted"
|
|
44
|
+
),
|
|
45
|
+
style: { width: `${OPTION_SIZE_REM}rem`, height: `${OPTION_SIZE_REM}rem` },
|
|
46
|
+
children: /* @__PURE__ */ jsx(MaterialSymbol, { icon: option.icon, size: 15, fill: isActive })
|
|
47
|
+
}
|
|
48
|
+
) }, option.value);
|
|
49
|
+
})
|
|
50
|
+
] }) });
|
|
51
|
+
};
|
|
52
|
+
export {
|
|
53
|
+
ThemeToggle as default
|
|
54
|
+
};
|
|
55
|
+
//# sourceMappingURL=ThemeToggle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ThemeToggle.js","sources":["../../../src/components/Shell/ThemeToggle.tsx"],"sourcesContent":["import classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\n\nimport ControlledButton from '@/components/Form/ControlledButton';\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { MaterialSymbol, SymbolCodepoints } from '@/components/UI/MaterialSymbol';\n\nimport { HEADER_CONTROL_SHELL_CLASS, HEADER_CONTROL_SIZE_REM } from './headerControlStyles';\n\nconst OPTION_SIZE_REM = HEADER_CONTROL_SIZE_REM;\n\nexport type TUiThemePreference = 'light' | 'system' | 'dark';\n\nconst OPTIONS: Array<{ value: TUiThemePreference; icon: SymbolCodepoints; labelKey: string }> = [\n { value: 'light', icon: 'light_mode', labelKey: 'theme.light' },\n { value: 'system', icon: 'computer', labelKey: 'theme.system' },\n { value: 'dark', icon: 'dark_mode', labelKey: 'theme.dark' },\n];\n\ninterface IThemeToggleProps {\n preference: TUiThemePreference;\n onChange: (preference: TUiThemePreference) => void;\n}\n\n/**\n * Three-state control: an explicit light/dark choice, or `system` to follow the OS. The host\n * owns the preference (persistence + writing `data-theme` on <html>); this renders the knob.\n */\nconst ThemeToggle: React.FC<IThemeToggleProps> = ({ preference, onChange }) => {\n const { t } = useTranslation('ui');\n\n const activeIndex = Math.max(\n 0,\n OPTIONS.findIndex((option) => option.value === preference),\n );\n\n return (\n <div role=\"group\" aria-label={t('theme.toggle')} className={HEADER_CONTROL_SHELL_CLASS}>\n <div className=\"relative flex items-center\">\n <div\n aria-hidden\n className=\"pointer-events-none absolute left-0 top-0 rounded-full bg-surface shadow-elevation1 ring-1 ring-border transition-transform duration-base ease-enter\"\n style={{\n width: `${OPTION_SIZE_REM}rem`,\n height: `${OPTION_SIZE_REM}rem`,\n transform: `translateX(${activeIndex * OPTION_SIZE_REM}rem)`,\n }}\n />\n\n {OPTIONS.map((option) => {\n const isActive = option.value === preference;\n\n return (\n <ControlledTooltip key={option.value} content={t(option.labelKey)}>\n <ControlledButton\n defaultHtmlButton\n aria-pressed={isActive}\n onClick={() => onChange(option.value)}\n className={classNames(\n 'relative z-10 flex shrink-0 items-center justify-center rounded-full leading-none transition-colors duration-base',\n isActive ? 'text-brand-emphasis' : 'text-foreground-faint hover:text-foreground-muted',\n )}\n style={{ width: `${OPTION_SIZE_REM}rem`, height: `${OPTION_SIZE_REM}rem` }}\n >\n <MaterialSymbol icon={option.icon} size={15} fill={isActive} />\n </ControlledButton>\n </ControlledTooltip>\n );\n })}\n </div>\n </div>\n );\n};\n\nexport default ThemeToggle;\n"],"names":[],"mappings":";;;;;;;AASA,MAAM,kBAAkB;AAIxB,MAAM,UAA0F;AAAA,EAC9F,EAAE,OAAO,SAAS,MAAM,cAAc,UAAU,cAAA;AAAA,EAChD,EAAE,OAAO,UAAU,MAAM,YAAY,UAAU,eAAA;AAAA,EAC/C,EAAE,OAAO,QAAQ,MAAM,aAAa,UAAU,aAAA;AAChD;AAWA,MAAM,cAA2C,CAAC,EAAE,YAAY,eAAe;AAC7E,QAAM,EAAE,EAAA,IAAM,eAAe,IAAI;AAEjC,QAAM,cAAc,KAAK;AAAA,IACvB;AAAA,IACA,QAAQ,UAAU,CAAC,WAAW,OAAO,UAAU,UAAU;AAAA,EAAA;AAG3D,SACE,oBAAC,OAAA,EAAI,MAAK,SAAQ,cAAY,EAAE,cAAc,GAAG,WAAW,4BAC1D,UAAA,qBAAC,OAAA,EAAI,WAAU,8BACb,UAAA;AAAA,IAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,eAAW;AAAA,QACX,WAAU;AAAA,QACV,OAAO;AAAA,UACL,OAAO,GAAG,eAAe;AAAA,UACzB,QAAQ,GAAG,eAAe;AAAA,UAC1B,WAAW,cAAc,cAAc,eAAe;AAAA,QAAA;AAAA,MACxD;AAAA,IAAA;AAAA,IAGD,QAAQ,IAAI,CAAC,WAAW;AACvB,YAAM,WAAW,OAAO,UAAU;AAElC,iCACG,mBAAA,EAAqC,SAAS,EAAE,OAAO,QAAQ,GAC9D,UAAA;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,mBAAiB;AAAA,UACjB,gBAAc;AAAA,UACd,SAAS,MAAM,SAAS,OAAO,KAAK;AAAA,UACpC,WAAW;AAAA,YACT;AAAA,YACA,WAAW,wBAAwB;AAAA,UAAA;AAAA,UAErC,OAAO,EAAE,OAAO,GAAG,eAAe,OAAO,QAAQ,GAAG,eAAe,MAAA;AAAA,UAEnE,UAAA,oBAAC,kBAAe,MAAM,OAAO,MAAM,MAAM,IAAI,MAAM,SAAA,CAAU;AAAA,QAAA;AAAA,MAAA,EAC/D,GAZsB,OAAO,KAa/B;AAAA,IAEJ,CAAC;AAAA,EAAA,EAAA,CACH,EAAA,CACF;AAEJ;"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every control on the right side of the header (workspace actions, theme, language) lives
|
|
3
|
+
* inside the same pill shell, so they read as one family instead of three unrelated widgets.
|
|
4
|
+
* The size is a constant because the theme/language knobs travel by exactly one option width:
|
|
5
|
+
* any drift between the knob and the buttons puts the highlight off the icon.
|
|
6
|
+
*/
|
|
7
|
+
export declare const HEADER_CONTROL_SIZE_REM = 1.75;
|
|
8
|
+
export declare const HEADER_CONTROL_SHELL_CLASS = "flex items-center rounded-full border border-border bg-muted p-[3px]";
|
|
9
|
+
export declare const HEADER_ICON_ACTION_CLASS = "interactive relative flex size-7 shrink-0 items-center justify-center rounded-full text-foreground-subtle hover:bg-surface hover:text-foreground hover:shadow-elevation1";
|
|
10
|
+
export declare const HEADER_ICON_ACTION_SIZE = 18;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const HEADER_CONTROL_SIZE_REM = 1.75;
|
|
2
|
+
const HEADER_CONTROL_SHELL_CLASS = "flex items-center rounded-full border border-border bg-muted p-[3px]";
|
|
3
|
+
const HEADER_ICON_ACTION_CLASS = "interactive relative flex size-7 shrink-0 items-center justify-center rounded-full text-foreground-subtle hover:bg-surface hover:text-foreground hover:shadow-elevation1";
|
|
4
|
+
const HEADER_ICON_ACTION_SIZE = 18;
|
|
5
|
+
export {
|
|
6
|
+
HEADER_CONTROL_SHELL_CLASS,
|
|
7
|
+
HEADER_CONTROL_SIZE_REM,
|
|
8
|
+
HEADER_ICON_ACTION_CLASS,
|
|
9
|
+
HEADER_ICON_ACTION_SIZE
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=headerControlStyles.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"headerControlStyles.js","sources":["../../../src/components/Shell/headerControlStyles.ts"],"sourcesContent":["/**\n * Every control on the right side of the header (workspace actions, theme, language) lives\n * inside the same pill shell, so they read as one family instead of three unrelated widgets.\n * The size is a constant because the theme/language knobs travel by exactly one option width:\n * any drift between the knob and the buttons puts the highlight off the icon.\n */\nexport const HEADER_CONTROL_SIZE_REM = 1.75;\n\nexport const HEADER_CONTROL_SHELL_CLASS = 'flex items-center rounded-full border border-border bg-muted p-[3px]';\n\nexport const HEADER_ICON_ACTION_CLASS =\n 'interactive relative flex size-7 shrink-0 items-center justify-center rounded-full text-foreground-subtle hover:bg-surface hover:text-foreground hover:shadow-elevation1';\n\nexport const HEADER_ICON_ACTION_SIZE = 18;\n"],"names":[],"mappings":"AAMO,MAAM,0BAA0B;AAEhC,MAAM,6BAA6B;AAEnC,MAAM,2BACX;AAEK,MAAM,0BAA0B;"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface ISidebarState {
|
|
2
|
+
isMobile: boolean;
|
|
3
|
+
isVisible: boolean;
|
|
4
|
+
isExpanded: boolean;
|
|
5
|
+
isDocked: boolean;
|
|
6
|
+
isOverlayOnMobile: boolean;
|
|
7
|
+
showRail: boolean;
|
|
8
|
+
toggleLabelKey: string;
|
|
9
|
+
togglePrimary: () => void;
|
|
10
|
+
onHoverEnter: () => void;
|
|
11
|
+
onHoverLeave: () => void;
|
|
12
|
+
closeMobileDrawer: () => void;
|
|
13
|
+
}
|
|
14
|
+
export declare const useSidebarState: () => ISidebarState;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { useState, useRef, useCallback, useEffect } from "react";
|
|
2
|
+
import { writeUiPreference, readUiPreference } from "../../helpers/uiPreferences.js";
|
|
3
|
+
import { useMobile } from "../../hooks/useMobile.js";
|
|
4
|
+
const HOVER_OPEN_DELAY_MS = 250;
|
|
5
|
+
const HOVER_CLOSE_DELAY_MS = 250;
|
|
6
|
+
const PIN_STORAGE_KEY = "sidebar_pinned";
|
|
7
|
+
const TOGGLE_SHORTCUT_KEY = "\\";
|
|
8
|
+
const readPinnedPreference = () => readUiPreference(PIN_STORAGE_KEY) ?? false;
|
|
9
|
+
const useSidebarState = () => {
|
|
10
|
+
const isMobile = useMobile();
|
|
11
|
+
const [isPinned, setIsPinned] = useState(readPinnedPreference);
|
|
12
|
+
const [isHovering, setIsHovering] = useState(false);
|
|
13
|
+
const [isMobileOpen, setIsMobileOpen] = useState(false);
|
|
14
|
+
const openTimerRef = useRef(null);
|
|
15
|
+
const closeTimerRef = useRef(null);
|
|
16
|
+
const clearOpenTimer = useCallback(() => {
|
|
17
|
+
if (openTimerRef.current !== null) {
|
|
18
|
+
window.clearTimeout(openTimerRef.current);
|
|
19
|
+
openTimerRef.current = null;
|
|
20
|
+
}
|
|
21
|
+
}, []);
|
|
22
|
+
const clearCloseTimer = useCallback(() => {
|
|
23
|
+
if (closeTimerRef.current !== null) {
|
|
24
|
+
window.clearTimeout(closeTimerRef.current);
|
|
25
|
+
closeTimerRef.current = null;
|
|
26
|
+
}
|
|
27
|
+
}, []);
|
|
28
|
+
const clearTimers = useCallback(() => {
|
|
29
|
+
clearOpenTimer();
|
|
30
|
+
clearCloseTimer();
|
|
31
|
+
}, [clearOpenTimer, clearCloseTimer]);
|
|
32
|
+
const persistPinned = useCallback((next) => {
|
|
33
|
+
setIsPinned(next);
|
|
34
|
+
writeUiPreference(PIN_STORAGE_KEY, next);
|
|
35
|
+
}, []);
|
|
36
|
+
const togglePrimary = useCallback(() => {
|
|
37
|
+
clearTimers();
|
|
38
|
+
if (isMobile) {
|
|
39
|
+
setIsMobileOpen((open) => !open);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
setIsHovering(false);
|
|
43
|
+
persistPinned(!isPinned);
|
|
44
|
+
}, [clearTimers, isMobile, isPinned, persistPinned]);
|
|
45
|
+
const onHoverEnter = useCallback(() => {
|
|
46
|
+
if (isMobile || isPinned) return;
|
|
47
|
+
clearCloseTimer();
|
|
48
|
+
if (isHovering || openTimerRef.current !== null) return;
|
|
49
|
+
openTimerRef.current = window.setTimeout(() => {
|
|
50
|
+
openTimerRef.current = null;
|
|
51
|
+
setIsHovering(true);
|
|
52
|
+
}, HOVER_OPEN_DELAY_MS);
|
|
53
|
+
}, [clearCloseTimer, isHovering, isMobile, isPinned]);
|
|
54
|
+
const onHoverLeave = useCallback(() => {
|
|
55
|
+
if (isMobile || isPinned) return;
|
|
56
|
+
clearOpenTimer();
|
|
57
|
+
if (closeTimerRef.current !== null) return;
|
|
58
|
+
closeTimerRef.current = window.setTimeout(() => {
|
|
59
|
+
closeTimerRef.current = null;
|
|
60
|
+
setIsHovering(false);
|
|
61
|
+
}, HOVER_CLOSE_DELAY_MS);
|
|
62
|
+
}, [clearOpenTimer, isMobile, isPinned]);
|
|
63
|
+
const closeMobileDrawer = useCallback(() => {
|
|
64
|
+
setIsMobileOpen(false);
|
|
65
|
+
}, []);
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
const handleKeyDown = (event) => {
|
|
68
|
+
if ((event.ctrlKey || event.metaKey) && event.key === TOGGLE_SHORTCUT_KEY) {
|
|
69
|
+
event.preventDefault();
|
|
70
|
+
togglePrimary();
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
74
|
+
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
75
|
+
}, [togglePrimary]);
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
clearTimers();
|
|
78
|
+
setIsHovering(false);
|
|
79
|
+
setIsMobileOpen(false);
|
|
80
|
+
}, [isMobile, clearTimers]);
|
|
81
|
+
useEffect(() => clearTimers, [clearTimers]);
|
|
82
|
+
const isDocked = !isMobile && isPinned;
|
|
83
|
+
const isExpanded = isMobile ? isMobileOpen : isPinned || isHovering;
|
|
84
|
+
const isVisible = isMobile ? isMobileOpen : true;
|
|
85
|
+
const showRail = !isMobile && !isExpanded;
|
|
86
|
+
const isOverlayOnMobile = isMobile && isMobileOpen;
|
|
87
|
+
const willCollapse = isMobile ? isMobileOpen : isPinned;
|
|
88
|
+
const toggleLabelKey = willCollapse ? "sidebar.collapseSidebar" : "sidebar.openSidebar";
|
|
89
|
+
return {
|
|
90
|
+
isMobile,
|
|
91
|
+
isVisible,
|
|
92
|
+
isExpanded,
|
|
93
|
+
isDocked,
|
|
94
|
+
isOverlayOnMobile,
|
|
95
|
+
showRail,
|
|
96
|
+
toggleLabelKey,
|
|
97
|
+
togglePrimary,
|
|
98
|
+
onHoverEnter,
|
|
99
|
+
onHoverLeave,
|
|
100
|
+
closeMobileDrawer
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
export {
|
|
104
|
+
useSidebarState
|
|
105
|
+
};
|
|
106
|
+
//# sourceMappingURL=useSidebarState.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useSidebarState.js","sources":["../../../src/components/Shell/useSidebarState.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\n\nimport { readUiPreference, writeUiPreference } from '@/helpers/uiPreferences';\nimport { useMobile } from '@/hooks/useMobile';\n\nconst HOVER_OPEN_DELAY_MS = 250;\nconst HOVER_CLOSE_DELAY_MS = 250;\nconst PIN_STORAGE_KEY = 'sidebar_pinned';\nconst TOGGLE_SHORTCUT_KEY = '\\\\';\n\nconst readPinnedPreference = (): boolean => readUiPreference<boolean>(PIN_STORAGE_KEY) ?? false;\n\nexport interface ISidebarState {\n isMobile: boolean;\n isVisible: boolean;\n isExpanded: boolean;\n isDocked: boolean;\n isOverlayOnMobile: boolean;\n showRail: boolean;\n toggleLabelKey: string;\n togglePrimary: () => void;\n onHoverEnter: () => void;\n onHoverLeave: () => void;\n closeMobileDrawer: () => void;\n}\n\nexport const useSidebarState = (): ISidebarState => {\n const isMobile = useMobile();\n\n const [isPinned, setIsPinned] = useState<boolean>(readPinnedPreference);\n const [isHovering, setIsHovering] = useState(false);\n const [isMobileOpen, setIsMobileOpen] = useState(false);\n\n const openTimerRef = useRef<number | null>(null);\n const closeTimerRef = useRef<number | null>(null);\n\n const clearOpenTimer = useCallback(() => {\n if (openTimerRef.current !== null) {\n window.clearTimeout(openTimerRef.current);\n openTimerRef.current = null;\n }\n }, []);\n\n const clearCloseTimer = useCallback(() => {\n if (closeTimerRef.current !== null) {\n window.clearTimeout(closeTimerRef.current);\n closeTimerRef.current = null;\n }\n }, []);\n\n const clearTimers = useCallback(() => {\n clearOpenTimer();\n clearCloseTimer();\n }, [clearOpenTimer, clearCloseTimer]);\n\n const persistPinned = useCallback((next: boolean) => {\n setIsPinned(next);\n writeUiPreference(PIN_STORAGE_KEY, next);\n }, []);\n\n const togglePrimary = useCallback(() => {\n clearTimers();\n if (isMobile) {\n setIsMobileOpen((open) => !open);\n return;\n }\n setIsHovering(false);\n persistPinned(!isPinned);\n }, [clearTimers, isMobile, isPinned, persistPinned]);\n\n const onHoverEnter = useCallback(() => {\n if (isMobile || isPinned) return;\n clearCloseTimer();\n if (isHovering || openTimerRef.current !== null) return;\n openTimerRef.current = window.setTimeout(() => {\n openTimerRef.current = null;\n setIsHovering(true);\n }, HOVER_OPEN_DELAY_MS);\n }, [clearCloseTimer, isHovering, isMobile, isPinned]);\n\n const onHoverLeave = useCallback(() => {\n if (isMobile || isPinned) return;\n clearOpenTimer();\n if (closeTimerRef.current !== null) return;\n closeTimerRef.current = window.setTimeout(() => {\n closeTimerRef.current = null;\n setIsHovering(false);\n }, HOVER_CLOSE_DELAY_MS);\n }, [clearOpenTimer, isMobile, isPinned]);\n\n const closeMobileDrawer = useCallback(() => {\n setIsMobileOpen(false);\n }, []);\n\n useEffect(() => {\n const handleKeyDown = (event: KeyboardEvent) => {\n if ((event.ctrlKey || event.metaKey) && event.key === TOGGLE_SHORTCUT_KEY) {\n event.preventDefault();\n togglePrimary();\n }\n };\n\n window.addEventListener('keydown', handleKeyDown);\n return () => window.removeEventListener('keydown', handleKeyDown);\n }, [togglePrimary]);\n\n useEffect(() => {\n clearTimers();\n setIsHovering(false);\n setIsMobileOpen(false);\n }, [isMobile, clearTimers]);\n\n useEffect(() => clearTimers, [clearTimers]);\n\n const isDocked = !isMobile && isPinned;\n const isExpanded = isMobile ? isMobileOpen : isPinned || isHovering;\n const isVisible = isMobile ? isMobileOpen : true;\n const showRail = !isMobile && !isExpanded;\n const isOverlayOnMobile = isMobile && isMobileOpen;\n const willCollapse = isMobile ? isMobileOpen : isPinned;\n const toggleLabelKey = willCollapse ? 'sidebar.collapseSidebar' : 'sidebar.openSidebar';\n\n return {\n isMobile,\n isVisible,\n isExpanded,\n isDocked,\n isOverlayOnMobile,\n showRail,\n toggleLabelKey,\n togglePrimary,\n onHoverEnter,\n onHoverLeave,\n closeMobileDrawer,\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 {
|
|
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(
|
|
103
|
+
actionHref && /* @__PURE__ */ jsx(NavigationLink, { to: actionHref, children: /* @__PURE__ */ jsx(
|
|
103
104
|
ControlledButton,
|
|
104
105
|
{
|
|
105
106
|
defaultHtmlButton: true,
|