@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,80 @@
1
+ import "./AppMenuItem.css";
2
+
3
+ import clsx from "clsx";
4
+ import type { To } from "react-router";
5
+ import { forwardRef } from "react";
6
+ import { NavLink, type NavLinkProps } from "react-router";
7
+ import type React from "react";
8
+
9
+ export type AppMenuItemProps = Omit<NavLinkProps, "className"> & {
10
+ children: React.ReactNode | React.ReactNode[];
11
+ info?: React.ReactNode | React.ReactNode[];
12
+ icon?: React.ReactNode;
13
+ onClick?: React.MouseEventHandler<HTMLAnchorElement>;
14
+ to: To;
15
+ end?: boolean;
16
+ className?: string;
17
+ };
18
+
19
+ export const AppMenuItem = forwardRef<HTMLAnchorElement, AppMenuItemProps>(function AppMenuItem(
20
+ { children, className, end = true, info, to, icon, ...props }: AppMenuItemProps,
21
+ ref,
22
+ ) {
23
+ return (
24
+ <NavLink viewTransition to={to} className={clsx("AppMenuItem-root", className)} end={end} {...props} ref={ref}>
25
+ {icon && <span className="AppMenuItem-icon">{icon}</span>}
26
+ <span className="AppMenuItem-text">{children}</span>
27
+ {info && <span className="AppMenuItem-info">{info}</span>}
28
+ </NavLink>
29
+ );
30
+ });
31
+
32
+ export type AppMenuItemExternalLinkProps = Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, "href"> & {
33
+ children: React.ReactNode | React.ReactNode[];
34
+ info?: React.ReactNode | React.ReactNode[];
35
+ icon?: React.ReactNode;
36
+ href: string;
37
+ target?: string;
38
+ };
39
+
40
+ export const AppMenuItemExternalLink = forwardRef<HTMLAnchorElement, AppMenuItemExternalLinkProps>(
41
+ function AppMenuItemExternalLink(
42
+ { children, className, href, info, icon, target, ...props }: AppMenuItemExternalLinkProps,
43
+ ref,
44
+ ) {
45
+ return (
46
+ <a href={href} className={clsx("AppMenuItem-root", className)} target={target} {...props} ref={ref}>
47
+ {icon && <span className="AppMenuItem-icon">{icon}</span>}
48
+ <span className="AppMenuItem-text">{children}</span>
49
+ {info && <span className="AppMenuItem-info">{info}</span>}
50
+ </a>
51
+ );
52
+ },
53
+ );
54
+
55
+ export type AppMenuItemButtonProps = Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onClick"> & {
56
+ active?: boolean;
57
+ children: React.ReactNode | React.ReactNode[];
58
+ info?: React.ReactNode | React.ReactNode[];
59
+ icon?: React.ReactNode;
60
+ onClick?: React.MouseEventHandler<HTMLButtonElement>;
61
+ };
62
+
63
+ export const AppMenuItemButton = forwardRef<HTMLButtonElement, AppMenuItemButtonProps>(function AppMenuItemButton(
64
+ { children, className, active = false, info, onClick, icon, ...props }: AppMenuItemButtonProps,
65
+ ref,
66
+ ) {
67
+ return (
68
+ <button
69
+ type="button"
70
+ onClick={onClick}
71
+ className={clsx("AppMenuItem-root", className, active && "active")}
72
+ {...props}
73
+ ref={ref}
74
+ >
75
+ {icon && <span className="AppMenuItem-icon">{icon}</span>}
76
+ <span className="AppMenuItem-text">{children}</span>
77
+ {info && <span className="AppMenuItem-info">{info}</span>}
78
+ </button>
79
+ );
80
+ });
@@ -0,0 +1,18 @@
1
+ .AppMenuSubheader-root {
2
+ font-size: var(--ds-font-size-xs, 0.75rem);
3
+ font-weight: 600;
4
+ letter-spacing: 0.08em;
5
+ text-transform: uppercase;
6
+ padding: 0.5rem 1rem;
7
+ color: var(--ds-color-neutral-text-subtle, #555);
8
+ background-color: var(--ds-color-neutral-background-default, transparent);
9
+ position: sticky;
10
+ top: 0;
11
+ z-index: 1;
12
+ }
13
+
14
+ @media (pointer: fine) {
15
+ .AppMenuSubheader-root {
16
+ line-height: 36px;
17
+ }
18
+ }
@@ -0,0 +1,14 @@
1
+ import "./AppMenuSubheader.css";
2
+
3
+ import clsx from "clsx";
4
+ import type React from "react";
5
+
6
+ export type AppMenuSubheaderProps = React.HTMLAttributes<HTMLDivElement>;
7
+
8
+ export function AppMenuSubheader({ children, className, ...props }: AppMenuSubheaderProps) {
9
+ return (
10
+ <div className={clsx("AppMenuSubheader-root", className)} {...props}>
11
+ {children}
12
+ </div>
13
+ );
14
+ }
@@ -0,0 +1,101 @@
1
+ .AppErrorBoundary-root {
2
+ height: 100vh;
3
+ display: flex;
4
+ align-items: center;
5
+ justify-content: center;
6
+ }
7
+
8
+ .AppErrorBoundary-inner {
9
+ max-width: 36rem;
10
+ width: 100%;
11
+ margin: 0 auto;
12
+ padding: 1rem;
13
+ text-align: center;
14
+ }
15
+
16
+ .AppErrorBoundary-image {
17
+ opacity: 0.6;
18
+ max-height: 30vh;
19
+ max-width: 50vw;
20
+ }
21
+
22
+ .AppErrorBoundary-title {
23
+ margin-top: var(--ds-size-6, 1.5rem);
24
+ font-size: var(--ds-font-size-6, 1.5rem);
25
+ font-weight: 700;
26
+ }
27
+
28
+ .AppErrorBoundary-alert {
29
+ margin-block: var(--ds-size-6, 1.5rem);
30
+ background: var(--ds-color-danger-surface-default, #fde8e8);
31
+ color: var(--ds-color-danger-text-default, #c01c2c);
32
+ border: 1px solid var(--ds-color-danger-border-default, #f5a6ae);
33
+ border-radius: var(--ds-border-radius-md, 0.25rem);
34
+ padding: var(--ds-size-3, 0.75rem) var(--ds-size-4, 1rem);
35
+ text-align: left;
36
+ }
37
+
38
+ .AppErrorBoundary-message {
39
+ margin-bottom: var(--ds-size-6, 1.5rem);
40
+ }
41
+
42
+ .AppErrorBoundary-reload {
43
+ appearance: none;
44
+ border: 1px solid currentColor;
45
+ border-radius: var(--ds-border-radius-md, 0.25rem);
46
+ background: transparent;
47
+ color: inherit;
48
+ cursor: pointer;
49
+ font: inherit;
50
+ font-size: var(--ds-font-size-3, 1rem);
51
+ padding: var(--ds-size-2, 0.5rem) var(--ds-size-4, 1rem);
52
+ margin-block: var(--ds-size-6, 1.5rem);
53
+ }
54
+
55
+ .AppErrorBoundary-reload:hover {
56
+ background: rgb(0 0 0 / 0.05);
57
+ }
58
+
59
+ .ErrorAlertFallback-root {
60
+ display: flex;
61
+ align-items: flex-start;
62
+ gap: var(--ds-size-2, 0.5rem);
63
+ background: var(--ds-color-danger-surface-default, #fde8e8);
64
+ color: var(--ds-color-danger-text-default, #c01c2c);
65
+ border: 1px solid var(--ds-color-danger-border-default, #f5a6ae);
66
+ border-radius: var(--ds-border-radius-md, 0.25rem);
67
+ padding: var(--ds-size-3, 0.75rem) var(--ds-size-4, 1rem);
68
+ }
69
+
70
+ .ErrorAlertFallback-content {
71
+ flex: 1;
72
+ }
73
+
74
+ .ErrorAlertFallback-title {
75
+ font-weight: 600;
76
+ margin-bottom: var(--ds-size-1, 0.25rem);
77
+ }
78
+
79
+ .ErrorAlertFallback-reload {
80
+ appearance: none;
81
+ background: transparent;
82
+ border: none;
83
+ color: inherit;
84
+ cursor: pointer;
85
+ font: inherit;
86
+ font-weight: 600;
87
+ padding: 0;
88
+ text-decoration: underline;
89
+ flex-shrink: 0;
90
+ }
91
+
92
+ .RouteErrorPage-stack {
93
+ background: rgb(0 0 0 / 0.07);
94
+ border: 2px solid rgb(0 0 0 / 0.15);
95
+ border-radius: var(--ds-border-radius-md, 0.25rem);
96
+ padding: var(--ds-size-4, 1rem);
97
+ overflow-x: auto;
98
+ white-space: pre-wrap;
99
+ word-break: break-all;
100
+ font-size: var(--ds-font-size-1, 0.75rem);
101
+ }
@@ -0,0 +1,162 @@
1
+ import "./ErrorBoundary.css";
2
+
3
+ import { getLocalizedString } from "@olenbetong/appframe-core";
4
+ import type { DataObject } from "@olenbetong/appframe-data";
5
+ import { useError } from "@olenbetong/appframe-react";
6
+
7
+ import { Component, useEffect } from "react";
8
+ import { useAsyncError, useRouteError } from "react-router";
9
+
10
+ import { PageContainer } from "./PageContainer.js";
11
+
12
+ function parseError(error: unknown) {
13
+ let message = "Unspecified error";
14
+ let stackTrace: string | null = null;
15
+ let title: string = getLocalizedString("Unexpected application error");
16
+
17
+ if (typeof error === "string") {
18
+ message = error;
19
+ } else if (error && typeof error === "object") {
20
+ const err = error as any;
21
+ if (err.status) {
22
+ title = `${err.status} ${err.statusText}`;
23
+ message = err.message ?? "";
24
+ } else if (err.message) {
25
+ message = err.message;
26
+ if (err.type) {
27
+ message = `${err.type}: ${message}`;
28
+ }
29
+ } else {
30
+ message = String(error);
31
+ }
32
+
33
+ if (err.stack || err.stackTrace) {
34
+ stackTrace = err.stack ?? err.stackTrace;
35
+ }
36
+ }
37
+
38
+ return { message, stackTrace, title };
39
+ }
40
+
41
+ // ── AppErrorBoundary ──────────────────────────────────────────────────────────
42
+
43
+ export type AppErrorBoundaryProps = {
44
+ children?: React.ReactNode;
45
+ message?: string | ((error: unknown) => string);
46
+ imgSrc?: string;
47
+ image?: React.ReactNode;
48
+ };
49
+
50
+ type AppErrorBoundaryState = {
51
+ hasError: boolean;
52
+ error: unknown;
53
+ };
54
+
55
+ export class AppErrorBoundary extends Component<AppErrorBoundaryProps, AppErrorBoundaryState> {
56
+ constructor(props: AppErrorBoundaryProps) {
57
+ super(props);
58
+ this.state = { hasError: false, error: null };
59
+ }
60
+
61
+ static getDerivedStateFromError(error: unknown): AppErrorBoundaryState {
62
+ return { hasError: true, error };
63
+ }
64
+
65
+ componentDidCatch(error: unknown, errorInfo: unknown) {
66
+ console.error(error, errorInfo);
67
+ }
68
+
69
+ render(): React.ReactNode {
70
+ if (this.state.hasError) {
71
+ const { imgSrc, message, image } = this.props;
72
+ const { message: errorMessage } = parseError(this.state.error);
73
+ const customMessage = typeof message === "function" ? message(this.state.error) : message;
74
+
75
+ return (
76
+ <div className="AppErrorBoundary-root">
77
+ <div className="AppErrorBoundary-inner">
78
+ {image ?? (
79
+ <img
80
+ alt=""
81
+ src={imgSrc ?? "/file/site/graphic/error-broken-robot.svg"}
82
+ className="AppErrorBoundary-image"
83
+ />
84
+ )}
85
+ <h1 className="AppErrorBoundary-title">{getLocalizedString("Something went terribly wrong!")}</h1>
86
+ <div className="AppErrorBoundary-alert">{errorMessage}</div>
87
+ <p className="AppErrorBoundary-message">
88
+ {customMessage ??
89
+ getLocalizedString(
90
+ "Sometimes it is enough to simply refresh the page. If the error persists, contact the IT department.",
91
+ )}
92
+ </p>
93
+ <button type="button" className="AppErrorBoundary-reload" onClick={() => window.location.reload()}>
94
+ {getLocalizedString("Refresh page")}
95
+ </button>
96
+ </div>
97
+ </div>
98
+ );
99
+ }
100
+
101
+ return this.props.children;
102
+ }
103
+ }
104
+
105
+ // ── ErrorAlertFallback ────────────────────────────────────────────────────────
106
+
107
+ export type ErrorAlertFallbackProps = {
108
+ error: unknown;
109
+ className?: string;
110
+ };
111
+
112
+ export function ErrorAlertFallback({ error, className }: ErrorAlertFallbackProps) {
113
+ const message = error instanceof Error ? error.message : String(error);
114
+
115
+ return (
116
+ <div className={`ErrorAlertFallback-root${className ? ` ${className}` : ""}`}>
117
+ <div className="ErrorAlertFallback-content">
118
+ <p className="ErrorAlertFallback-title">{getLocalizedString("An error occured. Try refreshing the page")}</p>
119
+ <p>{message}</p>
120
+ </div>
121
+ <button type="button" className="ErrorAlertFallback-reload" onClick={() => window.location.reload()}>
122
+ {getLocalizedString("Reload page")}
123
+ </button>
124
+ </div>
125
+ );
126
+ }
127
+
128
+ // ── RouteErrorPage ────────────────────────────────────────────────────────────
129
+
130
+ export function RouteErrorPage() {
131
+ const error = useRouteError();
132
+ const { message, stackTrace, title } = parseError(error);
133
+
134
+ useEffect(() => {
135
+ (globalThis as any).af?.common?.logError?.(error);
136
+ }, [error]);
137
+
138
+ return (
139
+ <PageContainer className="flow">
140
+ <h1>{title}</h1>
141
+ {message && <p>{message}</p>}
142
+ {stackTrace && <pre className="RouteErrorPage-stack">{stackTrace}</pre>}
143
+ </PageContainer>
144
+ );
145
+ }
146
+
147
+ // ── AsyncErrorAlert ───────────────────────────────────────────────────────────
148
+
149
+ export function AsyncErrorAlert() {
150
+ const error = useAsyncError();
151
+ const message = typeof error === "string" ? error : (error as any)?.message;
152
+
153
+ return <div className="ErrorAlertFallback-root">{message}</div>;
154
+ }
155
+
156
+ // ── DataObjectErrorAlert ──────────────────────────────────────────────────────
157
+
158
+ export function DataObjectErrorAlert({ dataObject }: { dataObject: DataObject<any> }) {
159
+ const error = useError(dataObject);
160
+
161
+ return error ? <div className="ErrorAlertFallback-root">{error}</div> : null;
162
+ }
@@ -0,0 +1,29 @@
1
+ @media screen {
2
+ .ObPageContainer-root {
3
+ max-height: 100%;
4
+ overflow: auto;
5
+ height: auto;
6
+ }
7
+
8
+ .ObPageContainer-root.ObPageContainer-fullHeight {
9
+ height: 100%;
10
+ }
11
+
12
+ .ObPageContainer-inner {
13
+ margin-left: 0;
14
+ height: auto;
15
+ padding: 1rem;
16
+ box-sizing: border-box;
17
+ max-width: 100%;
18
+ }
19
+
20
+ .ObPageContainer-fullHeight .ObPageContainer-inner {
21
+ height: 100%;
22
+ }
23
+ }
24
+
25
+ @media screen and (min-width: 600px) {
26
+ .ObPageContainer-inner {
27
+ padding: 1rem 1.5rem;
28
+ }
29
+ }
@@ -0,0 +1,19 @@
1
+ import "./PageContainer.css";
2
+
3
+ import clsx from "clsx";
4
+ import type React from "react";
5
+
6
+ export type PageContainerProps = React.HTMLAttributes<HTMLDivElement> & {
7
+ fullHeight?: boolean;
8
+ maxWidth?: "xs" | "sm" | "md" | "lg" | "xl" | false;
9
+ };
10
+
11
+ export function PageContainer({ children, className, fullHeight, maxWidth, ...props }: PageContainerProps) {
12
+ return (
13
+ <div className={clsx("ObPageContainer-root", fullHeight && "ObPageContainer-fullHeight")}>
14
+ <div className={clsx("ObPageContainer-inner", className)} {...props}>
15
+ {children}
16
+ </div>
17
+ </div>
18
+ );
19
+ }
@@ -0,0 +1,23 @@
1
+ import { createPortal } from "react-dom";
2
+ import type React from "react";
3
+
4
+ let toggleRoot = document.createElement("div");
5
+ let brand = document.querySelector(".partner-nav__brand");
6
+ toggleRoot.id = "toggle-root";
7
+
8
+ document.querySelector<HTMLDivElement>(".partner-nav")?.insertBefore(toggleRoot, brand);
9
+
10
+ export function PartnerToggle({
11
+ onClick,
12
+ open,
13
+ }: {
14
+ open: boolean;
15
+ onClick?: React.MouseEventHandler<HTMLButtonElement>;
16
+ }): React.ReactNode {
17
+ return createPortal(
18
+ <button type="button" className="LayoutSmall-toggle" onClick={onClick}>
19
+ {open ? "◀" : "☰"}
20
+ </button>,
21
+ toggleRoot,
22
+ );
23
+ }
@@ -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,62 @@
1
+ import "./index.css";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import type React from "react";
5
+
6
+ import { AppLayoutDesktop } from "./AppLayoutDesktop.js";
7
+ import { AppLayoutMobile } from "./AppLayoutMobile.js";
8
+ import type { AppMenuProps } from "./AppMenu.js";
9
+
10
+ export type AppLayoutProps = {
11
+ AppMenuProps?: AppMenuProps;
12
+ children: React.ReactNode | React.ReactNode[];
13
+ menu?: React.ReactNode | React.ReactNode[];
14
+ /**
15
+ * @deprecated The toolbar is now server-rendered. This prop has no effect.
16
+ */
17
+ toolbar?: React.ReactNode | React.ReactNode[];
18
+ /**
19
+ * @deprecated The title is now rendered in the server-rendered toolbar. This prop has no effect.
20
+ */
21
+ title?: React.ReactNode;
22
+ /**
23
+ * @deprecated The toolbar is now server-rendered. This prop has no effect.
24
+ */
25
+ enableMenuToggle?: boolean;
26
+ /**
27
+ * Enable partner mode (for partner.olenbetong.no applications).
28
+ * Adds a toggle button to the existing partner header instead of the app bar.
29
+ */
30
+ partner?: boolean;
31
+ };
32
+
33
+ function useIsMobile() {
34
+ let [isMobile, setIsMobile] = useState(() => {
35
+ if (typeof window === "undefined") return false;
36
+ return window.matchMedia("(max-width: 1199px)").matches;
37
+ });
38
+
39
+ useEffect(() => {
40
+ let mq = window.matchMedia("(max-width: 1199px)");
41
+ let handler = (e: MediaQueryListEvent) => setIsMobile(e.matches);
42
+ mq.addEventListener("change", handler);
43
+ return () => mq.removeEventListener("change", handler);
44
+ }, []);
45
+
46
+ return isMobile;
47
+ }
48
+
49
+ export * from "./AppMenu.js";
50
+ export * from "./AppMenuItem.js";
51
+ export * from "./AppMenuSubheader.js";
52
+ export * from "./constants.js";
53
+ export * from "./ErrorBoundary.js";
54
+ export * from "./PageContainer.js";
55
+ export * from "./PartnerToggle.js";
56
+
57
+ export function AppLayout(props: AppLayoutProps) {
58
+ let isSmall = useIsMobile();
59
+ let Component = isSmall ? AppLayoutMobile : AppLayoutDesktop;
60
+
61
+ return <Component {...props} />;
62
+ }