@olenbetong/appframe-ds 0.5.0 → 0.6.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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # @olenbetong/appframe-ds
2
2
 
3
+ ## 0.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 0147e80: Add `LinearProgress` component: a designsystemet-styled linear progress bar equivalent to MUI `LinearProgress`, with indeterminate and determinate modes and an `overlay` prop for an absolutely positioned bar that does not affect the layout.
8
+ - 56080ed: Add `InputAdornments` and `InputSlot`: an input with adornments (MUI `TextField` start/end adornment equivalent) where the border, background, hover and focus styling lives on a wrapper around the input and its adornments. Adornments are passed as children, and `InputSlot` controls where the input is placed (defaults to the start).
9
+ - bc89c1e: Migrated all icons in appframe-ds from `@mui/icons-material` to `@navikt/aksel-icons`. Bound action buttons (`SaveButton`, `DeleteButton`, `CancelButton`, `RefreshButton` and their icon-only variants) now show a leading icon that is hidden while loading, so button width no longer changes when toggling the loading state. `SaveButton` and `SaveIconButton` also display a small warning badge on the save icon when the current record has unsaved changes, matching the existing appframe-mui behavior.
10
+
11
+ ### Patch Changes
12
+
13
+ - 0147e80: Make the indeterminate `LinearProgress` animation more visible by using a single travelling bar that stretches and shrinks, instead of the two subtle MUI-style bars.
14
+
3
15
  ## 0.5.0
4
16
 
5
17
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@olenbetong/appframe-ds",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Components that bind Designsystemet components to Appframe data objects",
5
5
  "type": "module",
6
6
  "types": "./es/index.d.ts",
@@ -16,7 +16,7 @@
16
16
  "license": "MIT",
17
17
  "peerDependencies": {
18
18
  "@digdir/designsystemet-react": ">=1.18.0",
19
- "@mui/icons-material": ">=7.0.0",
19
+ "@navikt/aksel-icons": ">=8.16.1",
20
20
  "react": ">=19.2.8",
21
21
  "react-dom": ">=19.2.8",
22
22
  "react-error-boundary": ">=6.1.2",
@@ -25,14 +25,14 @@
25
25
  "dependencies": {
26
26
  "@olenbetong/appframe-core": "2.11.10",
27
27
  "@olenbetong/appframe-data": "1.5.0",
28
- "@olenbetong/appframe-react": "1.23.0",
28
+ "@olenbetong/appframe-react": "2.0.0",
29
29
  "react-window": "^1.8.11",
30
30
  "react-virtualized-auto-sizer": "^1.0.26",
31
31
  "clsx": "^2.1.1"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@digdir/designsystemet-react": "1.18.0",
35
- "@mui/icons-material": "9.2.0",
35
+ "@navikt/aksel-icons": "8.16.1",
36
36
  "@types/react": "19.2.17",
37
37
  "@types/react-dom": "19.2.3",
38
38
  "@types/react-window": "^1.8.8",
@@ -16,13 +16,17 @@ const files = [
16
16
  ["src/page/PageBlock.css", "es/page/PageBlock.css"],
17
17
  ["src/page/PageHeader.css", "es/page/PageHeader.css"],
18
18
  ["src/list/ItemList.css", "es/list/ItemList.css"],
19
+ ["src/input/InputAdornments.css", "es/input/InputAdornments.css"],
19
20
  ["src/paper/Paper.css", "es/paper/Paper.css"],
21
+ ["src/progress/LinearProgress.css", "es/progress/LinearProgress.css"],
20
22
  ];
21
23
 
22
24
  mkdirSync("es/layout", { recursive: true });
25
+ mkdirSync("es/input", { recursive: true });
23
26
  mkdirSync("es/list", { recursive: true });
24
27
  mkdirSync("es/page", { recursive: true });
25
28
  mkdirSync("es/paper", { recursive: true });
29
+ mkdirSync("es/progress", { recursive: true });
26
30
 
27
31
  for (let [src, dest] of files) {
28
32
  cpSync(src, dest);
@@ -1,8 +1,7 @@
1
1
  import "./Lookup.css";
2
2
 
3
3
  import { Button, Textfield, type TextfieldProps } from "@digdir/designsystemet-react";
4
- import ClearIcon from "@mui/icons-material/Close";
5
- import SearchIcon from "@mui/icons-material/Search";
4
+ import { MagnifyingGlassIcon, XMarkIcon } from "@navikt/aksel-icons";
6
5
  import { getLocalizedString } from "@olenbetong/appframe-core";
7
6
  import { type DistributiveOmit, useLookupState } from "@olenbetong/appframe-react";
8
7
 
@@ -143,7 +142,7 @@ export function AfLookup<T extends Record<string, unknown>>({
143
142
  document.getElementById(inputId)?.focus();
144
143
  }}
145
144
  >
146
- <ClearIcon />
145
+ <XMarkIcon />
147
146
  </Button>
148
147
  )}
149
148
  <Button
@@ -153,7 +152,7 @@ export function AfLookup<T extends Record<string, unknown>>({
153
152
  aria-label={getLocalizedString("Search")}
154
153
  onClick={() => openDialog()}
155
154
  >
156
- <SearchIcon />
155
+ <MagnifyingGlassIcon />
157
156
  </Button>
158
157
  </div>
159
158
  </div>
@@ -1,8 +1,7 @@
1
1
  import "./Lookup.css";
2
2
 
3
3
  import { Button, Textfield, type TextfieldProps } from "@digdir/designsystemet-react";
4
- import ClearIcon from "@mui/icons-material/Close";
5
- import SearchIcon from "@mui/icons-material/Search";
4
+ import { MagnifyingGlassIcon, XMarkIcon } from "@navikt/aksel-icons";
6
5
  import { getLocalizedString } from "@olenbetong/appframe-core";
7
6
  import type { DistributiveOmit } from "@olenbetong/appframe-react";
8
7
 
@@ -108,7 +107,7 @@ export function AfLookupEdit<T extends Record<string, unknown>>({
108
107
  document.getElementById(inputId)?.focus();
109
108
  }}
110
109
  >
111
- <ClearIcon />
110
+ <XMarkIcon />
112
111
  </Button>
113
112
  )}
114
113
  <Button
@@ -121,7 +120,7 @@ export function AfLookupEdit<T extends Record<string, unknown>>({
121
120
  openDialog();
122
121
  }}
123
122
  >
124
- <SearchIcon />
123
+ <MagnifyingGlassIcon />
125
124
  </Button>
126
125
  </div>
127
126
  </div>
@@ -1,4 +1,5 @@
1
1
  import { Button, type ButtonProps } from "@digdir/designsystemet-react";
2
+ import { ArrowUndoIcon } from "@navikt/aksel-icons";
2
3
  import { getLocalizedString } from "@olenbetong/appframe-core";
3
4
  import { useCancelButton } from "@olenbetong/appframe-react";
4
5
  import { type ForwardRefExoticComponent, forwardRef, type RefAttributes } from "react";
@@ -6,11 +7,12 @@ import { type ForwardRefExoticComponent, forwardRef, type RefAttributes } from "
6
7
  export const CancelButton: ForwardRefExoticComponent<ButtonProps & RefAttributes<HTMLButtonElement>> = forwardRef<
7
8
  HTMLButtonElement,
8
9
  ButtonProps
9
- >(function CancelButton({ onClick, children, ...props }, ref) {
10
+ >(function CancelButton({ onClick, children, loading, ...props }, ref) {
10
11
  let { cancelEdit } = useCancelButton();
11
12
 
12
13
  return (
13
14
  <Button
15
+ loading={loading}
14
16
  onClick={(evt) => {
15
17
  onClick?.(evt);
16
18
  if (!evt.defaultPrevented) {
@@ -21,6 +23,7 @@ export const CancelButton: ForwardRefExoticComponent<ButtonProps & RefAttributes
21
23
  {...props}
22
24
  ref={ref}
23
25
  >
26
+ {!loading && <ArrowUndoIcon aria-hidden="true" />}
24
27
  {children ?? getLocalizedString("Cancel")}
25
28
  </Button>
26
29
  );
@@ -1,5 +1,5 @@
1
1
  import { Button, type ButtonProps } from "@digdir/designsystemet-react";
2
- import UndoIcon from "@mui/icons-material/Undo";
2
+ import { ArrowUndoIcon } from "@navikt/aksel-icons";
3
3
  import { getLocalizedString } from "@olenbetong/appframe-core";
4
4
  import { useCancelButton } from "@olenbetong/appframe-react";
5
5
  import { forwardRef } from "react";
@@ -24,7 +24,7 @@ export const CancelIconButton = forwardRef<HTMLButtonElement, Omit<ButtonProps,
24
24
  {...props}
25
25
  ref={ref}
26
26
  >
27
- {children ?? <UndoIcon />}
27
+ {children ?? <ArrowUndoIcon />}
28
28
  </Button>
29
29
  );
30
30
  });
@@ -1,6 +1,5 @@
1
1
  import { Button, Tooltip } from "@digdir/designsystemet-react";
2
- import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
3
- import ChevronRightIcon from "@mui/icons-material/ChevronRight";
2
+ import { ChevronLeftIcon, ChevronRightIcon } from "@navikt/aksel-icons";
4
3
  import { getLocalizedString } from "@olenbetong/appframe-core";
5
4
  import type { DataObject } from "@olenbetong/appframe-data";
6
5
  import { useCurrentIndex, useDataLength, useDataObject } from "@olenbetong/appframe-react";
@@ -1,4 +1,5 @@
1
1
  import { Button, type ButtonProps } from "@digdir/designsystemet-react";
2
+ import { TrashIcon } from "@navikt/aksel-icons";
2
3
  import { getLocalizedString } from "@olenbetong/appframe-core";
3
4
  import { type DeletePrompt, useDeleteButton } from "@olenbetong/appframe-react";
4
5
  import { type ForwardRefExoticComponent, forwardRef, type RefAttributes } from "react";
@@ -8,12 +9,13 @@ export type DeleteButtonProps = ButtonProps & { index?: number; prompt?: DeleteP
8
9
  export const DeleteButton: ForwardRefExoticComponent<DeleteButtonProps & RefAttributes<HTMLButtonElement>> = forwardRef<
9
10
  HTMLButtonElement,
10
11
  DeleteButtonProps
11
- >(function DeleteButton({ prompt, onClick, index, children, ...props }, ref) {
12
+ >(function DeleteButton({ prompt, onClick, index, children, loading, ...props }, ref) {
12
13
  let { deleting, deleteRow } = useDeleteButton(prompt);
14
+ let isLoading = deleting || loading;
13
15
 
14
16
  return (
15
17
  <Button
16
- loading={deleting}
18
+ loading={isLoading}
17
19
  data-color="danger"
18
20
  variant="secondary"
19
21
  onClick={(evt) => {
@@ -25,6 +27,8 @@ export const DeleteButton: ForwardRefExoticComponent<DeleteButtonProps & RefAttr
25
27
  {...props}
26
28
  ref={ref}
27
29
  >
30
+ {/* Icon is hidden while loading so the spinner takes its place instead of adding width */}
31
+ {!isLoading && <TrashIcon aria-hidden="true" />}
28
32
  {children ?? getLocalizedString("Delete")}
29
33
  </Button>
30
34
  );
@@ -1,5 +1,5 @@
1
1
  import { Button, type ButtonProps } from "@digdir/designsystemet-react";
2
- import DeleteIcon from "@mui/icons-material/Delete";
2
+ import { TrashIcon } from "@navikt/aksel-icons";
3
3
  import { getLocalizedString } from "@olenbetong/appframe-core";
4
4
  import { type DeletePrompt, useDeleteButton } from "@olenbetong/appframe-react";
5
5
  import { forwardRef } from "react";
@@ -7,15 +7,16 @@ import { forwardRef } from "react";
7
7
  export type DeleteIconButtonProps = Omit<ButtonProps, "icon"> & { index?: number; prompt?: DeletePrompt };
8
8
 
9
9
  export const DeleteIconButton = forwardRef<HTMLButtonElement, DeleteIconButtonProps>(function DeleteIconButton(
10
- { prompt, onClick, index, children, ...props },
10
+ { prompt, onClick, index, children, loading, ...props },
11
11
  ref,
12
12
  ) {
13
13
  let { isDeleting, deleteRow } = useDeleteButton(prompt);
14
+ let isLoading = isDeleting || loading;
14
15
 
15
16
  return (
16
17
  <Button
17
18
  icon
18
- loading={isDeleting}
19
+ loading={isLoading}
19
20
  data-color="danger"
20
21
  aria-label={getLocalizedString("Delete")}
21
22
  variant="tertiary"
@@ -28,7 +29,7 @@ export const DeleteIconButton = forwardRef<HTMLButtonElement, DeleteIconButtonPr
28
29
  {...props}
29
30
  ref={ref}
30
31
  >
31
- {children ?? <DeleteIcon />}
32
+ {children ?? <TrashIcon />}
32
33
  </Button>
33
34
  );
34
35
  });
@@ -1,4 +1,5 @@
1
1
  import { Button, type ButtonProps } from "@digdir/designsystemet-react";
2
+ import { ArrowsCirclepathIcon } from "@navikt/aksel-icons";
2
3
  import { getLocalizedString } from "@olenbetong/appframe-core";
3
4
  import { useRefreshButton } from "@olenbetong/appframe-react";
4
5
  import { type ForwardRefExoticComponent, forwardRef, type RefAttributes } from "react";
@@ -6,12 +7,13 @@ import { type ForwardRefExoticComponent, forwardRef, type RefAttributes } from "
6
7
  export const RefreshButton: ForwardRefExoticComponent<ButtonProps & RefAttributes<HTMLButtonElement>> = forwardRef<
7
8
  HTMLButtonElement,
8
9
  ButtonProps
9
- >(function RefreshButton({ onClick, children, ...props }, ref) {
10
- let { loading, refresh } = useRefreshButton();
10
+ >(function RefreshButton({ onClick, children, loading, ...props }, ref) {
11
+ let { loading: refreshing, refresh } = useRefreshButton();
12
+ let isLoading = refreshing || loading;
11
13
 
12
14
  return (
13
15
  <Button
14
- loading={loading}
16
+ loading={isLoading}
15
17
  onClick={(evt) => {
16
18
  onClick?.(evt);
17
19
  if (!evt.defaultPrevented) {
@@ -21,6 +23,8 @@ export const RefreshButton: ForwardRefExoticComponent<ButtonProps & RefAttribute
21
23
  {...props}
22
24
  ref={ref}
23
25
  >
26
+ {/* Icon is hidden while loading so the spinner takes its place instead of adding width */}
27
+ {!isLoading && <ArrowsCirclepathIcon aria-hidden="true" />}
24
28
  {children ?? getLocalizedString("Refresh")}
25
29
  </Button>
26
30
  );
@@ -1,20 +1,21 @@
1
1
  import { Button, type ButtonProps } from "@digdir/designsystemet-react";
2
- import RefreshIcon from "@mui/icons-material/Refresh";
2
+ import { ArrowsCirclepathIcon } from "@navikt/aksel-icons";
3
3
  import { getLocalizedString } from "@olenbetong/appframe-core";
4
4
  import { useDataObject, useLoading } from "@olenbetong/appframe-react";
5
5
  import { forwardRef } from "react";
6
6
 
7
7
  export const RefreshIconButton = forwardRef<HTMLButtonElement, Omit<ButtonProps, "icon">>(function RefreshIconButton(
8
- { onClick, children, ...props },
8
+ { onClick, children, loading, ...props },
9
9
  ref,
10
10
  ) {
11
11
  let dataObject = useDataObject();
12
- let loading = useLoading(dataObject);
12
+ let refreshing = useLoading(dataObject);
13
+ let isLoading = refreshing || loading;
13
14
 
14
15
  return (
15
16
  <Button
16
17
  icon
17
- loading={loading}
18
+ loading={isLoading}
18
19
  aria-label={getLocalizedString("Refresh data")}
19
20
  variant="tertiary"
20
21
  onClick={(evt) => {
@@ -26,7 +27,7 @@ export const RefreshIconButton = forwardRef<HTMLButtonElement, Omit<ButtonProps,
26
27
  {...props}
27
28
  ref={ref}
28
29
  >
29
- {children ?? <RefreshIcon />}
30
+ {children ?? <ArrowsCirclepathIcon />}
30
31
  </Button>
31
32
  );
32
33
  });
@@ -1,5 +1,5 @@
1
1
  import { Button, type ButtonProps } from "@digdir/designsystemet-react";
2
- import RefreshIcon from "@mui/icons-material/Refresh";
2
+ import { ArrowsCirclepathIcon } from "@navikt/aksel-icons";
3
3
  import { getLocalizedString } from "@olenbetong/appframe-core";
4
4
  import { useRefreshRowButton } from "@olenbetong/appframe-react";
5
5
  import { forwardRef } from "react";
@@ -7,13 +7,14 @@ import { forwardRef } from "react";
7
7
  export type RefreshRowIconButtonProps = Omit<ButtonProps, "icon"> & { index?: number };
8
8
 
9
9
  export const RefreshRowIconButton = forwardRef<HTMLButtonElement, RefreshRowIconButtonProps>(
10
- function RefreshRowIconButton({ onClick, index, children, ...props }, ref) {
11
- let { loading, refreshRow } = useRefreshRowButton();
10
+ function RefreshRowIconButton({ onClick, index, children, loading, ...props }, ref) {
11
+ let { loading: refreshing, refreshRow } = useRefreshRowButton();
12
+ let isLoading = refreshing || loading;
12
13
 
13
14
  return (
14
15
  <Button
15
16
  icon
16
- loading={loading}
17
+ loading={isLoading}
17
18
  aria-label={getLocalizedString("Refresh current row")}
18
19
  variant="tertiary"
19
20
  onClick={(evt) => {
@@ -25,7 +26,7 @@ export const RefreshRowIconButton = forwardRef<HTMLButtonElement, RefreshRowIcon
25
26
  {...props}
26
27
  ref={ref}
27
28
  >
28
- {children ?? <RefreshIcon />}
29
+ {children ?? <ArrowsCirclepathIcon />}
29
30
  </Button>
30
31
  );
31
32
  },
@@ -1,4 +1,5 @@
1
- import { Button, type ButtonProps } from "@digdir/designsystemet-react";
1
+ import { Badge, BadgePosition, Button, type ButtonProps } from "@digdir/designsystemet-react";
2
+ import { FloppydiskIcon } from "@navikt/aksel-icons";
2
3
  import { getLocalizedString } from "@olenbetong/appframe-core";
3
4
  import { useSaveButton } from "@olenbetong/appframe-react";
4
5
  import { type ForwardRefExoticComponent, forwardRef, type RefAttributes } from "react";
@@ -6,12 +7,13 @@ import { type ForwardRefExoticComponent, forwardRef, type RefAttributes } from "
6
7
  export const SaveButton: ForwardRefExoticComponent<ButtonProps & RefAttributes<HTMLButtonElement>> = forwardRef<
7
8
  HTMLButtonElement,
8
9
  ButtonProps
9
- >(function SaveButton({ onClick, children, ...props }, ref) {
10
- let { saving, save } = useSaveButton();
10
+ >(function SaveButton({ onClick, children, loading, ...props }, ref) {
11
+ let { dirty, saving, save } = useSaveButton();
12
+ let isLoading = saving || loading;
11
13
 
12
14
  return (
13
15
  <Button
14
- loading={saving}
16
+ loading={isLoading}
15
17
  onClick={(evt) => {
16
18
  onClick?.(evt);
17
19
  if (!evt.defaultPrevented) {
@@ -21,6 +23,14 @@ export const SaveButton: ForwardRefExoticComponent<ButtonProps & RefAttributes<H
21
23
  {...props}
22
24
  ref={ref}
23
25
  >
26
+ {/* Icon is hidden while loading so the spinner takes its place instead of adding width */}
27
+ {!isLoading && (
28
+ <BadgePosition placement="top-right" overlap="circle">
29
+ <FloppydiskIcon aria-hidden="true" />
30
+ {/* Indicates unsaved changes on the current record */}
31
+ {dirty && <Badge data-color="warning" aria-hidden="true" />}
32
+ </BadgePosition>
33
+ )}
24
34
  {children ?? getLocalizedString("Save")}
25
35
  </Button>
26
36
  );
@@ -1,19 +1,20 @@
1
- import { Button, type ButtonProps } from "@digdir/designsystemet-react";
2
- import SaveIcon from "@mui/icons-material/Save";
1
+ import { Badge, BadgePosition, Button, type ButtonProps } from "@digdir/designsystemet-react";
2
+ import { FloppydiskIcon } from "@navikt/aksel-icons";
3
3
  import { getLocalizedString } from "@olenbetong/appframe-core";
4
4
  import { useSaveButton } from "@olenbetong/appframe-react";
5
5
  import { forwardRef } from "react";
6
6
 
7
7
  export const SaveIconButton = forwardRef<HTMLButtonElement, Omit<ButtonProps, "icon">>(function SaveIconButton(
8
- { onClick, children, ...props },
8
+ { onClick, children, loading, ...props },
9
9
  ref,
10
10
  ) {
11
- let { saving, save } = useSaveButton();
11
+ let { dirty, saving, save } = useSaveButton();
12
+ let isLoading = saving || loading;
12
13
 
13
14
  return (
14
15
  <Button
15
16
  icon
16
- loading={saving}
17
+ loading={isLoading}
17
18
  aria-label={getLocalizedString("Save")}
18
19
  variant="tertiary"
19
20
  onClick={(evt) => {
@@ -25,7 +26,13 @@ export const SaveIconButton = forwardRef<HTMLButtonElement, Omit<ButtonProps, "i
25
26
  {...props}
26
27
  ref={ref}
27
28
  >
28
- {children ?? <SaveIcon />}
29
+ {children ?? (
30
+ <BadgePosition placement="top-right" overlap="circle">
31
+ <FloppydiskIcon />
32
+ {/* Indicates unsaved changes on the current record */}
33
+ {dirty && <Badge data-color="warning" aria-hidden="true" />}
34
+ </BadgePosition>
35
+ )}
29
36
  </Button>
30
37
  );
31
38
  });
package/src/index.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  export * from "./AfLookup/index.js";
2
2
  export * from "./binding/index.js";
3
+ export * from "./input/index.js";
3
4
  export * from "./layout/index.js";
4
5
  export * from "./list/index.js";
5
6
  export * from "./page/index.js";
6
7
  export * from "./paper/index.js";
8
+ export * from "./progress/index.js";
@@ -0,0 +1,75 @@
1
+ /*
2
+ * The wrapper is the visual input: it owns the border, background, hover and
3
+ * focus styling, while the inner Designsystemet input is rendered bare so the
4
+ * input and the adornments read as a single control.
5
+ */
6
+ .ObInputAdornments-root {
7
+ --obInputAdornments-padding: 0.25rem;
8
+ --obInputAdornments-gap: 0.25rem;
9
+ --obInputAdornments-fontSize: var(--ds-font-size-3, 1rem);
10
+
11
+ display: flex;
12
+ align-items: center;
13
+ box-sizing: border-box;
14
+ gap: var(--obInputAdornments-gap);
15
+ padding: var(--obInputAdornments-padding);
16
+ inline-size: 100%;
17
+ color: var(--ds-color-neutral-text-default, #1e2b3c);
18
+ background-color: var(--ds-color-neutral-surface-default, #fff);
19
+ border: var(--ds-border-width-default, 1px) solid var(--ds-color-neutral-border-default, #b3b3b3);
20
+ border-radius: var(--ds-border-radius-md, 0.375rem);
21
+ }
22
+
23
+ .ObInputAdornments-root[data-size="xs"] {
24
+ --obInputAdornments-padding: 0.125rem;
25
+ --obInputAdornments-fontSize: var(--ds-font-size-1, 0.75rem);
26
+ }
27
+
28
+ .ObInputAdornments-root[data-size="sm"] {
29
+ --obInputAdornments-padding: 0.25rem;
30
+ --obInputAdornments-fontSize: var(--ds-font-size-2, 0.875rem);
31
+ }
32
+
33
+ .ObInputAdornments-root[data-size="md"] {
34
+ --obInputAdornments-padding: 0.25rem;
35
+ --obInputAdornments-fontSize: var(--ds-font-size-3, 1rem);
36
+ }
37
+
38
+ .ObInputAdornments-root[data-size="lg"] {
39
+ --obInputAdornments-padding: 0.375rem;
40
+ --obInputAdornments-fontSize: var(--ds-font-size-4, 1.125rem);
41
+ }
42
+
43
+ .ObInputAdornments-root:hover:not(:focus-within) {
44
+ border-color: var(--ds-color-neutral-border-strong, #767676);
45
+ }
46
+
47
+ .ObInputAdornments-root:has(.ObInputAdornments-field:focus-visible) {
48
+ box-shadow: var(--dsc-focus-boxShadow);
49
+ outline: var(--dsc-focus-outline);
50
+ outline-offset: var(--ds-border-width-focus, 3px);
51
+ }
52
+
53
+ .ObInputAdornments-root:has(.ObInputAdornments-field:disabled) {
54
+ cursor: not-allowed;
55
+ opacity: var(--ds-disabled-opacity, 0.3);
56
+ }
57
+
58
+ /* The bare input: all the visual styling lives on the wrapper instead */
59
+ .ObInputAdornments-root .ObInputAdornments-field {
60
+ flex: 1;
61
+ min-inline-size: 0;
62
+ font-size: var(--obInputAdornments-fontSize);
63
+ background: transparent;
64
+ border: none;
65
+ }
66
+
67
+ .ObInputAdornments-root .ObInputAdornments-field:disabled {
68
+ opacity: 1;
69
+ }
70
+
71
+ .ObInputAdornments-root .ObInputAdornments-field:focus-visible {
72
+ box-shadow: none;
73
+ outline: none;
74
+ outline-offset: 0;
75
+ }
@@ -0,0 +1,97 @@
1
+ import "./InputAdornments.css";
2
+
3
+ import { Input, type InputProps } from "@digdir/designsystemet-react";
4
+ import clsx from "clsx";
5
+ import { Children, isValidElement, type ReactElement, type ReactNode } from "react";
6
+
7
+ export type InputSlotProps = {
8
+ /**
9
+ * Extra classes for the input rendered in this slot
10
+ */
11
+ className?: string;
12
+ };
13
+
14
+ /**
15
+ * Marks where the input should be rendered among the children of
16
+ * `InputAdornments`. Renders nothing on its own; `InputAdornments` swaps it
17
+ * for the actual input. When it is left out, the input is rendered before the
18
+ * adornments.
19
+ *
20
+ * @example
21
+ * ```jsx
22
+ * <InputAdornments value={value} onChange={handleChange}>
23
+ * <SearchIcon aria-hidden />
24
+ * <InputSlot />
25
+ * <Button icon variant="tertiary"><XMarkIcon aria-hidden /></Button>
26
+ * </InputAdornments>
27
+ * ```
28
+ */
29
+ export function InputSlot(_props: InputSlotProps) {
30
+ return null;
31
+ }
32
+
33
+ export type InputAdornmentsProps = Omit<InputProps, "children"> & {
34
+ /**
35
+ * Adornments rendered next to the input, e.g. icons or buttons. Include an
36
+ * `InputSlot` to control where the input itself ends up.
37
+ */
38
+ children?: ReactNode;
39
+ /**
40
+ * Classes for the wrapper that carries the input styling
41
+ */
42
+ className?: string;
43
+ /**
44
+ * Classes for the bare input element itself
45
+ */
46
+ inputClassName?: string;
47
+ };
48
+
49
+ /**
50
+ * Input with adornments, equivalent to a MUI `TextField` with
51
+ * `InputProps.startAdornment` / `endAdornment`. Designsystemet has no
52
+ * equivalent, so the border, background, hover and focus styling is moved from
53
+ * the input onto a wrapper element, making the input and its adornments read
54
+ * as a single control.
55
+ *
56
+ * All other props are forwarded to the Designsystemet `Input`, so `ref`,
57
+ * `value`, `onChange`, `data-size` and `data-color` work as usual.
58
+ *
59
+ * @example
60
+ * ```jsx
61
+ * // Input first (default), adornments after
62
+ * <InputAdornments aria-label="Search" value={value} onChange={handleChange}>
63
+ * <Button icon variant="tertiary"><MagnifyingGlassIcon aria-hidden /></Button>
64
+ * </InputAdornments>
65
+ *
66
+ * // Explicit placement with InputSlot
67
+ * <InputAdornments aria-label="Amount" data-size="lg">
68
+ * <span aria-hidden>kr</span>
69
+ * <InputSlot />
70
+ * <span aria-hidden>,-</span>
71
+ * </InputAdornments>
72
+ * ```
73
+ */
74
+ export function InputAdornments({ children, className, inputClassName, ...props }: InputAdornmentsProps) {
75
+ let size = props["data-size"];
76
+ let color = props["data-color"];
77
+ let childArray = Children.toArray(children);
78
+ let slot = childArray.find((child) => isValidElement(child) && child.type === InputSlot) as
79
+ | ReactElement<InputSlotProps>
80
+ | undefined;
81
+
82
+ let input = (
83
+ <Input
84
+ key="ObInputAdornments-input"
85
+ {...props}
86
+ className={clsx("ObInputAdornments-field", inputClassName, slot?.props.className)}
87
+ />
88
+ );
89
+
90
+ let content = slot ? childArray.map((child) => (child === slot ? input : child)) : [input, ...childArray];
91
+
92
+ return (
93
+ <div className={clsx("ObInputAdornments-root", className)} data-size={size} data-color={color}>
94
+ {content}
95
+ </div>
96
+ );
97
+ }
@@ -0,0 +1 @@
1
+ export * from "./InputAdornments.js";
@@ -0,0 +1,93 @@
1
+ .ObLinearProgress-root {
2
+ --obLinearProgress-thickness: 0.25rem;
3
+ --obLinearProgress-duration: 2.1s;
4
+
5
+ position: relative;
6
+ display: block;
7
+ box-sizing: border-box;
8
+ inline-size: 100%;
9
+ block-size: var(--obLinearProgress-thickness);
10
+ overflow: hidden;
11
+ background-color: var(--ds-color-surface-active, #cddffb);
12
+ border-radius: var(--ds-border-radius-full, 624.9375rem);
13
+ }
14
+
15
+ .ObLinearProgress-root[data-size="xs"] {
16
+ --obLinearProgress-thickness: 0.125rem;
17
+ }
18
+
19
+ .ObLinearProgress-root[data-size="sm"] {
20
+ --obLinearProgress-thickness: 0.25rem;
21
+ }
22
+
23
+ .ObLinearProgress-root[data-size="md"] {
24
+ --obLinearProgress-thickness: 0.5rem;
25
+ }
26
+
27
+ .ObLinearProgress-root[data-size="lg"] {
28
+ --obLinearProgress-thickness: 0.75rem;
29
+ }
30
+
31
+ .ObLinearProgress-bar {
32
+ position: absolute;
33
+ inset-block: 0;
34
+ inset-inline-start: 0;
35
+ inline-size: 100%;
36
+ background-color: var(--ds-color-base-default, #0062ba);
37
+ border-radius: inherit;
38
+ }
39
+
40
+ .ObLinearProgress-determinate .ObLinearProgress-bar {
41
+ transition: inline-size 0.2s linear;
42
+ }
43
+
44
+ /*
45
+ * The indeterminate bar is full width and animated with `transform`, where the
46
+ * start and end of the bar move at different speeds (the bar stretches and
47
+ * shrinks as it travels).
48
+ */
49
+ .ObLinearProgress-indeterminate .ObLinearProgress-bar {
50
+ transform-origin: left center;
51
+ will-change: transform;
52
+ animation: ObLinearProgress-indeterminate var(--obLinearProgress-duration) cubic-bezier(0.65, 0.815, 0.735, 0.395)
53
+ infinite;
54
+ }
55
+
56
+ /* Zero-height wrapper so the overlay bar never affects the layout */
57
+ .ObLinearProgress-overlay {
58
+ position: relative;
59
+ z-index: 1;
60
+ block-size: 0;
61
+ }
62
+
63
+ .ObLinearProgress-overlay > .ObLinearProgress-root {
64
+ position: absolute;
65
+ inset-block-start: 0;
66
+ inset-inline: 0;
67
+ }
68
+
69
+ @keyframes ObLinearProgress-indeterminate {
70
+ 0% {
71
+ transform: translateX(-35%) scaleX(0.35);
72
+ }
73
+
74
+ 60% {
75
+ transform: translateX(60%) scaleX(0.7);
76
+ }
77
+
78
+ 100% {
79
+ transform: translateX(100%) scaleX(0.2);
80
+ }
81
+ }
82
+
83
+ @media (prefers-reduced-motion: reduce) {
84
+ .ObLinearProgress-indeterminate .ObLinearProgress-bar {
85
+ animation-duration: 4s;
86
+ }
87
+ }
88
+
89
+ @media print {
90
+ .ObLinearProgress-root {
91
+ display: none;
92
+ }
93
+ }
@@ -0,0 +1,104 @@
1
+ import "./LinearProgress.css";
2
+
3
+ import clsx from "clsx";
4
+ import type React from "react";
5
+
6
+ export const linearProgressSizes = ["xs", "sm", "md", "lg"] as const;
7
+
8
+ export type LinearProgressSize = (typeof linearProgressSizes)[number];
9
+
10
+ export type LinearProgressProps = Omit<React.HTMLAttributes<HTMLDivElement>, "color"> & {
11
+ /**
12
+ * Designsystemet colour palette used for the bar
13
+ * @default "accent"
14
+ */
15
+ "data-color"?: string;
16
+ /**
17
+ * Thickness of the bar
18
+ * @default "sm"
19
+ */
20
+ "data-size"?: LinearProgressSize;
21
+ /**
22
+ * Renders the bar absolutely positioned in a zero-height wrapper, so it
23
+ * overlays the top of the following content instead of taking up space.
24
+ * Use it for loading indicators that should not shift the layout.
25
+ * @default false
26
+ */
27
+ overlay?: boolean;
28
+ /**
29
+ * Progress value. When omitted the bar is indeterminate.
30
+ */
31
+ value?: number;
32
+ /**
33
+ * Highest possible value of `value`
34
+ * @default 100
35
+ */
36
+ valueMax?: number;
37
+ /**
38
+ * Lowest possible value of `value`
39
+ * @default 0
40
+ */
41
+ valueMin?: number;
42
+ };
43
+
44
+ /**
45
+ * Linear progress indicator, equivalent to MUI `LinearProgress`. Indeterminate
46
+ * by default, determinate when `value` is given. Designsystemet has no linear
47
+ * progress component, so this is built with the designsystemet colour and
48
+ * radius tokens (use `data-color` to change palette).
49
+ *
50
+ * @example
51
+ * ```jsx
52
+ * // Indeterminate, in the normal document flow
53
+ * <LinearProgress aria-label="Loading..." />
54
+ *
55
+ * // Determinate
56
+ * <LinearProgress aria-label="Uploading" value={40} />
57
+ *
58
+ * // Absolutely positioned, does not affect the layout
59
+ * <LinearProgress overlay aria-label="Loading..." />
60
+ * ```
61
+ */
62
+ export function LinearProgress({
63
+ className,
64
+ overlay,
65
+ value,
66
+ valueMax = 100,
67
+ valueMin = 0,
68
+ "data-color": color = "accent",
69
+ "data-size": size = "sm",
70
+ ...props
71
+ }: LinearProgressProps) {
72
+ let isDeterminate = typeof value === "number" && Number.isFinite(value);
73
+ let clamped = isDeterminate ? Math.min(Math.max(value as number, valueMin), valueMax) : undefined;
74
+ let percentage =
75
+ clamped === undefined || valueMax === valueMin ? undefined : ((clamped - valueMin) / (valueMax - valueMin)) * 100;
76
+
77
+ let bar = (
78
+ <div
79
+ className={clsx(
80
+ "ObLinearProgress-root",
81
+ isDeterminate ? "ObLinearProgress-determinate" : "ObLinearProgress-indeterminate",
82
+ !overlay && className,
83
+ )}
84
+ data-color={color}
85
+ data-size={size}
86
+ role="progressbar"
87
+ aria-valuemin={valueMin}
88
+ aria-valuemax={valueMax}
89
+ aria-valuenow={clamped}
90
+ {...props}
91
+ >
92
+ <div
93
+ className="ObLinearProgress-bar"
94
+ style={percentage === undefined ? undefined : { inlineSize: `${percentage}%` }}
95
+ />
96
+ </div>
97
+ );
98
+
99
+ if (!overlay) {
100
+ return bar;
101
+ }
102
+
103
+ return <div className={clsx("ObLinearProgress-overlay", className)}>{bar}</div>;
104
+ }
@@ -0,0 +1 @@
1
+ export * from "./LinearProgress.js";