@olenbetong/appframe-ds 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/es/AfLookup/BoundLookup.d.ts +9 -0
- package/es/AfLookup/BoundLookup.js +16 -0
- package/es/AfLookup/Combobox.css +78 -0
- package/es/AfLookup/Combobox.d.ts +15 -0
- package/es/AfLookup/Combobox.js +124 -0
- package/es/AfLookup/Lookup.css +104 -0
- package/es/AfLookup/Lookup.d.ts +34 -0
- package/es/AfLookup/Lookup.js +161 -0
- package/es/AfLookup/index.d.ts +3 -0
- package/es/AfLookup/index.js +3 -0
- package/es/binding/BoundCheckbox.d.ts +9 -0
- package/es/binding/BoundCheckbox.js +13 -0
- package/es/binding/BoundDatePicker.d.ts +7 -0
- package/es/binding/BoundDatePicker.js +12 -0
- package/es/binding/BoundDateTimePicker.d.ts +7 -0
- package/es/binding/BoundDateTimePicker.js +14 -0
- package/es/binding/BoundInput.d.ts +6 -0
- package/es/binding/BoundInput.js +12 -0
- package/es/binding/BoundNumericTextField.d.ts +20 -0
- package/es/binding/BoundNumericTextField.js +58 -0
- package/es/binding/BoundSelect.d.ts +9 -0
- package/es/binding/BoundSelect.js +13 -0
- package/es/binding/BoundSwitch.d.ts +9 -0
- package/es/binding/BoundSwitch.js +13 -0
- package/es/binding/BoundTextField.d.ts +7 -0
- package/es/binding/BoundTextField.js +22 -0
- package/es/binding/CancelButton.d.ts +3 -0
- package/es/binding/CancelButton.js +14 -0
- package/es/binding/CancelIconButton.d.ts +2 -0
- package/es/binding/CancelIconButton.js +15 -0
- package/es/binding/DataEditToolbar.d.ts +13 -0
- package/es/binding/DataEditToolbar.js +28 -0
- package/es/binding/DataObjectSelect.d.ts +50 -0
- package/es/binding/DataObjectSelect.js +26 -0
- package/es/binding/DeleteButton.d.ts +8 -0
- package/es/binding/DeleteButton.js +14 -0
- package/es/binding/DeleteIconButton.d.ts +10 -0
- package/es/binding/DeleteIconButton.js +15 -0
- package/es/binding/RefreshButton.d.ts +3 -0
- package/es/binding/RefreshButton.js +14 -0
- package/es/binding/RefreshIconButton.d.ts +2 -0
- package/es/binding/RefreshIconButton.js +16 -0
- package/es/binding/RefreshRowIconButton.d.ts +7 -0
- package/es/binding/RefreshRowIconButton.js +15 -0
- package/es/binding/SaveButton.d.ts +3 -0
- package/es/binding/SaveButton.js +14 -0
- package/es/binding/SaveIconButton.d.ts +2 -0
- package/es/binding/SaveIconButton.js +15 -0
- package/es/binding/index.d.ts +19 -0
- package/es/binding/index.js +19 -0
- package/es/index.d.ts +3 -0
- package/es/index.js +3 -0
- package/es/layout/AppLayoutDesktop.css +65 -0
- package/es/layout/AppLayoutDesktop.d.ts +3 -0
- package/es/layout/AppLayoutDesktop.js +19 -0
- package/es/layout/AppLayoutMobile.css +75 -0
- package/es/layout/AppLayoutMobile.d.ts +3 -0
- package/es/layout/AppLayoutMobile.js +18 -0
- package/es/layout/AppMenu.css +67 -0
- package/es/layout/AppMenu.d.ts +8 -0
- package/es/layout/AppMenu.js +36 -0
- package/es/layout/AppMenuItem.css +97 -0
- package/es/layout/AppMenuItem.d.ts +50 -0
- package/es/layout/AppMenuItem.js +14 -0
- package/es/layout/AppMenuSubheader.css +18 -0
- package/es/layout/AppMenuSubheader.d.ts +4 -0
- package/es/layout/AppMenuSubheader.js +6 -0
- package/es/layout/ErrorBoundary.css +101 -0
- package/es/layout/ErrorBoundary.d.ts +30 -0
- package/es/layout/ErrorBoundary.js +81 -0
- package/es/layout/PageContainer.css +29 -0
- package/es/layout/PageContainer.d.ts +7 -0
- package/es/layout/PageContainer.js +6 -0
- package/es/layout/PartnerToggle.d.ts +5 -0
- package/es/layout/PartnerToggle.js +9 -0
- package/es/layout/constants.d.ts +2 -0
- package/es/layout/constants.js +2 -0
- package/es/layout/index.css +65 -0
- package/es/layout/index.d.ts +33 -0
- package/es/layout/index.js +31 -0
- package/package.json +14 -9
- package/src/index.ts +1 -0
- package/src/layout/AppLayoutDesktop.css +65 -0
- package/src/layout/AppLayoutDesktop.tsx +45 -0
- package/src/layout/AppLayoutMobile.css +75 -0
- package/src/layout/AppLayoutMobile.tsx +50 -0
- package/src/layout/AppMenu.css +67 -0
- package/src/layout/AppMenu.tsx +75 -0
- package/src/layout/AppMenuItem.css +97 -0
- package/src/layout/AppMenuItem.tsx +80 -0
- package/src/layout/AppMenuSubheader.css +18 -0
- package/src/layout/AppMenuSubheader.tsx +14 -0
- package/src/layout/ErrorBoundary.css +101 -0
- package/src/layout/ErrorBoundary.tsx +162 -0
- package/src/layout/PageContainer.css +29 -0
- package/src/layout/PageContainer.tsx +19 -0
- package/src/layout/PartnerToggle.tsx +23 -0
- package/src/layout/constants.ts +2 -0
- package/src/layout/index.css +65 -0
- package/src/layout/index.tsx +62 -0
- package/tsconfig.build.tsbuildinfo +1 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createPortal } from "react-dom";
|
|
3
|
+
let toggleRoot = document.createElement("div");
|
|
4
|
+
let brand = document.querySelector(".partner-nav__brand");
|
|
5
|
+
toggleRoot.id = "toggle-root";
|
|
6
|
+
document.querySelector(".partner-nav")?.insertBefore(toggleRoot, brand);
|
|
7
|
+
export function PartnerToggle({ onClick, open, }) {
|
|
8
|
+
return createPortal(_jsx("button", { type: "button", className: "LayoutSmall-toggle", onClick: onClick, children: open ? "◀" : "☰" }), toggleRoot);
|
|
9
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
.app-view {
|
|
2
|
+
height: 100%;
|
|
3
|
+
overflow-y: hidden;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.scroll-view {
|
|
7
|
+
height: 100%;
|
|
8
|
+
max-height: 100%;
|
|
9
|
+
overflow-y: auto;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.flow > * + * {
|
|
13
|
+
margin-top: var(--flow-spacing, 1rem) !important;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.flow-xs > * {
|
|
17
|
+
--flow-spacing: 0.25rem;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.flow-sm > * {
|
|
21
|
+
--flow-spacing: 0.5rem;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.flow-lg > * {
|
|
25
|
+
--flow-spacing: 1.5rem;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.flow-xl > * {
|
|
29
|
+
--flow-spacing: 2rem;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.flow-xxl > * {
|
|
33
|
+
--flow-spacing: 3rem;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.app-grid {
|
|
37
|
+
height: 100%;
|
|
38
|
+
overflow-y: hidden;
|
|
39
|
+
display: grid;
|
|
40
|
+
gap: 1rem;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.rows-af {
|
|
44
|
+
grid-template-rows: auto 1fr;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.rows-aaf {
|
|
48
|
+
grid-template-rows: auto auto 1fr;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.rows-afaf {
|
|
52
|
+
grid-template-rows: auto 1fr auto 1fr;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.rows-aff {
|
|
56
|
+
grid-template-rows: auto 1fr 1fr;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.rows-f {
|
|
60
|
+
grid-template-rows: 1fr;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.rows-ff {
|
|
64
|
+
grid-template-rows: 1fr 1fr;
|
|
65
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import "./index.css";
|
|
2
|
+
import type React from "react";
|
|
3
|
+
import type { AppMenuProps } from "./AppMenu.js";
|
|
4
|
+
export type AppLayoutProps = {
|
|
5
|
+
AppMenuProps?: AppMenuProps;
|
|
6
|
+
children: React.ReactNode | React.ReactNode[];
|
|
7
|
+
menu?: React.ReactNode | React.ReactNode[];
|
|
8
|
+
/**
|
|
9
|
+
* @deprecated The toolbar is now server-rendered. This prop has no effect.
|
|
10
|
+
*/
|
|
11
|
+
toolbar?: React.ReactNode | React.ReactNode[];
|
|
12
|
+
/**
|
|
13
|
+
* @deprecated The title is now rendered in the server-rendered toolbar. This prop has no effect.
|
|
14
|
+
*/
|
|
15
|
+
title?: React.ReactNode;
|
|
16
|
+
/**
|
|
17
|
+
* @deprecated The toolbar is now server-rendered. This prop has no effect.
|
|
18
|
+
*/
|
|
19
|
+
enableMenuToggle?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Enable partner mode (for partner.olenbetong.no applications).
|
|
22
|
+
* Adds a toggle button to the existing partner header instead of the app bar.
|
|
23
|
+
*/
|
|
24
|
+
partner?: boolean;
|
|
25
|
+
};
|
|
26
|
+
export * from "./AppMenu.js";
|
|
27
|
+
export * from "./AppMenuItem.js";
|
|
28
|
+
export * from "./AppMenuSubheader.js";
|
|
29
|
+
export * from "./constants.js";
|
|
30
|
+
export * from "./ErrorBoundary.js";
|
|
31
|
+
export * from "./PageContainer.js";
|
|
32
|
+
export * from "./PartnerToggle.js";
|
|
33
|
+
export declare function AppLayout(props: AppLayoutProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import "./index.css";
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { AppLayoutDesktop } from "./AppLayoutDesktop.js";
|
|
5
|
+
import { AppLayoutMobile } from "./AppLayoutMobile.js";
|
|
6
|
+
function useIsMobile() {
|
|
7
|
+
let [isMobile, setIsMobile] = useState(() => {
|
|
8
|
+
if (typeof window === "undefined")
|
|
9
|
+
return false;
|
|
10
|
+
return window.matchMedia("(max-width: 1199px)").matches;
|
|
11
|
+
});
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
let mq = window.matchMedia("(max-width: 1199px)");
|
|
14
|
+
let handler = (e) => setIsMobile(e.matches);
|
|
15
|
+
mq.addEventListener("change", handler);
|
|
16
|
+
return () => mq.removeEventListener("change", handler);
|
|
17
|
+
}, []);
|
|
18
|
+
return isMobile;
|
|
19
|
+
}
|
|
20
|
+
export * from "./AppMenu.js";
|
|
21
|
+
export * from "./AppMenuItem.js";
|
|
22
|
+
export * from "./AppMenuSubheader.js";
|
|
23
|
+
export * from "./constants.js";
|
|
24
|
+
export * from "./ErrorBoundary.js";
|
|
25
|
+
export * from "./PageContainer.js";
|
|
26
|
+
export * from "./PartnerToggle.js";
|
|
27
|
+
export function AppLayout(props) {
|
|
28
|
+
let isSmall = useIsMobile();
|
|
29
|
+
let Component = isSmall ? AppLayoutMobile : AppLayoutDesktop;
|
|
30
|
+
return _jsx(Component, { ...props });
|
|
31
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@olenbetong/appframe-ds",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Components that bind Designsystemet components to Appframe data objects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "./es/index.d.ts",
|
|
@@ -15,10 +15,12 @@
|
|
|
15
15
|
"author": "Bjørnar Vister Hansen <bvh@olenbetong.no>",
|
|
16
16
|
"license": "MIT",
|
|
17
17
|
"peerDependencies": {
|
|
18
|
-
"@digdir/designsystemet-react": ">=1.
|
|
18
|
+
"@digdir/designsystemet-react": ">=1.15.0",
|
|
19
19
|
"@mui/icons-material": ">=7.0.0",
|
|
20
20
|
"react": ">=18.2.0",
|
|
21
|
-
"react-dom": ">=18.2.0"
|
|
21
|
+
"react-dom": ">=18.2.0",
|
|
22
|
+
"react-error-boundary": ">=5.0.0",
|
|
23
|
+
"react-router": ">=7.0.0"
|
|
22
24
|
},
|
|
23
25
|
"dependencies": {
|
|
24
26
|
"@olenbetong/appframe-core": "2.11.10",
|
|
@@ -29,18 +31,21 @@
|
|
|
29
31
|
"clsx": "^2.1.1"
|
|
30
32
|
},
|
|
31
33
|
"devDependencies": {
|
|
32
|
-
"@digdir/designsystemet-react": "1.
|
|
33
|
-
"@mui/icons-material": "9.0.
|
|
34
|
-
"@types/react": "19.2.
|
|
34
|
+
"@digdir/designsystemet-react": "1.15.0",
|
|
35
|
+
"@mui/icons-material": "9.0.1",
|
|
36
|
+
"@types/react": "19.2.15",
|
|
35
37
|
"@types/react-dom": "19.2.3",
|
|
36
38
|
"@types/react-window": "^1.8.8",
|
|
37
|
-
"react": "19.2.
|
|
38
|
-
"react-dom": "19.2.
|
|
39
|
+
"react": "19.2.6",
|
|
40
|
+
"react-dom": "19.2.6",
|
|
41
|
+
"react-error-boundary": "6.1.2",
|
|
42
|
+
"react-router": "7.16.0",
|
|
39
43
|
"typescript": "6.0.3"
|
|
40
44
|
},
|
|
45
|
+
"optionalDependencies": {},
|
|
41
46
|
"scripts": {
|
|
42
47
|
"build": "pnpm run build:tsc && pnpm run build:css",
|
|
43
48
|
"build:tsc": "node --no-warnings ../../scripts/clean.ts ./es tsconfig.build.tsbuildinfo && tsc -p tsconfig.build.json",
|
|
44
|
-
"build:css": "cp src/AfLookup/Lookup.css es/AfLookup/Lookup.css && cp src/AfLookup/Combobox.css es/AfLookup/Combobox.css"
|
|
49
|
+
"build:css": "cp src/AfLookup/Lookup.css es/AfLookup/Lookup.css && cp src/AfLookup/Combobox.css es/AfLookup/Combobox.css && mkdir -p es/layout && cp src/layout/AppMenu.css es/layout/AppMenu.css && cp src/layout/AppMenuItem.css es/layout/AppMenuItem.css && cp src/layout/AppMenuSubheader.css es/layout/AppMenuSubheader.css && cp src/layout/AppLayoutDesktop.css es/layout/AppLayoutDesktop.css && cp src/layout/AppLayoutMobile.css es/layout/AppLayoutMobile.css && cp src/layout/index.css es/layout/index.css && cp src/layout/ErrorBoundary.css es/layout/ErrorBoundary.css && cp src/layout/PageContainer.css es/layout/PageContainer.css"
|
|
45
50
|
}
|
|
46
51
|
}
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
.LayoutLarge-container {
|
|
2
|
+
height: 100vh;
|
|
3
|
+
display: grid;
|
|
4
|
+
grid-template-rows: 1fr;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.LayoutLarge-root {
|
|
8
|
+
height: 100%;
|
|
9
|
+
overflow: hidden;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.LayoutLarge-root.LayoutLarge-withMenu {
|
|
13
|
+
transition: padding 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms !important;
|
|
14
|
+
padding-left: 0;
|
|
15
|
+
position: relative;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.LayoutLarge-root.LayoutLarge-withMenu.open {
|
|
19
|
+
padding-left: var(--navbar-drawer-width, 24rem);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.LayoutLarge-menu {
|
|
23
|
+
height: 100%;
|
|
24
|
+
width: var(--navbar-drawer-width, 24rem);
|
|
25
|
+
transition: left ease-in-out 200ms;
|
|
26
|
+
position: absolute;
|
|
27
|
+
top: 0;
|
|
28
|
+
left: calc(var(--navbar-drawer-width, 24rem) * -1);
|
|
29
|
+
z-index: 1200;
|
|
30
|
+
background-color: white;
|
|
31
|
+
box-shadow:
|
|
32
|
+
rgb(0 0 0 / 20%) 0px 2px 1px -1px,
|
|
33
|
+
rgb(0 0 0 / 14%) 0px 1px 1px 0px,
|
|
34
|
+
rgb(0 0 0 / 12%) 0px 1px 3px 0px;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.LayoutLarge-withMenu.open .LayoutLarge-menu {
|
|
38
|
+
left: 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@media screen {
|
|
42
|
+
.LayoutLarge-main {
|
|
43
|
+
height: 100%;
|
|
44
|
+
overflow: auto;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.LayoutLarge-main:has(> .app-view, > .ObPageContainer-fullHeight) {
|
|
48
|
+
max-height: 100%;
|
|
49
|
+
overflow: hidden;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@media print {
|
|
54
|
+
.LayoutLarge-menu {
|
|
55
|
+
display: none !important;
|
|
56
|
+
box-shadow: none !important;
|
|
57
|
+
overflow: hidden !important;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.LayoutLarge-root {
|
|
61
|
+
display: block;
|
|
62
|
+
height: auto;
|
|
63
|
+
overflow: visible;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import "./AppLayoutDesktop.css";
|
|
2
|
+
|
|
3
|
+
import clsx from "clsx";
|
|
4
|
+
import { Suspense, useState } from "react";
|
|
5
|
+
import { ErrorBoundary } from "react-error-boundary";
|
|
6
|
+
|
|
7
|
+
import { AppMenu } from "./AppMenu.js";
|
|
8
|
+
import { ErrorAlertFallback } from "./ErrorBoundary.js";
|
|
9
|
+
import type { AppLayoutProps } from "./index.js";
|
|
10
|
+
import { PartnerToggle } from "./PartnerToggle.js";
|
|
11
|
+
|
|
12
|
+
function LoadingFallback() {
|
|
13
|
+
return (
|
|
14
|
+
<div
|
|
15
|
+
style={{
|
|
16
|
+
height: "4px",
|
|
17
|
+
background: "linear-gradient(90deg, var(--ds-color-brand-base-default, #0062ba) 0%, transparent 100%)",
|
|
18
|
+
animation: "progress-indeterminate 1.5s ease-in-out infinite",
|
|
19
|
+
}}
|
|
20
|
+
/>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function AppLayoutDesktop({ AppMenuProps, children, menu, partner }: AppLayoutProps) {
|
|
25
|
+
let [open, setOpen] = useState(true);
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className={clsx(partner ? "app-view" : "LayoutLarge-container")}>
|
|
29
|
+
{partner && menu && <PartnerToggle open={open} onClick={() => setOpen((o) => !o)} />}
|
|
30
|
+
|
|
31
|
+
<div className={clsx("LayoutLarge-root", menu && "LayoutLarge-withMenu", open && "open")}>
|
|
32
|
+
{menu && (
|
|
33
|
+
<AppMenu className="LayoutLarge-menu AppMenu-drawer" {...AppMenuProps}>
|
|
34
|
+
{menu}
|
|
35
|
+
</AppMenu>
|
|
36
|
+
)}
|
|
37
|
+
<main className="LayoutLarge-main">
|
|
38
|
+
<Suspense fallback={<LoadingFallback />}>
|
|
39
|
+
<ErrorBoundary FallbackComponent={ErrorAlertFallback}>{children}</ErrorBoundary>
|
|
40
|
+
</Suspense>
|
|
41
|
+
</main>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
.LayoutSmall-root:has(> .ObPageContainer-fullHeight, > .app-view) {
|
|
2
|
+
display: grid;
|
|
3
|
+
grid-template-rows: auto 1fr;
|
|
4
|
+
height: 100dvh;
|
|
5
|
+
overflow: hidden;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.partner-nav {
|
|
9
|
+
color: white;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/* Minimal toggle-only bar shown when there's a sidebar menu but no server AppBar */
|
|
13
|
+
.LayoutSmall-menuBar {
|
|
14
|
+
display: flex;
|
|
15
|
+
align-items: center;
|
|
16
|
+
height: var(--navbar-height, 3.5rem);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.LayoutSmall-toggle {
|
|
20
|
+
display: flex;
|
|
21
|
+
align-items: center;
|
|
22
|
+
justify-content: center;
|
|
23
|
+
background: transparent;
|
|
24
|
+
border: none;
|
|
25
|
+
color: inherit;
|
|
26
|
+
cursor: pointer;
|
|
27
|
+
border-radius: 50%;
|
|
28
|
+
height: var(--navbar-height, 3.5rem);
|
|
29
|
+
width: var(--navbar-height, 3.5rem);
|
|
30
|
+
font-size: 1.5rem;
|
|
31
|
+
flex-shrink: 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.LayoutSmall-toggle:hover {
|
|
35
|
+
background-color: rgb(0 0 0 / 0.06);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* Drawer overlay */
|
|
39
|
+
.LayoutSmall-drawerOverlay {
|
|
40
|
+
position: fixed;
|
|
41
|
+
inset: 0;
|
|
42
|
+
background-color: rgb(0 0 0 / 50%);
|
|
43
|
+
z-index: 1199;
|
|
44
|
+
opacity: 0;
|
|
45
|
+
pointer-events: none;
|
|
46
|
+
transition: opacity 225ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.LayoutSmall-drawerOverlay.open {
|
|
50
|
+
opacity: 1;
|
|
51
|
+
pointer-events: auto;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* Drawer panel */
|
|
55
|
+
.LayoutSmall-drawer {
|
|
56
|
+
position: fixed;
|
|
57
|
+
top: 0;
|
|
58
|
+
left: 0;
|
|
59
|
+
bottom: 0;
|
|
60
|
+
max-width: 90%;
|
|
61
|
+
width: var(--navbar-drawer-width, 20rem);
|
|
62
|
+
z-index: 1200;
|
|
63
|
+
background-color: var(--ds-color-neutral-background-default, #fff);
|
|
64
|
+
box-shadow:
|
|
65
|
+
rgb(0 0 0 / 20%) 0px 8px 10px -5px,
|
|
66
|
+
rgb(0 0 0 / 14%) 0px 16px 24px 2px,
|
|
67
|
+
rgb(0 0 0 / 12%) 0px 6px 30px 5px;
|
|
68
|
+
transform: translateX(-100%);
|
|
69
|
+
transition: transform 225ms cubic-bezier(0, 0, 0.2, 1);
|
|
70
|
+
overflow: hidden;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.LayoutSmall-drawer.open {
|
|
74
|
+
transform: translateX(0);
|
|
75
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import "./AppLayoutMobile.css";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { useLocation } from "react-router";
|
|
5
|
+
|
|
6
|
+
import { AppMenu } from "./AppMenu.js";
|
|
7
|
+
import type { AppLayoutProps } from "./index.js";
|
|
8
|
+
import { PartnerToggle } from "./PartnerToggle.js";
|
|
9
|
+
|
|
10
|
+
function Toggle({ onToggle }: { onToggle: () => void }) {
|
|
11
|
+
return (
|
|
12
|
+
<button type="button" className="LayoutSmall-toggle" onClick={onToggle} aria-label="Toggle menu">
|
|
13
|
+
☰
|
|
14
|
+
</button>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function AppLayoutMobile({ AppMenuProps, children, menu, partner }: AppLayoutProps) {
|
|
19
|
+
let [drawerOpen, setDrawerOpen] = useState(false);
|
|
20
|
+
let location = useLocation();
|
|
21
|
+
|
|
22
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: close drawer on navigation
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
setDrawerOpen(false);
|
|
25
|
+
}, [location]);
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className="LayoutSmall-root">
|
|
29
|
+
{partner && menu && <PartnerToggle open={drawerOpen} onClick={() => setDrawerOpen((c) => !c)} />}
|
|
30
|
+
{!partner && menu && (
|
|
31
|
+
<div className="LayoutSmall-menuBar">
|
|
32
|
+
<Toggle onToggle={() => setDrawerOpen((c) => !c)} />
|
|
33
|
+
</div>
|
|
34
|
+
)}
|
|
35
|
+
{menu && (
|
|
36
|
+
<>
|
|
37
|
+
<div
|
|
38
|
+
className={`LayoutSmall-drawerOverlay${drawerOpen ? " open" : ""}`}
|
|
39
|
+
onClick={() => setDrawerOpen(false)}
|
|
40
|
+
aria-hidden="true"
|
|
41
|
+
/>
|
|
42
|
+
<div className={`LayoutSmall-drawer AppMenu-drawer${drawerOpen ? " open" : ""}`}>
|
|
43
|
+
<AppMenu {...AppMenuProps}>{menu}</AppMenu>
|
|
44
|
+
</div>
|
|
45
|
+
</>
|
|
46
|
+
)}
|
|
47
|
+
{children}
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
.AppMenu-header {
|
|
2
|
+
padding: 1rem;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.AppMenu-header .AppMenu-title {
|
|
6
|
+
font-weight: 500;
|
|
7
|
+
letter-spacing: -0.02rem;
|
|
8
|
+
font-size: var(--ds-font-size-xl, 1.5rem);
|
|
9
|
+
margin: 0;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.AppMenu-header .AppMenu-subTitle {
|
|
13
|
+
font-size: var(--ds-font-size-sm, 1.1rem);
|
|
14
|
+
margin: 0;
|
|
15
|
+
color: var(--ds-color-neutral-text-subtle, #555);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.AppMenu-divider {
|
|
19
|
+
border: none;
|
|
20
|
+
border-top: 1px solid var(--ds-color-neutral-border-default, #dfdfdf);
|
|
21
|
+
margin: 0;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.AppMenu-pendingCount {
|
|
25
|
+
color: var(--ds-color-neutral-text-subtle, #666) !important;
|
|
26
|
+
font-size: 0.8em;
|
|
27
|
+
font-weight: 700;
|
|
28
|
+
padding-right: 0.5rem;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.MenuItems-root {
|
|
32
|
+
overflow-y: auto;
|
|
33
|
+
scrollbar-width: thin;
|
|
34
|
+
scrollbar-color: var(--ds-color-neutral-border-default, #dfdfdf) transparent;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.MenuItems-root::-webkit-scrollbar {
|
|
38
|
+
width: 0.4rem;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.MenuItems-root::-webkit-scrollbar-button {
|
|
42
|
+
display: none;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.MenuItems-root::-webkit-scrollbar-thumb {
|
|
46
|
+
width: 0.4rem;
|
|
47
|
+
background-color: var(--ds-color-neutral-border-default, #dfdfdf);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.MenuItems-root .AppMenuList-nested .AppMenuItem-root {
|
|
51
|
+
padding-left: 3rem;
|
|
52
|
+
padding-block: 0.25rem;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.MenuItems-root .AppMenuList-nested .AppMenuItem-root .AppMenuItem-text {
|
|
56
|
+
font-size: 0.9rem;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.MenuItems-collapse {
|
|
60
|
+
margin-bottom: -0.5rem;
|
|
61
|
+
margin-top: -0.5rem;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.MenuItem-noWrap {
|
|
65
|
+
overflow: hidden;
|
|
66
|
+
white-space: nowrap;
|
|
67
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import "./AppMenu.css";
|
|
2
|
+
|
|
3
|
+
import { getLocalizedString } from "@olenbetong/appframe-core";
|
|
4
|
+
|
|
5
|
+
import clsx from "clsx";
|
|
6
|
+
import type React from "react";
|
|
7
|
+
|
|
8
|
+
type MenuHeaderProps = {
|
|
9
|
+
children?: React.ReactNode[] | React.ReactNode;
|
|
10
|
+
primary: string;
|
|
11
|
+
secondary?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function MenuHeader({ primary, secondary }: MenuHeaderProps) {
|
|
15
|
+
return (
|
|
16
|
+
<>
|
|
17
|
+
<div className="AppMenu-header">
|
|
18
|
+
<h1 className="AppMenu-title" id="NavigationHeader">
|
|
19
|
+
{primary}
|
|
20
|
+
</h1>
|
|
21
|
+
{secondary && <p className="AppMenu-subTitle">{secondary}</p>}
|
|
22
|
+
</div>
|
|
23
|
+
<hr className="AppMenu-divider" />
|
|
24
|
+
</>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function handleKeyDown(evt: React.KeyboardEvent) {
|
|
29
|
+
let nextElement: Element | null | undefined = null;
|
|
30
|
+
let appMenu = evt.currentTarget;
|
|
31
|
+
|
|
32
|
+
if (!document.activeElement || !(appMenu instanceof HTMLElement) || !appMenu.contains(document.activeElement)) return;
|
|
33
|
+
|
|
34
|
+
let links = Array.from(appMenu.querySelectorAll<HTMLElement>("a, button"));
|
|
35
|
+
let currentIndex = links.indexOf(document.activeElement as HTMLElement);
|
|
36
|
+
|
|
37
|
+
if (evt.key === "ArrowDown" || evt.key === "ArrowRight") {
|
|
38
|
+
if (currentIndex < links.length) {
|
|
39
|
+
nextElement = links[currentIndex + 1];
|
|
40
|
+
}
|
|
41
|
+
} else if (evt.key === "ArrowUp" || evt.key === "ArrowLeft") {
|
|
42
|
+
if (currentIndex > 0) {
|
|
43
|
+
nextElement = links[currentIndex - 1];
|
|
44
|
+
}
|
|
45
|
+
} else if (evt.key === "Home" || evt.key === "End") {
|
|
46
|
+
nextElement = links.at(evt.key === "Home" ? 0 : -1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (nextElement instanceof HTMLElement) {
|
|
50
|
+
evt.preventDefault();
|
|
51
|
+
nextElement.scrollIntoView({ block: "nearest" });
|
|
52
|
+
nextElement.focus();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type AppMenuProps = Pick<React.HTMLProps<HTMLElement>, "aria-label" | "aria-labelledby" | "className" | "id"> & {
|
|
57
|
+
children?: React.ReactNode[] | React.ReactNode;
|
|
58
|
+
headerPrimary?: string;
|
|
59
|
+
headerSecondary?: string;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export function AppMenu({ children, className, headerPrimary, headerSecondary, ...props }: AppMenuProps) {
|
|
63
|
+
return (
|
|
64
|
+
<nav
|
|
65
|
+
aria-labelledby={headerPrimary ? "NavigationHeader" : undefined}
|
|
66
|
+
aria-label={headerPrimary ? undefined : getLocalizedString("Application pages")}
|
|
67
|
+
className={clsx(className, "MenuItems-root")}
|
|
68
|
+
onKeyDown={handleKeyDown}
|
|
69
|
+
{...props}
|
|
70
|
+
>
|
|
71
|
+
{headerPrimary && <MenuHeader primary={headerPrimary} secondary={headerSecondary} />}
|
|
72
|
+
{children}
|
|
73
|
+
</nav>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
.AppMenuItem-root {
|
|
2
|
+
display: flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
position: relative;
|
|
5
|
+
border-radius: var(--ds-border-radius-md, 0.25rem);
|
|
6
|
+
overflow: hidden;
|
|
7
|
+
margin: 0.5rem;
|
|
8
|
+
padding: 0.5rem;
|
|
9
|
+
width: calc(100% - 1rem);
|
|
10
|
+
box-sizing: border-box;
|
|
11
|
+
text-decoration: none;
|
|
12
|
+
color: var(--ds-color-neutral-text-default, rgba(0, 0, 0, 0.87));
|
|
13
|
+
background: transparent;
|
|
14
|
+
border: none;
|
|
15
|
+
cursor: pointer;
|
|
16
|
+
font-family: inherit;
|
|
17
|
+
font-size: inherit;
|
|
18
|
+
text-align: left;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@media (pointer: fine) {
|
|
22
|
+
.AppMenuItem-root {
|
|
23
|
+
margin: 0.2rem 0.5rem;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.AppMenuItem-icon {
|
|
28
|
+
display: flex;
|
|
29
|
+
align-items: center;
|
|
30
|
+
min-width: 2.5rem;
|
|
31
|
+
color: inherit;
|
|
32
|
+
flex-shrink: 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.AppMenuItem-text {
|
|
36
|
+
flex: 1;
|
|
37
|
+
font-weight: 500;
|
|
38
|
+
color: var(--ds-color-neutral-text-default, rgba(0, 0, 0, 0.87));
|
|
39
|
+
min-width: 0;
|
|
40
|
+
overflow: hidden;
|
|
41
|
+
text-overflow: ellipsis;
|
|
42
|
+
white-space: nowrap;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.AppMenuItem-info {
|
|
46
|
+
margin-left: auto;
|
|
47
|
+
flex-shrink: 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.AppMenuItem-root::before {
|
|
51
|
+
background-color: #000;
|
|
52
|
+
opacity: 0;
|
|
53
|
+
transition:
|
|
54
|
+
opacity 30ms linear,
|
|
55
|
+
background-color 30ms linear;
|
|
56
|
+
content: "";
|
|
57
|
+
position: absolute;
|
|
58
|
+
top: 0;
|
|
59
|
+
left: 0;
|
|
60
|
+
height: 100%;
|
|
61
|
+
width: 100%;
|
|
62
|
+
pointer-events: none;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.AppMenuItem-root:hover,
|
|
66
|
+
.AppMenuItem-root:focus,
|
|
67
|
+
.AppMenuItem-root:active {
|
|
68
|
+
text-decoration: none;
|
|
69
|
+
outline: none;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.AppMenuItem-root:hover::before {
|
|
73
|
+
opacity: 0.04;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.AppMenuItem-root:active::before {
|
|
77
|
+
opacity: 0.16;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.AppMenuItem-root.active::before,
|
|
81
|
+
.AppMenuItem-root.active:hover::before {
|
|
82
|
+
background-color: var(--ds-color-brand-base-default, var(--brand-primary, #0062ba));
|
|
83
|
+
opacity: 0.16;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.AppMenuItem-root.active:hover::before {
|
|
87
|
+
opacity: 0.2;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.AppMenuItem-root.active:active::before {
|
|
91
|
+
opacity: 0.24;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.AppMenuItem-root.active .AppMenuItem-text,
|
|
95
|
+
.AppMenuItem-root.active .AppMenuItem-icon {
|
|
96
|
+
color: var(--ds-color-brand-base-default, var(--brand-primary, #0062ba));
|
|
97
|
+
}
|