@umodeapporg/ui 0.3.0 → 0.4.1

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 (37) hide show
  1. package/dist/assets/umode-ui.css +148 -0
  2. package/dist/components/Shell/AppHeader.d.ts +19 -0
  3. package/dist/components/Shell/AppHeader.js +64 -0
  4. package/dist/components/Shell/AppHeader.js.map +1 -0
  5. package/dist/components/Shell/AppShell.d.ts +20 -0
  6. package/dist/components/Shell/AppShell.js +100 -0
  7. package/dist/components/Shell/AppShell.js.map +1 -0
  8. package/dist/components/Shell/AppSidebar.d.ts +15 -0
  9. package/dist/components/Shell/AppSidebar.js +63 -0
  10. package/dist/components/Shell/AppSidebar.js.map +1 -0
  11. package/dist/components/Shell/LanguageToggle.d.ts +8 -0
  12. package/dist/components/Shell/LanguageToggle.js +59 -0
  13. package/dist/components/Shell/LanguageToggle.js.map +1 -0
  14. package/dist/components/Shell/SidebarMenu.d.ts +24 -0
  15. package/dist/components/Shell/SidebarMenu.js +204 -0
  16. package/dist/components/Shell/SidebarMenu.js.map +1 -0
  17. package/dist/components/Shell/ThemeToggle.d.ts +11 -0
  18. package/dist/components/Shell/ThemeToggle.js +55 -0
  19. package/dist/components/Shell/ThemeToggle.js.map +1 -0
  20. package/dist/components/Shell/headerControlStyles.d.ts +10 -0
  21. package/dist/components/Shell/headerControlStyles.js +11 -0
  22. package/dist/components/Shell/headerControlStyles.js.map +1 -0
  23. package/dist/components/Shell/useSidebarState.d.ts +14 -0
  24. package/dist/components/Shell/useSidebarState.js +106 -0
  25. package/dist/components/Shell/useSidebarState.js.map +1 -0
  26. package/dist/index.d.ts +4 -0
  27. package/dist/index.js +44 -36
  28. package/dist/index.js.map +1 -1
  29. package/dist/locales/en/ui.json.d.ts +21 -0
  30. package/dist/locales/en/ui.json.js +9 -0
  31. package/dist/locales/en/ui.json.js.map +1 -1
  32. package/dist/locales/index.d.ts +42 -0
  33. package/dist/locales/pt/ui.json.d.ts +21 -0
  34. package/dist/locales/pt/ui.json.js +9 -0
  35. package/dist/locales/pt/ui.json.js.map +1 -1
  36. package/package.json +1 -1
  37. package/src/assets/styles/base.css +14 -2
@@ -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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"}
package/dist/index.d.ts CHANGED
@@ -16,6 +16,10 @@ export { default as PageTitle } from './components/Navigation/PageTitle';
16
16
  export * from './components/Navigation/PageTitleNavigationContext';
17
17
  export * from './components/Navigation/UiNavigationContext';
18
18
  export { default as UnifiedSearchBar } from './components/Search/UnifiedSearchBar';
19
+ export { default as AppShell } from './components/Shell/AppShell';
20
+ export { default as ShellLanguageToggle } from './components/Shell/LanguageToggle';
21
+ export * from './components/Shell/SidebarMenu';
22
+ export { default as ShellThemeToggle } from './components/Shell/ThemeToggle';
19
23
  export * from './components/UI/access/UiAccessContext';
20
24
  export { default as BaseDataCard } from './components/UI/BaseDataCard';
21
25
  export { default as OutlinedChip } from './components/UI/Chips/OutlinedChip';
package/dist/index.js CHANGED
@@ -16,26 +16,30 @@ import { default as default15 } from "./components/Navigation/PageTitle.js";
16
16
  import { PageTitleNavigationProvider, usePageTitleNavigation } from "./components/Navigation/PageTitleNavigationContext.js";
17
17
  import { UiNavigationProvider, useUiNavigation } from "./components/Navigation/UiNavigationContext.js";
18
18
  import { default as default16 } from "./components/Search/UnifiedSearchBar.js";
19
+ import { default as default17 } from "./components/Shell/AppShell.js";
20
+ import { default as default18 } from "./components/Shell/LanguageToggle.js";
21
+ import { resolveSidebarItemId } from "./components/Shell/SidebarMenu.js";
22
+ import { default as default19 } from "./components/Shell/ThemeToggle.js";
19
23
  import { UiAccessProvider, useUiAccess } from "./components/UI/access/UiAccessContext.js";
20
- import { default as default17 } from "./components/UI/BaseDataCard.js";
21
- import { default as default18 } from "./components/UI/Chips/OutlinedChip.js";
22
- import { default as default19 } from "./components/UI/Colors.js";
23
- import { default as default20 } from "./components/UI/ColorSwatch.js";
24
- import { default as default21 } from "./components/UI/ControlledAccordion.js";
25
- import { default as default22 } from "./components/UI/ControlledCollapse.js";
26
- import { default as default23 } from "./components/UI/ControlledMenu.js";
27
- import { default as default24 } from "./components/UI/ControlledPopover.js";
28
- import { default as default25 } from "./components/UI/ControlledProgress.js";
29
- import { default as default26 } from "./components/UI/ControlledSlider.js";
30
- import { default as default27 } from "./components/UI/ControlledTooltip.js";
31
- import { default as default28 } from "./components/UI/CustomBadge.js";
32
- import { default as default29 } from "./components/UI/CustomCard.js";
33
- import { default as default30 } from "./components/UI/InformationCard.js";
34
- import { default as default31 } from "./components/UI/LoadingSpinner.js";
24
+ import { default as default20 } from "./components/UI/BaseDataCard.js";
25
+ import { default as default21 } from "./components/UI/Chips/OutlinedChip.js";
26
+ import { default as default22 } from "./components/UI/Colors.js";
27
+ import { default as default23 } from "./components/UI/ColorSwatch.js";
28
+ import { default as default24 } from "./components/UI/ControlledAccordion.js";
29
+ import { default as default25 } from "./components/UI/ControlledCollapse.js";
30
+ import { default as default26 } from "./components/UI/ControlledMenu.js";
31
+ import { default as default27 } from "./components/UI/ControlledPopover.js";
32
+ import { default as default28 } from "./components/UI/ControlledProgress.js";
33
+ import { default as default29 } from "./components/UI/ControlledSlider.js";
34
+ import { default as default30 } from "./components/UI/ControlledTooltip.js";
35
+ import { default as default31 } from "./components/UI/CustomBadge.js";
36
+ import { default as default32 } from "./components/UI/CustomCard.js";
37
+ import { default as default33 } from "./components/UI/InformationCard.js";
38
+ import { default as default34 } from "./components/UI/LoadingSpinner.js";
35
39
  import { MaterialSymbol, combineClasses } from "./components/UI/MaterialSymbol/index.js";
36
- import { default as default32 } from "./components/UI/NoDataFound/index.js";
37
- import { default as default33 } from "./components/UI/SkeletonLoading.js";
38
- import { default as default34 } from "./components/UI/UserMenu.js";
40
+ import { default as default35 } from "./components/UI/NoDataFound/index.js";
41
+ import { default as default36 } from "./components/UI/SkeletonLoading.js";
42
+ import { default as default37 } from "./components/UI/UserMenu.js";
39
43
  import { composeRefs } from "./helpers/composeRefs.js";
40
44
  import { isPressOnLayeredSurface } from "./helpers/layeredSurfaces.js";
41
45
  import { readThemeToken, useThemeChartTokens } from "./helpers/themeTokens.js";
@@ -52,46 +56,49 @@ import { uiLocales } from "./locales/index.js";
52
56
  import { collapseVariants, createDrawerVariants, dialogVariants, fadeVariants, floatingVariants, scrimVariants, tooltipVariants } from "./motion/motionPresets.js";
53
57
  import { MOTION_OFFSET_PX, enterTransition, exitTransition, motionDuration, motionEase } from "./motion/motionTokens.js";
54
58
  export {
59
+ default17 as AppShell,
55
60
  BREAKPOINTS,
56
- default17 as BaseDataCard,
57
- default20 as ColorSwatch,
58
- default19 as Colors,
59
- default21 as ControlledAccordion,
61
+ default20 as BaseDataCard,
62
+ default23 as ColorSwatch,
63
+ default22 as Colors,
64
+ default24 as ControlledAccordion,
60
65
  default13 as ControlledBreadcrumb,
61
66
  default2 as ControlledButton,
62
67
  default3 as ControlledCheckbox,
63
- default22 as ControlledCollapse,
68
+ default25 as ControlledCollapse,
64
69
  default4 as ControlledDialog,
65
70
  default5 as ControlledDrawer,
66
71
  default6 as ControlledImage,
67
72
  default7 as ControlledInput,
68
- default23 as ControlledMenu,
69
- default24 as ControlledPopover,
70
- default25 as ControlledProgress,
73
+ default26 as ControlledMenu,
74
+ default27 as ControlledPopover,
75
+ default28 as ControlledProgress,
71
76
  default8 as ControlledRadio,
72
77
  ControlledSelect,
73
- default26 as ControlledSlider,
78
+ default29 as ControlledSlider,
74
79
  default9 as ControlledSwitch,
75
80
  default10 as ControlledTabs,
76
81
  default11 as ControlledTextarea,
77
- default27 as ControlledTooltip,
82
+ default30 as ControlledTooltip,
78
83
  default12 as ControlledViewToggle,
79
- default28 as CustomBadge,
80
- default29 as CustomCard,
84
+ default31 as CustomBadge,
85
+ default32 as CustomCard,
81
86
  default14 as ErrorPage,
82
- default30 as InformationCard,
83
- default31 as LoadingSpinner,
87
+ default33 as InformationCard,
88
+ default34 as LoadingSpinner,
84
89
  MOTION_OFFSET_PX,
85
90
  MaterialSymbol,
86
- default32 as NoDataFound,
87
- default18 as OutlinedChip,
91
+ default35 as NoDataFound,
92
+ default21 as OutlinedChip,
88
93
  default15 as PageTitle,
89
94
  PageTitleNavigationProvider,
90
- default33 as SkeletonLoading,
95
+ default18 as ShellLanguageToggle,
96
+ default19 as ShellThemeToggle,
97
+ default36 as SkeletonLoading,
91
98
  UiAccessProvider,
92
99
  UiNavigationProvider,
93
100
  default16 as UnifiedSearchBar,
94
- default34 as UserMenu,
101
+ default37 as UserMenu,
95
102
  collapseVariants,
96
103
  combineClasses,
97
104
  composeRefs,
@@ -108,6 +115,7 @@ export {
108
115
  motionEase,
109
116
  readThemeToken,
110
117
  readUiPreference,
118
+ resolveSidebarItemId,
111
119
  resolveUiPreferenceScopeId,
112
120
  scrimVariants,
113
121
  tooltipVariants,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}