@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.
Files changed (102) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/es/AfLookup/BoundLookup.d.ts +9 -0
  3. package/es/AfLookup/BoundLookup.js +16 -0
  4. package/es/AfLookup/Combobox.css +78 -0
  5. package/es/AfLookup/Combobox.d.ts +15 -0
  6. package/es/AfLookup/Combobox.js +124 -0
  7. package/es/AfLookup/Lookup.css +104 -0
  8. package/es/AfLookup/Lookup.d.ts +34 -0
  9. package/es/AfLookup/Lookup.js +161 -0
  10. package/es/AfLookup/index.d.ts +3 -0
  11. package/es/AfLookup/index.js +3 -0
  12. package/es/binding/BoundCheckbox.d.ts +9 -0
  13. package/es/binding/BoundCheckbox.js +13 -0
  14. package/es/binding/BoundDatePicker.d.ts +7 -0
  15. package/es/binding/BoundDatePicker.js +12 -0
  16. package/es/binding/BoundDateTimePicker.d.ts +7 -0
  17. package/es/binding/BoundDateTimePicker.js +14 -0
  18. package/es/binding/BoundInput.d.ts +6 -0
  19. package/es/binding/BoundInput.js +12 -0
  20. package/es/binding/BoundNumericTextField.d.ts +20 -0
  21. package/es/binding/BoundNumericTextField.js +58 -0
  22. package/es/binding/BoundSelect.d.ts +9 -0
  23. package/es/binding/BoundSelect.js +13 -0
  24. package/es/binding/BoundSwitch.d.ts +9 -0
  25. package/es/binding/BoundSwitch.js +13 -0
  26. package/es/binding/BoundTextField.d.ts +7 -0
  27. package/es/binding/BoundTextField.js +22 -0
  28. package/es/binding/CancelButton.d.ts +3 -0
  29. package/es/binding/CancelButton.js +14 -0
  30. package/es/binding/CancelIconButton.d.ts +2 -0
  31. package/es/binding/CancelIconButton.js +15 -0
  32. package/es/binding/DataEditToolbar.d.ts +13 -0
  33. package/es/binding/DataEditToolbar.js +28 -0
  34. package/es/binding/DataObjectSelect.d.ts +50 -0
  35. package/es/binding/DataObjectSelect.js +26 -0
  36. package/es/binding/DeleteButton.d.ts +8 -0
  37. package/es/binding/DeleteButton.js +14 -0
  38. package/es/binding/DeleteIconButton.d.ts +10 -0
  39. package/es/binding/DeleteIconButton.js +15 -0
  40. package/es/binding/RefreshButton.d.ts +3 -0
  41. package/es/binding/RefreshButton.js +14 -0
  42. package/es/binding/RefreshIconButton.d.ts +2 -0
  43. package/es/binding/RefreshIconButton.js +16 -0
  44. package/es/binding/RefreshRowIconButton.d.ts +7 -0
  45. package/es/binding/RefreshRowIconButton.js +15 -0
  46. package/es/binding/SaveButton.d.ts +3 -0
  47. package/es/binding/SaveButton.js +14 -0
  48. package/es/binding/SaveIconButton.d.ts +2 -0
  49. package/es/binding/SaveIconButton.js +15 -0
  50. package/es/binding/index.d.ts +19 -0
  51. package/es/binding/index.js +19 -0
  52. package/es/index.d.ts +3 -0
  53. package/es/index.js +3 -0
  54. package/es/layout/AppLayoutDesktop.css +65 -0
  55. package/es/layout/AppLayoutDesktop.d.ts +3 -0
  56. package/es/layout/AppLayoutDesktop.js +19 -0
  57. package/es/layout/AppLayoutMobile.css +75 -0
  58. package/es/layout/AppLayoutMobile.d.ts +3 -0
  59. package/es/layout/AppLayoutMobile.js +18 -0
  60. package/es/layout/AppMenu.css +67 -0
  61. package/es/layout/AppMenu.d.ts +8 -0
  62. package/es/layout/AppMenu.js +36 -0
  63. package/es/layout/AppMenuItem.css +97 -0
  64. package/es/layout/AppMenuItem.d.ts +50 -0
  65. package/es/layout/AppMenuItem.js +14 -0
  66. package/es/layout/AppMenuSubheader.css +18 -0
  67. package/es/layout/AppMenuSubheader.d.ts +4 -0
  68. package/es/layout/AppMenuSubheader.js +6 -0
  69. package/es/layout/ErrorBoundary.css +101 -0
  70. package/es/layout/ErrorBoundary.d.ts +30 -0
  71. package/es/layout/ErrorBoundary.js +81 -0
  72. package/es/layout/PageContainer.css +29 -0
  73. package/es/layout/PageContainer.d.ts +7 -0
  74. package/es/layout/PageContainer.js +6 -0
  75. package/es/layout/PartnerToggle.d.ts +5 -0
  76. package/es/layout/PartnerToggle.js +9 -0
  77. package/es/layout/constants.d.ts +2 -0
  78. package/es/layout/constants.js +2 -0
  79. package/es/layout/index.css +65 -0
  80. package/es/layout/index.d.ts +33 -0
  81. package/es/layout/index.js +31 -0
  82. package/package.json +14 -9
  83. package/src/index.ts +1 -0
  84. package/src/layout/AppLayoutDesktop.css +65 -0
  85. package/src/layout/AppLayoutDesktop.tsx +45 -0
  86. package/src/layout/AppLayoutMobile.css +75 -0
  87. package/src/layout/AppLayoutMobile.tsx +50 -0
  88. package/src/layout/AppMenu.css +67 -0
  89. package/src/layout/AppMenu.tsx +75 -0
  90. package/src/layout/AppMenuItem.css +97 -0
  91. package/src/layout/AppMenuItem.tsx +80 -0
  92. package/src/layout/AppMenuSubheader.css +18 -0
  93. package/src/layout/AppMenuSubheader.tsx +14 -0
  94. package/src/layout/ErrorBoundary.css +101 -0
  95. package/src/layout/ErrorBoundary.tsx +162 -0
  96. package/src/layout/PageContainer.css +29 -0
  97. package/src/layout/PageContainer.tsx +19 -0
  98. package/src/layout/PartnerToggle.tsx +23 -0
  99. package/src/layout/constants.ts +2 -0
  100. package/src/layout/index.css +65 -0
  101. package/src/layout/index.tsx +62 -0
  102. 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,2 @@
1
+ export declare const AF_MUI_PAGE_TITLE_ID = "PageTitleSmallLayout";
2
+ export declare const AF_MUI_APPBAR_TOOLS_ID = "AppBarTools";
@@ -0,0 +1,2 @@
1
+ export const AF_MUI_PAGE_TITLE_ID = "PageTitleSmallLayout";
2
+ export const AF_MUI_APPBAR_TOOLS_ID = "AppBarTools";
@@ -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.2.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.0.0",
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.13.3",
33
- "@mui/icons-material": "9.0.0",
34
- "@types/react": "19.2.14",
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.5",
38
- "react-dom": "19.2.5",
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
@@ -1,2 +1,3 @@
1
1
  export * from "./AfLookup/index.js";
2
2
  export * from "./binding/index.js";
3
+ export * from "./layout/index.js";
@@ -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
+ }