@olenbetong/appframe-ds 0.4.0 → 0.5.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 (112) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/package.json +10 -10
  3. package/scripts/copyAssets.mjs +30 -0
  4. package/src/AfLookup/Combobox.tsx +16 -1
  5. package/src/AfLookup/Lookup.tsx +37 -6
  6. package/src/index.ts +3 -0
  7. package/src/layout/AppLayoutDesktop.tsx +3 -0
  8. package/src/layout/AppLayoutMobile.tsx +3 -0
  9. package/src/layout/AppMenu.css +1 -0
  10. package/src/layout/PageContainer.tsx +6 -0
  11. package/src/layout/index.tsx +5 -0
  12. package/src/list/ItemList.css +30 -0
  13. package/src/list/ItemList.tsx +52 -0
  14. package/src/list/ItemListItem.tsx +41 -0
  15. package/src/list/ItemListItemText.tsx +23 -0
  16. package/src/list/index.ts +3 -0
  17. package/src/page/Page.css +45 -0
  18. package/src/page/Page.tsx +70 -0
  19. package/src/page/PageBlock.css +59 -0
  20. package/src/page/PageBlock.tsx +78 -0
  21. package/src/page/PageHeader.css +148 -0
  22. package/src/page/PageHeader.tsx +184 -0
  23. package/src/page/index.ts +3 -0
  24. package/src/paper/Paper.css +35 -0
  25. package/src/paper/Paper.tsx +50 -0
  26. package/src/paper/index.ts +1 -0
  27. package/es/AfLookup/BoundLookup.d.ts +0 -10
  28. package/es/AfLookup/BoundLookup.js +0 -16
  29. package/es/AfLookup/Combobox.css +0 -78
  30. package/es/AfLookup/Combobox.d.ts +0 -31
  31. package/es/AfLookup/Combobox.js +0 -71
  32. package/es/AfLookup/Lookup.css +0 -125
  33. package/es/AfLookup/Lookup.d.ts +0 -35
  34. package/es/AfLookup/Lookup.js +0 -42
  35. package/es/AfLookup/LookupEdit.d.ts +0 -21
  36. package/es/AfLookup/LookupEdit.js +0 -49
  37. package/es/AfLookup/LookupGrid.css +0 -57
  38. package/es/AfLookup/LookupGrid.d.ts +0 -13
  39. package/es/AfLookup/LookupGrid.js +0 -27
  40. package/es/AfLookup/index.d.ts +0 -5
  41. package/es/AfLookup/index.js +0 -5
  42. package/es/binding/BoundCheckbox.d.ts +0 -9
  43. package/es/binding/BoundCheckbox.js +0 -13
  44. package/es/binding/BoundDatePicker.d.ts +0 -7
  45. package/es/binding/BoundDatePicker.js +0 -12
  46. package/es/binding/BoundDateTimePicker.d.ts +0 -7
  47. package/es/binding/BoundDateTimePicker.js +0 -14
  48. package/es/binding/BoundInput.d.ts +0 -6
  49. package/es/binding/BoundInput.js +0 -12
  50. package/es/binding/BoundNumericTextField.d.ts +0 -20
  51. package/es/binding/BoundNumericTextField.js +0 -58
  52. package/es/binding/BoundSelect.d.ts +0 -9
  53. package/es/binding/BoundSelect.js +0 -13
  54. package/es/binding/BoundSwitch.d.ts +0 -9
  55. package/es/binding/BoundSwitch.js +0 -13
  56. package/es/binding/BoundTextField.d.ts +0 -7
  57. package/es/binding/BoundTextField.js +0 -22
  58. package/es/binding/CancelButton.d.ts +0 -3
  59. package/es/binding/CancelButton.js +0 -14
  60. package/es/binding/CancelIconButton.d.ts +0 -2
  61. package/es/binding/CancelIconButton.js +0 -15
  62. package/es/binding/DataEditToolbar.d.ts +0 -13
  63. package/es/binding/DataEditToolbar.js +0 -28
  64. package/es/binding/DataObjectSelect.d.ts +0 -50
  65. package/es/binding/DataObjectSelect.js +0 -26
  66. package/es/binding/DeleteButton.d.ts +0 -8
  67. package/es/binding/DeleteButton.js +0 -14
  68. package/es/binding/DeleteIconButton.d.ts +0 -10
  69. package/es/binding/DeleteIconButton.js +0 -15
  70. package/es/binding/RefreshButton.d.ts +0 -3
  71. package/es/binding/RefreshButton.js +0 -14
  72. package/es/binding/RefreshIconButton.d.ts +0 -2
  73. package/es/binding/RefreshIconButton.js +0 -16
  74. package/es/binding/RefreshRowIconButton.d.ts +0 -7
  75. package/es/binding/RefreshRowIconButton.js +0 -15
  76. package/es/binding/SaveButton.d.ts +0 -3
  77. package/es/binding/SaveButton.js +0 -14
  78. package/es/binding/SaveIconButton.d.ts +0 -2
  79. package/es/binding/SaveIconButton.js +0 -15
  80. package/es/binding/index.d.ts +0 -19
  81. package/es/binding/index.js +0 -19
  82. package/es/index.d.ts +0 -3
  83. package/es/index.js +0 -3
  84. package/es/layout/AppLayoutDesktop.css +0 -65
  85. package/es/layout/AppLayoutDesktop.d.ts +0 -3
  86. package/es/layout/AppLayoutDesktop.js +0 -19
  87. package/es/layout/AppLayoutMobile.css +0 -75
  88. package/es/layout/AppLayoutMobile.d.ts +0 -3
  89. package/es/layout/AppLayoutMobile.js +0 -18
  90. package/es/layout/AppMenu.css +0 -67
  91. package/es/layout/AppMenu.d.ts +0 -8
  92. package/es/layout/AppMenu.js +0 -36
  93. package/es/layout/AppMenuItem.css +0 -97
  94. package/es/layout/AppMenuItem.d.ts +0 -50
  95. package/es/layout/AppMenuItem.js +0 -14
  96. package/es/layout/AppMenuSubheader.css +0 -18
  97. package/es/layout/AppMenuSubheader.d.ts +0 -4
  98. package/es/layout/AppMenuSubheader.js +0 -6
  99. package/es/layout/ErrorBoundary.css +0 -101
  100. package/es/layout/ErrorBoundary.d.ts +0 -30
  101. package/es/layout/ErrorBoundary.js +0 -81
  102. package/es/layout/PageContainer.css +0 -29
  103. package/es/layout/PageContainer.d.ts +0 -7
  104. package/es/layout/PageContainer.js +0 -6
  105. package/es/layout/PartnerToggle.d.ts +0 -5
  106. package/es/layout/PartnerToggle.js +0 -9
  107. package/es/layout/constants.d.ts +0 -2
  108. package/es/layout/constants.js +0 -2
  109. package/es/layout/index.css +0 -65
  110. package/es/layout/index.d.ts +0 -33
  111. package/es/layout/index.js +0 -31
  112. package/tsconfig.build.tsbuildinfo +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # @olenbetong/appframe-ds
2
2
 
3
+ ## 0.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - a118bef: Add Aksel-inspired `Page` primitives (`Page`, `Page.Block`, `Page.Header`) replacing the now-deprecated `AppLayout`, `AppLayoutDesktop`, `AppLayoutMobile` and `PageContainer`. `Page` supports both normal document scrolling and full-viewport app layouts (`layout="app"`, replacing `fullHeight`), `Page.Block` handles max-width/gutters/centering, and `Page.Header` comes in `appbar` (with responsive app menu drawer) and `plain` variants — both prepared for the upcoming sidebar-free site layout.
8
+
9
+ Add a `Paper` surface component (MUI `Paper` equivalent): neutral surface background, rounded corners and `--ds-shadow-*` elevation, with an optional `border` for an outlined variant. Pages using the `Page` primitive get a tinted body background (`--ds-color-neutral-background-tinted`) so surfaces stand out, matching the old site design.
10
+
11
+ Add `ItemList` components (`ItemList`, `ItemList.Item`, `ItemList.ItemText`) equivalent to MUI `List`/`ListItem`/`ListItemText`, for flex-row lists with dividers and primary/secondary text — typically rendered inside a `Paper` (`<Paper as={ItemList}>`).
12
+
13
+ ## 0.4.1
14
+
15
+ ### Patch Changes
16
+
17
+ - f7f6cea: AfLookup now supports typeahead from the input field: typing opens the lookup popup and forwards the typed character to the combobox search field.
18
+
3
19
  ## 0.4.0
4
20
 
5
21
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@olenbetong/appframe-ds",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Components that bind Designsystemet components to Appframe data objects",
5
5
  "type": "module",
6
6
  "types": "./es/index.d.ts",
@@ -17,15 +17,15 @@
17
17
  "peerDependencies": {
18
18
  "@digdir/designsystemet-react": ">=1.18.0",
19
19
  "@mui/icons-material": ">=7.0.0",
20
- "react": ">=19.2.7",
21
- "react-dom": ">=19.2.7",
22
- "react-error-boundary": ">=5.0.0",
23
- "react-router": ">=7.0.0"
20
+ "react": ">=19.2.8",
21
+ "react-dom": ">=19.2.8",
22
+ "react-error-boundary": ">=6.1.2",
23
+ "react-router": ">=8.3.0"
24
24
  },
25
25
  "dependencies": {
26
26
  "@olenbetong/appframe-core": "2.11.10",
27
27
  "@olenbetong/appframe-data": "1.5.0",
28
- "@olenbetong/appframe-react": "1.22.0",
28
+ "@olenbetong/appframe-react": "1.23.0",
29
29
  "react-window": "^1.8.11",
30
30
  "react-virtualized-auto-sizer": "^1.0.26",
31
31
  "clsx": "^2.1.1"
@@ -36,16 +36,16 @@
36
36
  "@types/react": "19.2.17",
37
37
  "@types/react-dom": "19.2.3",
38
38
  "@types/react-window": "^1.8.8",
39
- "react": "19.2.7",
40
- "react-dom": "19.2.7",
39
+ "react": "19.2.8",
40
+ "react-dom": "19.2.8",
41
41
  "react-error-boundary": "6.1.2",
42
- "react-router": "7.17.0",
42
+ "react-router": "8.3.0",
43
43
  "typescript": "7.0.2"
44
44
  },
45
45
  "optionalDependencies": {},
46
46
  "scripts": {
47
47
  "build": "pnpm run build:tsc && pnpm run build:css",
48
48
  "build:tsc": "node --no-warnings ../../scripts/clean.ts ./es tsconfig.build.tsbuildinfo && tsc -p tsconfig.build.json",
49
- "build:css": "cp src/AfLookup/Lookup.css es/AfLookup/Lookup.css && cp src/AfLookup/Combobox.css es/AfLookup/Combobox.css && cp src/AfLookup/LookupGrid.css es/AfLookup/LookupGrid.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"
49
+ "build:css": "node scripts/copyAssets.mjs"
50
50
  }
51
51
  }
@@ -0,0 +1,30 @@
1
+ import { cpSync, mkdirSync } from "node:fs";
2
+
3
+ const files = [
4
+ ["src/AfLookup/Lookup.css", "es/AfLookup/Lookup.css"],
5
+ ["src/AfLookup/LookupGrid.css", "es/AfLookup/LookupGrid.css"],
6
+ ["src/AfLookup/Combobox.css", "es/AfLookup/Combobox.css"],
7
+ ["src/layout/AppMenu.css", "es/layout/AppMenu.css"],
8
+ ["src/layout/AppMenuItem.css", "es/layout/AppMenuItem.css"],
9
+ ["src/layout/AppMenuSubheader.css", "es/layout/AppMenuSubheader.css"],
10
+ ["src/layout/AppLayoutDesktop.css", "es/layout/AppLayoutDesktop.css"],
11
+ ["src/layout/AppLayoutMobile.css", "es/layout/AppLayoutMobile.css"],
12
+ ["src/layout/index.css", "es/layout/index.css"],
13
+ ["src/layout/ErrorBoundary.css", "es/layout/ErrorBoundary.css"],
14
+ ["src/layout/PageContainer.css", "es/layout/PageContainer.css"],
15
+ ["src/page/Page.css", "es/page/Page.css"],
16
+ ["src/page/PageBlock.css", "es/page/PageBlock.css"],
17
+ ["src/page/PageHeader.css", "es/page/PageHeader.css"],
18
+ ["src/list/ItemList.css", "es/list/ItemList.css"],
19
+ ["src/paper/Paper.css", "es/paper/Paper.css"],
20
+ ];
21
+
22
+ mkdirSync("es/layout", { recursive: true });
23
+ mkdirSync("es/list", { recursive: true });
24
+ mkdirSync("es/page", { recursive: true });
25
+ mkdirSync("es/paper", { recursive: true });
26
+
27
+ for (let [src, dest] of files) {
28
+ cpSync(src, dest);
29
+ console.log(`Copied ${src} → ${dest}`);
30
+ }
@@ -15,6 +15,7 @@ export interface ComboboxBaseProps<T extends Record<string, unknown>> {
15
15
  nullable?: boolean;
16
16
  dataObject: DataObject<T>;
17
17
  uniqueIdField?: string;
18
+ initialSearchValue?: string;
18
19
  isItemSelected?: (item: T) => boolean;
19
20
  onChange?: (item: T | null) => void;
20
21
  value?: T;
@@ -41,7 +42,15 @@ export interface ComboboxGridProps<T extends Record<string, unknown>> extends Co
41
42
  export type ComboboxProps<T extends Record<string, unknown>> = ComboboxListProps<T> | ComboboxGridProps<T>;
42
43
 
43
44
  export function Combobox<T extends Record<string, unknown>>(props: ComboboxProps<T>) {
44
- let { nullable, dataObject, isItemSelected, uniqueIdField = "PrimKey", onChange, onClose } = props;
45
+ let {
46
+ nullable,
47
+ dataObject,
48
+ isItemSelected,
49
+ uniqueIdField = "PrimKey",
50
+ initialSearchValue,
51
+ onChange,
52
+ onClose,
53
+ } = props;
45
54
  let isGrid = props.variant === "grid";
46
55
  let columns = isGrid ? (props as ComboboxGridProps<T>).columns : undefined;
47
56
  let listRef = useRef<VirtualList>(null);
@@ -133,6 +142,12 @@ export function Combobox<T extends Record<string, unknown>>(props: ComboboxProps
133
142
 
134
143
  useEffect(() => listRef.current?.scrollToItem(currentIndex, "smart"), [currentIndex]);
135
144
 
145
+ useEffect(() => {
146
+ if (initialSearchValue !== undefined) {
147
+ setInputValue(initialSearchValue);
148
+ }
149
+ }, [initialSearchValue, setInputValue]);
150
+
136
151
  return (
137
152
  // oxlint-disable-next-line jsx-a11y/click-events-have-key-events -- element is not interactive, click is used to prevent dialog close
138
153
  // oxlint-disable-next-line jsx-a11y/no-static-element-interactions -- element is not interactive, click is used to prevent dialog close
@@ -7,6 +7,7 @@ import { getLocalizedString } from "@olenbetong/appframe-core";
7
7
  import { type DistributiveOmit, useLookupState } from "@olenbetong/appframe-react";
8
8
 
9
9
  import clsx from "clsx";
10
+ import { useRef } from "react";
10
11
  import { createPortal } from "react-dom";
11
12
 
12
13
  import { Combobox, type ComboboxProps } from "./Combobox.js";
@@ -42,15 +43,21 @@ export function useLookup<T extends Record<string, unknown>>({
42
43
  getAnchorElement,
43
44
  scrollParentSelector: "[data-virtualized-scroller]",
44
45
  });
46
+ let initialSearchValueRef = useRef<string | undefined>(undefined);
45
47
 
46
48
  function handleChange(item: T | null) {
47
49
  props.onChange?.(item);
48
50
  dialogRef.current?.close();
49
51
  }
50
52
 
53
+ function handleOpenDialog(searchValue?: string) {
54
+ initialSearchValueRef.current = searchValue;
55
+ openDialog();
56
+ }
57
+
51
58
  return {
52
59
  open,
53
- openDialog,
60
+ openDialog: handleOpenDialog,
54
61
  onKeyDown,
55
62
  onChange: handleChange,
56
63
  dialogRef,
@@ -63,7 +70,14 @@ export function useLookup<T extends Record<string, unknown>>({
63
70
  onKeyDown={(evt) => evt.stopPropagation()}
64
71
  style={{ positionAnchor: anchorName, ...slotProps?.dialog?.style } as React.CSSProperties}
65
72
  >
66
- {open && <Combobox {...props} onClose={() => dialogRef.current?.close()} onChange={handleChange} />}
73
+ {open && (
74
+ <Combobox
75
+ {...props}
76
+ initialSearchValue={initialSearchValueRef.current}
77
+ onClose={() => dialogRef.current?.close()}
78
+ onChange={handleChange}
79
+ />
80
+ )}
67
81
  </dialog>,
68
82
  document.body,
69
83
  inputId,
@@ -82,6 +96,18 @@ export function AfLookup<T extends Record<string, unknown>>({
82
96
 
83
97
  let hasClear = !!(props.nullable && value && props.onChange);
84
98
 
99
+ function handleInputKeyDown(event: React.KeyboardEvent<HTMLElement>) {
100
+ let hasModifier = event.ctrlKey || event.altKey || event.metaKey;
101
+ let isTypeaheadKey = event.key.length === 1 && !hasModifier && !event.nativeEvent.isComposing;
102
+ if (isTypeaheadKey) {
103
+ event.preventDefault();
104
+ event.stopPropagation();
105
+ openDialog(event.key);
106
+ return;
107
+ }
108
+ onKeyDown(event);
109
+ }
110
+
85
111
  return (
86
112
  <div style={{ anchorName } as React.CSSProperties}>
87
113
  <div
@@ -97,9 +123,8 @@ export function AfLookup<T extends Record<string, unknown>>({
97
123
  id={inputId}
98
124
  value={value ?? ""}
99
125
  label={label}
100
- readOnly
101
- onClick={openDialog}
102
- onKeyDown={onKeyDown}
126
+ onClick={() => openDialog()}
127
+ onKeyDown={handleInputKeyDown}
103
128
  className={clsx(slotProps?.textField?.className, "ObLookup-input")}
104
129
  {...(slotProps?.textField as any)}
105
130
  style={{ width: fullWidth ? "100%" : undefined, ...slotProps?.textField?.style }}
@@ -121,7 +146,13 @@ export function AfLookup<T extends Record<string, unknown>>({
121
146
  <ClearIcon />
122
147
  </Button>
123
148
  )}
124
- <Button icon variant="tertiary" data-size="sm" aria-label={getLocalizedString("Search")} onClick={openDialog}>
149
+ <Button
150
+ icon
151
+ variant="tertiary"
152
+ data-size="sm"
153
+ aria-label={getLocalizedString("Search")}
154
+ onClick={() => openDialog()}
155
+ >
125
156
  <SearchIcon />
126
157
  </Button>
127
158
  </div>
package/src/index.ts CHANGED
@@ -1,3 +1,6 @@
1
1
  export * from "./AfLookup/index.js";
2
2
  export * from "./binding/index.js";
3
3
  export * from "./layout/index.js";
4
+ export * from "./list/index.js";
5
+ export * from "./page/index.js";
6
+ export * from "./paper/index.js";
@@ -21,6 +21,9 @@ function LoadingFallback() {
21
21
  );
22
22
  }
23
23
 
24
+ /**
25
+ * @deprecated Use the `Page` primitives instead (`Page`, `Page.Header`, `Page.Block`).
26
+ */
24
27
  export function AppLayoutDesktop({ AppMenuProps, children, menu, partner }: AppLayoutProps) {
25
28
  let [open, setOpen] = useState(true);
26
29
 
@@ -15,6 +15,9 @@ function Toggle({ onToggle }: { onToggle: () => void }) {
15
15
  );
16
16
  }
17
17
 
18
+ /**
19
+ * @deprecated Use the `Page` primitives instead (`Page`, `Page.Header`, `Page.Block`).
20
+ */
18
21
  export function AppLayoutMobile({ AppMenuProps, children, menu, partner }: AppLayoutProps) {
19
22
  let [drawerOpen, setDrawerOpen] = useState(false);
20
23
  let location = useLocation();
@@ -7,6 +7,7 @@
7
7
  letter-spacing: -0.02rem;
8
8
  font-size: var(--ds-font-size-xl, 1.5rem);
9
9
  margin: 0;
10
+ color: var(--ds-color-neutral-text-default, #1e2b3c);
10
11
  }
11
12
 
12
13
  .AppMenu-header .AppMenu-subTitle {
@@ -8,6 +8,12 @@ export type PageContainerProps = React.HTMLAttributes<HTMLDivElement> & {
8
8
  maxWidth?: "xs" | "sm" | "md" | "lg" | "xl" | false;
9
9
  };
10
10
 
11
+ /**
12
+ * @deprecated Use `Page.Block` instead. `maxWidth` mapping: `xs`/`sm` →
13
+ * `width="text"`, `md` → `"md"`, `lg` → `"lg"`, `xl` → `"xl"`, `false` → omit
14
+ * `width`. Replace `fullHeight` with `<Page layout="app">` and
15
+ * `<Page.Block grow overflow="auto">`.
16
+ */
11
17
  export function PageContainer({ children, className, fullHeight, maxWidth, ...props }: PageContainerProps) {
12
18
  return (
13
19
  <div className={clsx("ObPageContainer-root", fullHeight && "ObPageContainer-fullHeight")}>
@@ -54,6 +54,11 @@ export * from "./ErrorBoundary.js";
54
54
  export * from "./PageContainer.js";
55
55
  export * from "./PartnerToggle.js";
56
56
 
57
+ /**
58
+ * @deprecated Use the `Page` primitives instead (`Page`, `Page.Header`,
59
+ * `Page.Block`). `<AppLayout menu={...} title={...}>` becomes
60
+ * `<Page layout="app"><Page.Header title={...} menu={...} />...</Page>`.
61
+ */
57
62
  export function AppLayout(props: AppLayoutProps) {
58
63
  let isSmall = useIsMobile();
59
64
  let Component = isSmall ? AppLayoutMobile : AppLayoutDesktop;
@@ -0,0 +1,30 @@
1
+ .ObItemList-root {
2
+ list-style: none;
3
+ margin: 0;
4
+ padding: 0;
5
+ }
6
+
7
+ .ObItemList-dividers > .ObItemListItem-root:not(:last-child) {
8
+ border-bottom: var(--ds-border-width-default, 1px) solid var(--ds-color-neutral-border-subtle, #dfdfdf);
9
+ }
10
+
11
+ .ObItemListItem-root {
12
+ display: flex;
13
+ align-items: center;
14
+ gap: var(--ds-size-3, 0.75rem);
15
+ padding: var(--ds-size-2, 0.5rem) var(--ds-size-3, 0.75rem);
16
+ }
17
+
18
+ .ObItemListItem-disablePadding {
19
+ padding: 0;
20
+ }
21
+
22
+ .ObItemListItemText-root {
23
+ flex: 1 1 auto;
24
+ min-width: 0;
25
+ }
26
+
27
+ .ObItemListItemText-secondary {
28
+ font-size: 0.875rem;
29
+ color: var(--ds-color-neutral-text-subtle, #666);
30
+ }
@@ -0,0 +1,52 @@
1
+ import "./ItemList.css";
2
+
3
+ import clsx from "clsx";
4
+ import type React from "react";
5
+
6
+ import { ItemListItem } from "./ItemListItem.js";
7
+ import { ItemListItemText } from "./ItemListItemText.js";
8
+
9
+ export type ItemListProps = React.HTMLAttributes<HTMLElement> & {
10
+ /**
11
+ * Overrides the rendered element (e.g. `"ol"`, `"menu"`)
12
+ * @default "ul"
13
+ */
14
+ as?: React.ElementType;
15
+ children?: React.ReactNode;
16
+ /**
17
+ * Renders a divider between items
18
+ * @default true
19
+ */
20
+ dividers?: boolean;
21
+ };
22
+
23
+ function ItemListComponent({ as: Component = "ul", children, className, dividers = true, ...props }: ItemListProps) {
24
+ return (
25
+ <Component className={clsx("ObItemList-root", dividers && "ObItemList-dividers", className)} {...props}>
26
+ {children}
27
+ </Component>
28
+ );
29
+ }
30
+
31
+ /**
32
+ * List of flex-row items, equivalent to MUI `List`/`ListItem`. Named
33
+ * `ItemList` to avoid clashing with the designsystemet `List` component
34
+ * (which renders regular text lists). Typically used inside a `Paper`.
35
+ *
36
+ * @example
37
+ * ```jsx
38
+ * <Paper as={ItemList}>
39
+ * {records.map((record) => (
40
+ * <ItemList.Item key={record.PrimKey}>
41
+ * <img src="..." alt="" />
42
+ * <ItemList.ItemText primary={record.Title} secondary={record.Slug} />
43
+ * <Button>...</Button>
44
+ * </ItemList.Item>
45
+ * ))}
46
+ * </Paper>
47
+ * ```
48
+ */
49
+ export const ItemList = Object.assign(ItemListComponent, {
50
+ Item: ItemListItem,
51
+ ItemText: ItemListItemText,
52
+ });
@@ -0,0 +1,41 @@
1
+ import clsx from "clsx";
2
+ import type React from "react";
3
+
4
+ export type ItemListItemProps = React.HTMLAttributes<HTMLElement> & {
5
+ /**
6
+ * Overrides the rendered element
7
+ * @default "li"
8
+ */
9
+ as?: React.ElementType;
10
+ children?: React.ReactNode;
11
+ ref?: React.Ref<HTMLElement>;
12
+ /**
13
+ * Removes the inline/block padding, e.g. when the item content is a
14
+ * link or button that should span the full row (add the padding on the
15
+ * interactive child instead so the whole row is clickable).
16
+ * @default false
17
+ */
18
+ disablePadding?: boolean;
19
+ };
20
+
21
+ /**
22
+ * A single flex-row item inside an `ItemList`. Children are laid out
23
+ * horizontally with a standard gap; use `ItemList.ItemText` for the
24
+ * primary/secondary text block.
25
+ */
26
+ export function ItemListItem({
27
+ as: Component = "li",
28
+ children,
29
+ className,
30
+ disablePadding,
31
+ ...props
32
+ }: ItemListItemProps) {
33
+ return (
34
+ <Component
35
+ className={clsx("ObItemListItem-root", disablePadding && "ObItemListItem-disablePadding", className)}
36
+ {...props}
37
+ >
38
+ {children}
39
+ </Component>
40
+ );
41
+ }
@@ -0,0 +1,23 @@
1
+ import clsx from "clsx";
2
+ import type React from "react";
3
+
4
+ export type ItemListItemTextProps = React.HTMLAttributes<HTMLElement> & {
5
+ children?: React.ReactNode;
6
+ /** Primary line. `children` can be used instead. */
7
+ primary?: React.ReactNode;
8
+ /** Secondary line, rendered smaller with subtle text color */
9
+ secondary?: React.ReactNode;
10
+ };
11
+
12
+ /**
13
+ * Text block inside an `ItemList.Item`. Fills the remaining row space and
14
+ * renders a primary and an optional secondary line, like MUI `ListItemText`.
15
+ */
16
+ export function ItemListItemText({ children, className, primary, secondary, ...props }: ItemListItemTextProps) {
17
+ return (
18
+ <div className={clsx("ObItemListItemText-root", className)} {...props}>
19
+ <div className="ObItemListItemText-primary">{primary ?? children}</div>
20
+ {secondary != null && <div className="ObItemListItemText-secondary">{secondary}</div>}
21
+ </div>
22
+ );
23
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./ItemList.js";
2
+ export * from "./ItemListItem.js";
3
+ export * from "./ItemListItemText.js";
@@ -0,0 +1,45 @@
1
+ @media screen {
2
+ /*
3
+ * Pages get a tinted background so surfaces (Paper, drawer, dialogs —
4
+ * which use neutral-background/surface-default) stand out, matching the
5
+ * old site design. Scoped to Page usage so the token itself is untouched.
6
+ */
7
+ body:has(.ObPage-root) {
8
+ background-color: var(--ds-color-neutral-background-tinted, #f3f3f4);
9
+ }
10
+
11
+ .ObPage-root {
12
+ display: flex;
13
+ flex-direction: column;
14
+ box-sizing: border-box;
15
+ }
16
+
17
+ .ObPage-document {
18
+ min-height: 100%;
19
+ }
20
+
21
+ .ObPage-app {
22
+ height: 100vh;
23
+ height: 100dvh;
24
+ overflow: hidden;
25
+ }
26
+
27
+ /* Offset content when a docked Page.Header drawer is open (desktop) */
28
+ .ObPage-root:has(.ObPageHeader-drawer.ObPageHeader-drawerDocked.ObPageHeader-drawerOpen) {
29
+ padding-left: var(--ob-page-drawer-width, 20rem);
30
+ transition: padding-left 225ms cubic-bezier(0.4, 0, 0.2, 1);
31
+ }
32
+ }
33
+
34
+ @media print {
35
+ .ObPage-root,
36
+ .ObPage-app {
37
+ display: block;
38
+ height: auto;
39
+ overflow: visible;
40
+ }
41
+
42
+ .ObPage-root:has(.ObPageHeader-drawer.ObPageHeader-drawerDocked.ObPageHeader-drawerOpen) {
43
+ padding-left: 0;
44
+ }
45
+ }
@@ -0,0 +1,70 @@
1
+ import "./Page.css";
2
+
3
+ import clsx from "clsx";
4
+ import type React from "react";
5
+
6
+ import { PageBlock } from "./PageBlock.js";
7
+ import { PageHeader } from "./PageHeader.js";
8
+
9
+ export type PageLayout = "document" | "app";
10
+
11
+ export type PageProps = React.HTMLAttributes<HTMLElement> & {
12
+ /**
13
+ * Overrides the rendered element
14
+ * @default "div"
15
+ */
16
+ as?: React.ElementType;
17
+ children?: React.ReactNode;
18
+ /**
19
+ * Top-level layout strategy for the page.
20
+ *
21
+ * - `"document"` (default): normal document flow — the page grows with its
22
+ * content and the document scrolls.
23
+ * - `"app"`: the page fills the available viewport height with
24
+ * `overflow: hidden`, and scrolling is delegated down the tree (use
25
+ * `Page.Block` with `grow`/`overflow="auto"`, or utilities like
26
+ * `.scroll-view`). Replaces the old `PageContainer` `fullHeight` prop.
27
+ *
28
+ * @default "document"
29
+ */
30
+ layout?: PageLayout;
31
+ };
32
+
33
+ function PageComponent({ as: Component = "div", children, className, layout = "document", ...props }: PageProps) {
34
+ return (
35
+ <Component
36
+ className={clsx("ObPage-root", layout === "app" ? "ObPage-app" : "ObPage-document", className)}
37
+ {...props}
38
+ >
39
+ {children}
40
+ </Component>
41
+ );
42
+ }
43
+
44
+ /**
45
+ * Top-level layout primitive for application pages, inspired by the Aksel
46
+ * `Page` primitives. Composes with `Page.Header` and `Page.Block`.
47
+ *
48
+ * Works both in the current sidebar-based site layout and the upcoming
49
+ * toolbar-based (sidebar-free) site layout.
50
+ *
51
+ * @example Document-style page (content scrolls with the document)
52
+ * ```jsx
53
+ * <Page>
54
+ * <Page.Header variant="plain" title="Report issue" />
55
+ * <Page.Block as="main" width="text" gutters>...</Page.Block>
56
+ * </Page>
57
+ * ```
58
+ *
59
+ * @example App-style page (fills the viewport, scrolling delegated down the tree)
60
+ * ```jsx
61
+ * <Page layout="app">
62
+ * <Page.Header title="Products" menu={<ProductsMenu />} />
63
+ * <Page.Block as="main" gutters grow overflow="auto">...</Page.Block>
64
+ * </Page>
65
+ * ```
66
+ */
67
+ export const Page = Object.assign(PageComponent, {
68
+ Block: PageBlock,
69
+ Header: PageHeader,
70
+ });
@@ -0,0 +1,59 @@
1
+ .ObPageBlock-root {
2
+ box-sizing: border-box;
3
+ width: 100%;
4
+ margin-inline: auto;
5
+ }
6
+
7
+ .ObPageBlock-text {
8
+ max-width: calc(36rem + 2 * var(--ob-page-gutter, 0rem));
9
+ }
10
+
11
+ .ObPageBlock-md {
12
+ max-width: 48rem;
13
+ }
14
+
15
+ .ObPageBlock-lg {
16
+ max-width: 64rem;
17
+ }
18
+
19
+ .ObPageBlock-xl {
20
+ max-width: 80rem;
21
+ }
22
+
23
+ .ObPageBlock-2xl {
24
+ max-width: 90rem;
25
+ }
26
+
27
+ .ObPageBlock-gutters {
28
+ --ob-page-gutter: 1rem;
29
+
30
+ padding-inline: var(--ob-page-gutter);
31
+ }
32
+
33
+ @media screen and (min-width: 600px) {
34
+ .ObPageBlock-gutters {
35
+ --ob-page-gutter: 1.5rem;
36
+ }
37
+ }
38
+
39
+ @media screen {
40
+ .ObPageBlock-grow {
41
+ flex: 1 1 auto;
42
+ min-height: 0;
43
+ }
44
+
45
+ .ObPageBlock-overflowAuto {
46
+ overflow: auto;
47
+ }
48
+
49
+ .ObPageBlock-overflowHidden {
50
+ overflow: hidden;
51
+ }
52
+ }
53
+
54
+ @media print {
55
+ .ObPageBlock-overflowAuto,
56
+ .ObPageBlock-overflowHidden {
57
+ overflow: visible;
58
+ }
59
+ }