@olenbetong/synergi-react 2.4.0 → 2.5.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.
@@ -0,0 +1,158 @@
1
+ /* App Menu – trigger button */
2
+ .app-menu-trigger {
3
+ display: inline-flex;
4
+ align-items: center;
5
+ justify-content: center;
6
+ padding: var(--ds-spacing-2);
7
+ background: transparent;
8
+ border: none;
9
+ border-radius: var(--ds-border-radius-md);
10
+ color: var(--ds-color-neutral-text-default);
11
+ cursor: pointer;
12
+ line-height: 1;
13
+ transition: background 0.15s ease;
14
+ }
15
+
16
+ .app-menu-trigger:hover {
17
+ background: var(--ds-color-neutral-background-hover);
18
+ }
19
+
20
+ .app-menu-trigger:focus-visible {
21
+ outline: 2px solid var(--ds-color-focus-outer);
22
+ outline-offset: 2px;
23
+ }
24
+
25
+ .app-menu-icon {
26
+ display: block;
27
+ }
28
+
29
+ /* Dialog backdrop */
30
+ .app-menu-dialog::backdrop {
31
+ background: rgba(0, 0, 0, 0.4);
32
+ }
33
+
34
+ /* Dialog container */
35
+ .app-menu-dialog {
36
+ padding: 0;
37
+ border: 1px solid var(--ds-color-neutral-border-default);
38
+ border-radius: var(--ds-border-radius-lg);
39
+ background: var(--ds-color-neutral-background-default);
40
+ box-shadow: var(--ds-shadow-lg, 0 8px 32px rgba(0, 0, 0, 0.18));
41
+ max-width: min(90vw, 36rem);
42
+ width: 100%;
43
+ max-height: min(90vh, 40rem);
44
+ overflow: hidden;
45
+ display: flex;
46
+ flex-direction: column;
47
+ }
48
+
49
+ /* Inner wrapper – scrollable */
50
+ .app-menu-dialog-inner {
51
+ display: flex;
52
+ flex-direction: column;
53
+ gap: 0;
54
+ padding: var(--ds-spacing-4);
55
+ overflow: hidden;
56
+ height: 100%;
57
+ }
58
+
59
+ /* Search row */
60
+ .app-menu-search-row {
61
+ margin-bottom: var(--ds-spacing-3);
62
+ }
63
+
64
+ .app-menu-search {
65
+ width: 100%;
66
+ padding: var(--ds-spacing-2) var(--ds-spacing-3);
67
+ font-size: var(--ds-font-size-md);
68
+ border: 1px solid var(--ds-color-neutral-border-strong);
69
+ border-radius: var(--ds-border-radius-md);
70
+ background: var(--ds-color-neutral-background-subtle);
71
+ color: var(--ds-color-neutral-text-default);
72
+ box-sizing: border-box;
73
+ }
74
+
75
+ .app-menu-search:focus {
76
+ outline: 2px solid var(--ds-color-focus-outer);
77
+ outline-offset: 2px;
78
+ border-color: var(--ds-color-accent-border-strong);
79
+ }
80
+
81
+ /* Loading / empty states */
82
+ .app-menu-loading,
83
+ .app-menu-empty {
84
+ text-align: center;
85
+ color: var(--ds-color-neutral-text-subtle);
86
+ font-size: var(--ds-font-size-sm);
87
+ padding: var(--ds-spacing-4) 0;
88
+ margin: 0;
89
+ }
90
+
91
+ /* Groups scroll area */
92
+ .app-menu-groups {
93
+ overflow-y: auto;
94
+ flex: 1 1 auto;
95
+ display: flex;
96
+ flex-direction: column;
97
+ gap: var(--ds-spacing-4);
98
+ }
99
+
100
+ /* Individual group */
101
+ .app-menu-group {
102
+ display: flex;
103
+ flex-direction: column;
104
+ gap: var(--ds-spacing-1);
105
+ }
106
+
107
+ .app-menu-group-title {
108
+ font-size: var(--ds-font-size-xs);
109
+ font-weight: var(--ds-font-weight-medium, 600);
110
+ text-transform: uppercase;
111
+ letter-spacing: 0.06em;
112
+ color: var(--ds-color-neutral-text-subtle);
113
+ margin: 0 0 var(--ds-spacing-1);
114
+ padding: 0;
115
+ }
116
+
117
+ /* App list */
118
+ .app-menu-list {
119
+ list-style: none;
120
+ margin: 0;
121
+ padding: 0;
122
+ display: grid;
123
+ grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
124
+ gap: var(--ds-spacing-1);
125
+ }
126
+
127
+ .app-menu-item {
128
+ display: contents;
129
+ }
130
+
131
+ /* App link tiles */
132
+ .app-menu-link {
133
+ display: flex;
134
+ align-items: center;
135
+ padding: var(--ds-spacing-2) var(--ds-spacing-3);
136
+ border-radius: var(--ds-border-radius-sm);
137
+ font-size: var(--ds-font-size-sm);
138
+ color: var(--ds-color-neutral-text-default);
139
+ text-decoration: none;
140
+ background: var(--ds-color-neutral-background-subtle);
141
+ border: 1px solid transparent;
142
+ transition:
143
+ background 0.12s ease,
144
+ border-color 0.12s ease;
145
+ word-break: break-word;
146
+ line-height: 1.4;
147
+ }
148
+
149
+ .app-menu-link:hover {
150
+ background: var(--ds-color-accent-background-hover, var(--ds-color-neutral-background-hover));
151
+ border-color: var(--ds-color-accent-border-default, var(--ds-color-neutral-border-default));
152
+ color: var(--ds-color-accent-text-default, var(--ds-color-neutral-text-default));
153
+ }
154
+
155
+ .app-menu-link:focus-visible {
156
+ outline: 2px solid var(--ds-color-focus-outer);
157
+ outline-offset: 2px;
158
+ }
@@ -0,0 +1,12 @@
1
+ import "./AppMenu.css";
2
+ import type { DataObject } from "@olenbetong/appframe-data";
3
+ export type AppMenuArticle = {
4
+ ArticleId: string;
5
+ Title: string | null;
6
+ MenuGroup: string | null;
7
+ Path: string | null;
8
+ };
9
+ export type AppMenuProps = {
10
+ dataObject: DataObject<AppMenuArticle>;
11
+ };
12
+ export declare function AppMenu({ dataObject }: AppMenuProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,85 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import "./AppMenu.css";
3
+ import { useData, useLoading } from "@olenbetong/appframe-react";
4
+ import { useEffect, useMemo, useRef, useState } from "react";
5
+ /** Strip version suffixes from ArticleId for building hrefs. */
6
+ function stripVersionSuffix(articleId) {
7
+ if (articleId.length > 9 && articleId[articleId.length - 8] === ".") {
8
+ return articleId.slice(0, -8);
9
+ }
10
+ if (articleId.length > 5 && articleId[articleId.length - 4] === ".") {
11
+ return articleId.slice(0, -4);
12
+ }
13
+ return articleId;
14
+ }
15
+ /** Group records by MenuGroup, returning an ordered list of [group, items] pairs. */
16
+ function groupByMenuGroup(items) {
17
+ let map = new Map();
18
+ for (let item of items) {
19
+ let key = item.MenuGroup ?? "";
20
+ let list = map.get(key);
21
+ if (!list) {
22
+ list = [];
23
+ map.set(key, list);
24
+ }
25
+ list.push(item);
26
+ }
27
+ return [...map.entries()];
28
+ }
29
+ export function AppMenu({ dataObject }) {
30
+ let dialogRef = useRef(null);
31
+ let searchRef = useRef(null);
32
+ let [search, setSearch] = useState("");
33
+ let [isOpen, setIsOpen] = useState(false);
34
+ let isLoading = useLoading(dataObject);
35
+ let allArticles = useData(dataObject);
36
+ // Refresh data when opened for the first time
37
+ useEffect(() => {
38
+ if (isOpen && allArticles.length === 0 && !isLoading) {
39
+ dataObject.refreshDataSource();
40
+ }
41
+ }, [isOpen, allArticles.length, isLoading, dataObject]);
42
+ let filtered = useMemo(() => {
43
+ if (!search.trim())
44
+ return allArticles;
45
+ let lower = search.toLowerCase();
46
+ return allArticles.filter((a) => (a.Title ?? "").toLowerCase().includes(lower));
47
+ }, [allArticles, search]);
48
+ let groups = useMemo(() => groupByMenuGroup(filtered), [filtered]);
49
+ function openDialog() {
50
+ let dialog = dialogRef.current;
51
+ if (!dialog)
52
+ return;
53
+ setIsOpen(true);
54
+ dialog.showModal();
55
+ // Focus search after dialog opens
56
+ requestAnimationFrame(() => searchRef.current?.focus());
57
+ }
58
+ function closeDialog() {
59
+ let dialog = dialogRef.current;
60
+ if (!dialog)
61
+ return;
62
+ setSearch("");
63
+ setIsOpen(false);
64
+ dialog.close();
65
+ }
66
+ function handleBackdropClick(e) {
67
+ // Close when clicking outside the dialog content (on the backdrop)
68
+ let rect = dialogRef.current?.getBoundingClientRect();
69
+ if (!rect)
70
+ return;
71
+ let { clientX, clientY } = e;
72
+ if (clientX < rect.left || clientX > rect.right || clientY < rect.top || clientY > rect.bottom) {
73
+ closeDialog();
74
+ }
75
+ }
76
+ // Native <dialog> handles Escape automatically — just sync state
77
+ function handleCancel() {
78
+ setSearch("");
79
+ setIsOpen(false);
80
+ }
81
+ return (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "app-menu-trigger", "aria-label": "Open app menu", "aria-haspopup": "dialog", "aria-expanded": isOpen, onClick: openDialog, children: _jsx("svg", { className: "app-menu-icon", xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "currentColor", "aria-hidden": "true", focusable: "false", width: "24", height: "24", children: _jsx("path", { d: "M4 6h4V4H4v2zm0 5h4V9H4v2zm0 5h4v-2H4v2zM10 4v2h10V4H10zm0 7h10V9H10v2zm0 5h10v-2H10v2z" }) }) }), _jsx("dialog", { ref: dialogRef, className: "app-menu-dialog", "aria-label": "App menu", onClick: handleBackdropClick, onCancel: handleCancel, children: _jsxs("div", { className: "app-menu-dialog-inner", children: [_jsxs("div", { className: "app-menu-search-row", children: [_jsx("label", { htmlFor: "app-menu-search", className: "visually-hidden", children: "Search apps" }), _jsx("input", { ref: searchRef, id: "app-menu-search", type: "search", className: "app-menu-search", placeholder: "Search apps\u2026", value: search, onChange: (e) => setSearch(e.target.value), autoComplete: "off" })] }), isLoading && _jsx("p", { className: "app-menu-loading", children: "Loading\u2026" }), !isLoading && groups.length === 0 && _jsx("p", { className: "app-menu-empty", children: "No apps found." }), _jsx("div", { className: "app-menu-groups", children: groups.map(([group, items]) => (_jsxs("section", { className: "app-menu-group", children: [group && _jsx("h2", { className: "app-menu-group-title", children: group }), _jsx("ul", { className: "app-menu-list", children: items.map((article) => {
82
+ let href = `/${stripVersionSuffix(article.ArticleId)}`;
83
+ return (_jsx("li", { className: "app-menu-item", children: _jsx("a", { href: href, className: "app-menu-link", onClick: closeDialog, children: article.Title ?? article.ArticleId }) }, article.ArticleId));
84
+ }) })] }, group))) })] }) })] }));
85
+ }
@@ -0,0 +1,5 @@
1
+ import { type ReactNode } from "react";
2
+ export type AppShellProps = {
3
+ children: ReactNode;
4
+ };
5
+ export declare function AppShell({ children }: AppShellProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,112 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { StyledEngineProvider, ThemeProvider } from "@mui/material";
3
+ import { createTheme as muiCreateTheme } from "@mui/material/styles";
4
+ import { AppErrorBoundary } from "@olenbetong/appframe-mui";
5
+ import { generateApiDataObject, SortOrder } from "@olenbetong/appframe-data";
6
+ import { useEffect, useState } from "react";
7
+ import { createPortal } from "react-dom";
8
+ import { AppMenu } from "../AppMenu/index.js";
9
+ // Created once — shared across all AppShell instances on a page.
10
+ const dsMenuArticles = generateApiDataObject({
11
+ resource: "aviw_WebShared_Articles",
12
+ id: "dsMenuArticles",
13
+ allowUpdate: false,
14
+ allowInsert: false,
15
+ allowDelete: false,
16
+ dynamicLoading: false,
17
+ fields: [
18
+ { name: "ArticleId", type: "string", nullable: false },
19
+ { name: "Title", type: "string", nullable: true },
20
+ { name: "MenuGroup", type: "string", nullable: true },
21
+ { name: "Path", type: "string", nullable: true },
22
+ ],
23
+ parameters: {
24
+ maxRecords: -1,
25
+ whereClause: "ListOnFrontPage = 1 AND SysApp = 0 AND HideFromNav = 0 AND (IsPublishedVersion IS NULL OR IsPublishedVersion = 1) AND ISNULL(ParentArticleId, '') <> 'news'",
26
+ sortOrder: [{ MenuGroup: SortOrder.Asc }, { Title: SortOrder.Asc }],
27
+ },
28
+ });
29
+ function buildMuiTheme() {
30
+ const s = getComputedStyle(document.body);
31
+ const get = (v) => s.getPropertyValue(v).trim();
32
+ return muiCreateTheme({
33
+ palette: {
34
+ primary: {
35
+ main: get("--ds-color-brand-base-default") || "#116069",
36
+ dark: get("--ds-color-brand-base-hover") || "#0d4b52",
37
+ contrastText: get("--ds-color-brand-base-contrast-default") || "#ffffff",
38
+ },
39
+ secondary: {
40
+ main: get("--ds-color-brand-light-base-default") || "#1a95a2",
41
+ },
42
+ },
43
+ typography: {
44
+ fontFamily: [
45
+ "-apple-system",
46
+ "BlinkMacSystemFont",
47
+ '"Segoe UI"',
48
+ "Roboto",
49
+ '"Helvetica Neue"',
50
+ "Arial",
51
+ "sans-serif",
52
+ '"Apple Color Emoji"',
53
+ '"Segoe UI Emoji"',
54
+ '"Segoe UI Symbol"',
55
+ ].join(","),
56
+ },
57
+ components: {
58
+ MuiTypography: {
59
+ styleOverrides: {
60
+ h1: { fontSize: "2.488rem" },
61
+ h2: { fontSize: "2.074rem" },
62
+ h3: { fontSize: "1.728rem" },
63
+ h4: { fontSize: "1.44rem" },
64
+ h5: { fontSize: "1.2rem" },
65
+ h6: { fontSize: "1rem" },
66
+ },
67
+ },
68
+ MuiAppBar: {
69
+ styleOverrides: {
70
+ root: {
71
+ "& .MuiTypography-h1": {
72
+ fontSize: "1.25rem",
73
+ fontWeight: "500",
74
+ },
75
+ },
76
+ },
77
+ },
78
+ MuiButton: {
79
+ styleOverrides: {
80
+ root: { borderRadius: 2 },
81
+ },
82
+ },
83
+ MuiDialogTitle: {
84
+ styleOverrides: {
85
+ root: {
86
+ fontSize: "1.2rem",
87
+ fontWeight: 500,
88
+ paddingLeft: 16,
89
+ paddingRight: 16,
90
+ },
91
+ },
92
+ },
93
+ MuiFormLabel: {
94
+ styleOverrides: {
95
+ root: { color: "rgba(0, 0, 0, 0.72)" },
96
+ },
97
+ },
98
+ MuiTextField: { defaultProps: { variant: "filled" } },
99
+ MuiSelect: { defaultProps: { variant: "filled" } },
100
+ MuiFormControl: { defaultProps: { variant: "filled" } },
101
+ },
102
+ });
103
+ }
104
+ export function AppShell({ children }) {
105
+ const [theme, setTheme] = useState(() => buildMuiTheme());
106
+ // Re-read tokens after mount so CSS variables are fully resolved.
107
+ useEffect(() => {
108
+ setTheme(buildMuiTheme());
109
+ }, []);
110
+ const appMenuSlot = document.getElementById("ob-toolbar-app-menu");
111
+ return (_jsx(StyledEngineProvider, { injectFirst: true, children: _jsx(ThemeProvider, { theme: theme, children: _jsxs(AppErrorBoundary, { children: [appMenuSlot && createPortal(_jsx(AppMenu, { dataObject: dsMenuArticles }), appMenuSlot), children] }) }) }));
112
+ }
@@ -0,0 +1,124 @@
1
+ .ob-breadcrumbs {
2
+ display: flex;
3
+ align-items: center;
4
+ overflow: hidden;
5
+ width: 100%;
6
+ height: 100%;
7
+ }
8
+
9
+ .ob-breadcrumbs__list {
10
+ display: flex;
11
+ align-items: center;
12
+ list-style: none;
13
+ margin: 0;
14
+ padding: 0;
15
+ gap: var(--ds-space-100, 0.25rem);
16
+ flex-wrap: nowrap;
17
+ overflow: hidden;
18
+ }
19
+
20
+ .ob-breadcrumbs__item {
21
+ display: flex;
22
+ align-items: center;
23
+ font-size: var(--ds-font-size-100, 0.875rem);
24
+ white-space: nowrap;
25
+ color: var(--ds-text-subtle, currentColor);
26
+
27
+ a {
28
+ color: var(--ds-link-default, var(--ds-text-subtle, currentColor));
29
+ text-decoration: none;
30
+
31
+ &:hover {
32
+ text-decoration: underline;
33
+ color: var(--ds-link-default-hovered, var(--ds-text, currentColor));
34
+ }
35
+
36
+ &:focus-visible {
37
+ outline: 2px solid var(--ds-border-focused, currentColor);
38
+ outline-offset: 2px;
39
+ border-radius: var(--ds-border-radius-100, 2px);
40
+ }
41
+ }
42
+
43
+ &:not(:last-child)::after {
44
+ content: "/";
45
+ margin-inline-start: var(--ds-space-100, 0.25rem);
46
+ color: var(--ds-text-disabled, currentColor);
47
+ opacity: 0.5;
48
+ }
49
+
50
+ span[aria-current="page"] {
51
+ color: var(--ds-text, currentColor);
52
+ font-weight: var(--ds-font-weight-semibold, 500);
53
+ }
54
+ }
55
+
56
+ .ob-breadcrumbs__overflow {
57
+ position: relative;
58
+
59
+ button {
60
+ background: none;
61
+ border: 1px solid transparent;
62
+ border-radius: var(--ds-border-radius-100, 2px);
63
+ color: var(--ds-text-subtle, currentColor);
64
+ cursor: pointer;
65
+ font-size: var(--ds-font-size-100, 0.875rem);
66
+ line-height: 1;
67
+ padding: var(--ds-space-050, 0.125rem) var(--ds-space-150, 0.375rem);
68
+
69
+ &:hover {
70
+ background: var(--ds-background-neutral-default, rgba(0, 0, 0, 0.06));
71
+ border-color: var(--ds-border-default, transparent);
72
+ color: var(--ds-text, currentColor);
73
+ }
74
+
75
+ &:focus-visible {
76
+ outline: 2px solid var(--ds-border-focused, currentColor);
77
+ outline-offset: 2px;
78
+ }
79
+ }
80
+
81
+ &::after {
82
+ content: "/";
83
+ margin-inline-start: var(--ds-space-100, 0.25rem);
84
+ color: var(--ds-text-disabled, currentColor);
85
+ opacity: 0.5;
86
+ }
87
+ }
88
+
89
+ .ob-breadcrumbs__overflow-menu {
90
+ background: var(--ds-surface-overlay, #fff);
91
+ border: 1px solid var(--ds-border-default, currentColor);
92
+ border-radius: var(--ds-border-radius-100, 4px);
93
+ box-shadow: var(--ds-shadow-overlay, 0 4px 8px rgba(0, 0, 0, 0.16));
94
+ left: 0;
95
+ list-style: none;
96
+ margin: var(--ds-space-050, 0.25rem) 0 0;
97
+ min-width: 10rem;
98
+ padding: var(--ds-space-100, 0.25rem) 0;
99
+ position: absolute;
100
+ top: 100%;
101
+ z-index: var(--ds-z-index-modal, 400);
102
+
103
+ li {
104
+ a,
105
+ span {
106
+ color: var(--ds-text, inherit);
107
+ display: block;
108
+ font-size: var(--ds-font-size-100, 0.875rem);
109
+ padding: var(--ds-space-100, 0.25rem) var(--ds-space-200, 0.5rem);
110
+ text-decoration: none;
111
+ white-space: nowrap;
112
+
113
+ &:hover {
114
+ background: var(--ds-background-neutral-default, rgba(0, 0, 0, 0.06));
115
+ color: var(--ds-text, inherit);
116
+ }
117
+ }
118
+
119
+ a:focus-visible {
120
+ outline: 2px solid var(--ds-border-focused, currentColor);
121
+ outline-offset: -2px;
122
+ }
123
+ }
124
+ }
@@ -0,0 +1,15 @@
1
+ import "./Toolbar.css";
2
+ export type ToolbarPortalSlot = "breadcrumbs" | "search" | "actions";
3
+ export type ToolbarPortalProps = {
4
+ slot: ToolbarPortalSlot;
5
+ children: React.ReactNode;
6
+ };
7
+ export declare function ToolbarPortal({ slot, children }: ToolbarPortalProps): import("react").ReactPortal | null;
8
+ export type BreadcrumbItem = {
9
+ label: string;
10
+ href?: string;
11
+ };
12
+ export type ToolbarBreadcrumbsProps = {
13
+ items: BreadcrumbItem[];
14
+ };
15
+ export declare function ToolbarBreadcrumbs({ items }: ToolbarBreadcrumbsProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,64 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import "./Toolbar.css";
3
+ import { useEffect, useLayoutEffect, useRef, useState } from "react";
4
+ import { createPortal } from "react-dom";
5
+ export function ToolbarPortal({ slot, children }) {
6
+ // Target element IDs: ob-toolbar-breadcrumbs, ob-toolbar-search, ob-toolbar-actions
7
+ const target = document.getElementById(`ob-toolbar-${slot}`);
8
+ if (!target)
9
+ return null;
10
+ return createPortal(children, target);
11
+ }
12
+ function BreadcrumbsInner({ items }) {
13
+ let [firstVisible, setFirstVisible] = useState(0);
14
+ let [overflowOpen, setOverflowOpen] = useState(false);
15
+ let navRef = useRef(null);
16
+ let listRef = useRef(null);
17
+ let overflowItemRef = useRef(null);
18
+ // Detect overflow: increment firstVisible until the list fits within the nav.
19
+ // Runs after every render so it keeps pushing items into overflow until the
20
+ // list stops overflowing or only the last item (current page) remains.
21
+ useLayoutEffect(() => {
22
+ let nav = navRef.current;
23
+ let list = listRef.current;
24
+ if (!nav || !list)
25
+ return;
26
+ if (list.scrollWidth > nav.clientWidth && firstVisible < items.length - 1) {
27
+ setFirstVisible((prev) => prev + 1);
28
+ }
29
+ });
30
+ // ResizeObserver resets firstVisible to 0 on container resize so that the
31
+ // overflow calculation starts fresh at the new container width.
32
+ useEffect(() => {
33
+ let nav = navRef.current;
34
+ if (!nav)
35
+ return;
36
+ let observer = new ResizeObserver(() => {
37
+ setFirstVisible(0);
38
+ });
39
+ observer.observe(nav);
40
+ return () => observer.disconnect();
41
+ }, []);
42
+ // Close the overflow dropdown when the user clicks outside of it.
43
+ useEffect(() => {
44
+ if (!overflowOpen)
45
+ return;
46
+ function handleClickOutside(event) {
47
+ if (overflowItemRef.current && !overflowItemRef.current.contains(event.target)) {
48
+ setOverflowOpen(false);
49
+ }
50
+ }
51
+ document.addEventListener("mousedown", handleClickOutside);
52
+ return () => document.removeEventListener("mousedown", handleClickOutside);
53
+ }, [overflowOpen]);
54
+ let hiddenItems = items.slice(0, firstVisible);
55
+ let visibleItems = items.slice(firstVisible);
56
+ return (_jsx("nav", { className: "ob-breadcrumbs", "aria-label": "Breadcrumb", ref: navRef, children: _jsxs("ol", { className: "ob-breadcrumbs__list", ref: listRef, children: [firstVisible > 0 && (_jsxs("li", { className: "ob-breadcrumbs__item ob-breadcrumbs__overflow", ref: overflowItemRef, children: [_jsx("button", { type: "button", "aria-label": "Show hidden breadcrumbs", "aria-expanded": overflowOpen, "aria-haspopup": "true", onClick: () => setOverflowOpen((prev) => !prev), children: "\u2026" }), overflowOpen && (_jsx("ul", { className: "ob-breadcrumbs__overflow-menu", role: "menu", children: hiddenItems.map((item, index) => (_jsx("li", { role: "menuitem", children: item.href ? _jsx("a", { href: item.href, children: item.label }) : _jsx("span", { children: item.label }) }, index))) }))] })), visibleItems.map((item, index) => {
57
+ let actualIndex = firstVisible + index;
58
+ let isCurrent = actualIndex === items.length - 1;
59
+ return (_jsx("li", { className: "ob-breadcrumbs__item", children: isCurrent ? (_jsx("span", { "aria-current": "page", children: item.label })) : item.href ? (_jsx("a", { href: item.href, children: item.label })) : (_jsx("span", { children: item.label })) }, actualIndex));
60
+ })] }) }));
61
+ }
62
+ export function ToolbarBreadcrumbs({ items }) {
63
+ return (_jsx(ToolbarPortal, { slot: "breadcrumbs", children: _jsx(BreadcrumbsInner, { items: items }) }));
64
+ }
package/es/index.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ export { AppMenu, type AppMenuArticle, type AppMenuProps } from "./components/AppMenu/index.js";
2
+ export { AppShell, type AppShellProps } from "./components/AppShell/index.js";
1
3
  export { Checkbox, type CheckboxProps } from "./components/Checkbox/index.js";
2
4
  export { CardColumn, CardDetail, CardLabel, CardStep, CardStepList, ColorCard } from "./components/ColorCard/index.js";
3
5
  export { DateNavigator } from "./components/DateNavigator/index.js";
@@ -8,6 +10,7 @@ export { MergeProgressDialog, type MergeProgressDialogProps } from "./components
8
10
  export { BannerImage as PageBannerImage, type BannerImageProps, PageBanner, type PageBannerProps, Tab as PageBannerTab, TabList as PageBannerTabList, type TabListProps as PageBannerTabListProps, type TabProps as PageBannerTabProps, Title as PageBannerTitle, type TitleProps as PageBannerTitleProps, } from "./components/PageBanner/index.js";
9
11
  export * from "./components/PdfViewer/index.js";
10
12
  export { Portal } from "./components/Portal/index.js";
13
+ export { ToolbarBreadcrumbs, ToolbarPortal, type BreadcrumbItem, type ToolbarBreadcrumbsProps, type ToolbarPortalProps, type ToolbarPortalSlot, } from "./components/Toolbar/index.js";
11
14
  export { ProgressBar } from "./components/ProgressBar/index.js";
12
15
  export { Sidebar, type SidebarProps } from "./components/Sidebar/index.js";
13
16
  export { Spinner, type SpinnerProps } from "./components/Spinner/index.js";
package/es/index.js CHANGED
@@ -1,3 +1,5 @@
1
+ export { AppMenu } from "./components/AppMenu/index.js";
2
+ export { AppShell } from "./components/AppShell/index.js";
1
3
  export { Checkbox } from "./components/Checkbox/index.js";
2
4
  export { CardColumn, CardDetail, CardLabel, CardStep, CardStepList, ColorCard } from "./components/ColorCard/index.js";
3
5
  export { DateNavigator } from "./components/DateNavigator/index.js";
@@ -8,6 +10,7 @@ export { MergeProgressDialog } from "./components/MergeProgressDialog/index.js";
8
10
  export { BannerImage as PageBannerImage, PageBanner, Tab as PageBannerTab, TabList as PageBannerTabList, Title as PageBannerTitle, } from "./components/PageBanner/index.js";
9
11
  export * from "./components/PdfViewer/index.js";
10
12
  export { Portal } from "./components/Portal/index.js";
13
+ export { ToolbarBreadcrumbs, ToolbarPortal, } from "./components/Toolbar/index.js";
11
14
  export { ProgressBar } from "./components/ProgressBar/index.js";
12
15
  export { Sidebar } from "./components/Sidebar/index.js";
13
16
  export { Spinner } from "./components/Spinner/index.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@olenbetong/synergi-react",
3
- "version": "2.4.0",
3
+ "version": "2.5.1",
4
4
  "description": "Standalone React component for SynergiWeb and Partner Portal",
5
5
  "type": "module",
6
6
  "exports": {
@@ -22,6 +22,7 @@
22
22
  "author": "Bjørnar Vister Hansen <bvh@olenbetong.no>",
23
23
  "license": "MIT",
24
24
  "peerDependencies": {
25
+ "@mui/material": ">=9.0.0",
25
26
  "react": ">=18.2.0",
26
27
  "react-dom": ">=18.2.0"
27
28
  },
@@ -30,7 +31,7 @@
30
31
  "@emotion/styled": "^11.14.1",
31
32
  "clsx": "^2.1.1",
32
33
  "dynamsoft-javascript-barcode": "6.5.4",
33
- "filepond": "^4.32.9",
34
+ "filepond": "^4.32.12",
34
35
  "filepond-plugin-file-poster": "^2.5.2",
35
36
  "filepond-plugin-image-exif-orientation": "^1.0.11",
36
37
  "filepond-plugin-image-preview": "^4.6.12",
@@ -39,11 +40,12 @@
39
40
  "localforage": "^1.10.0",
40
41
  "mitt": "^3.0.1",
41
42
  "pdf-lib": "^1.17.1",
42
- "pdfjs-dist": "5.4.296",
43
- "react-pdf": "^10.2.0",
43
+ "pdfjs-dist": "5.7.284",
44
+ "react-pdf": "^10.4.1",
44
45
  "@olenbetong/appframe-core": "2.11.10",
46
+ "@olenbetong/appframe-data": "1.5.0",
45
47
  "@olenbetong/appframe-react": "1.21.25",
46
- "@olenbetong/appframe-mui": "6.1.0"
48
+ "@olenbetong/appframe-mui": "6.2.0"
47
49
  },
48
50
  "repository": {
49
51
  "type": "git",
@@ -57,8 +59,8 @@
57
59
  ]
58
60
  },
59
61
  "devDependencies": {
60
- "react": "19.2.5",
61
- "react-dom": "19.2.5",
62
+ "react": "19.2.6",
63
+ "react-dom": "19.2.6",
62
64
  "replace-in-files": "^3.0.0"
63
65
  },
64
66
  "optionalDependencies": {},