@fellesdatakatalog/ui 1.0.15 → 1.0.17

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 (34) hide show
  1. package/dist/shared-ui.cjs.map +1 -1
  2. package/dist/shared-ui.es.js.map +1 -1
  3. package/package.json +33 -24
  4. package/src/components/app-bar/app-bar.stories.tsx +22 -0
  5. package/src/components/app-bar/index.tsx +12 -0
  6. package/src/components/app-bar/styles.module.scss +7 -0
  7. package/src/components/checkbox-group/checkbox-group.stories.tsx +143 -0
  8. package/src/components/checkbox-group/index.tsx +105 -0
  9. package/src/components/copy-button/copy-button.stories.tsx +28 -0
  10. package/src/components/copy-button/index.tsx +54 -0
  11. package/src/components/core/ds-overrides.scss +105 -0
  12. package/src/components/core/mixins.scss +143 -0
  13. package/src/components/core/reset.css +131 -0
  14. package/src/components/core/tokens.scss +24 -0
  15. package/src/components/helptext/helptext.module.scss +46 -0
  16. package/src/components/helptext/helptext.stories.tsx +42 -0
  17. package/src/components/helptext/index.tsx +37 -0
  18. package/src/components/hstack/hstack.module.scss +5 -0
  19. package/src/components/hstack/hstack.stories.tsx +38 -0
  20. package/src/components/hstack/index.tsx +17 -0
  21. package/src/components/org-logo/index.tsx +27 -0
  22. package/src/components/org-logo/org-logo.stories.tsx +35 -0
  23. package/src/components/org-logo/styles.module.scss +20 -0
  24. package/src/components/radio-group/index.tsx +101 -0
  25. package/src/components/radio-group/radio-group.stories.tsx +141 -0
  26. package/src/components/tag-list/index.tsx +39 -0
  27. package/src/components/tag-list/styles.module.scss +8 -0
  28. package/src/components/tag-list/tag-list.stories.tsx +107 -0
  29. package/src/components/vstack/index.tsx +16 -0
  30. package/src/components/vstack/vstack.module.scss +5 -0
  31. package/src/components/vstack/vstack.stories.tsx +37 -0
  32. package/src/index.ts +11 -0
  33. package/src/styles/storybook.scss +34 -0
  34. package/src/types/scss.d.ts +9 -0
@@ -0,0 +1,37 @@
1
+ import React from 'react';
2
+ import { Popover } from '@digdir/designsystemet-react';
3
+ import styles from './helptext.module.scss';
4
+
5
+ export type HelpTextProps = {
6
+ /**
7
+ * Required descriptive label for screen readers.
8
+ **/
9
+ "aria-label": string;
10
+ /**
11
+ * Placement of the Popover.
12
+ * @default 'right'
13
+ */
14
+ placement?: "right" | "bottom" | "left" | "top";
15
+ } & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "color">;
16
+
17
+ export const HelpText = React.forwardRef<HTMLButtonElement, HelpTextProps>(
18
+ function HelpText(
19
+ { placement = "right", children, ...rest },
20
+ ref
21
+ ) {
22
+ return (
23
+ <Popover.TriggerContext>
24
+ <Popover.Trigger
25
+ className={styles.helptext}
26
+ ref={ref}
27
+ variant="tertiary"
28
+ data-color="info"
29
+ {...rest}
30
+ />
31
+ <Popover placement={placement} data-color="info">
32
+ {children}
33
+ </Popover>
34
+ </Popover.TriggerContext>
35
+ );
36
+ }
37
+ );
@@ -0,0 +1,5 @@
1
+ .wrapper {
2
+ display: flex;
3
+ align-items: center;
4
+ gap: 0.5rem;
5
+ }
@@ -0,0 +1,38 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+
3
+ import HStack from '.';
4
+
5
+ const meta: Meta<typeof HStack> = {
6
+ component: HStack,
7
+ title: 'HStack',
8
+ };
9
+
10
+ export default meta;
11
+ type Story = StoryObj<typeof HStack>;
12
+
13
+ const Item = ({ children }: { children: React.ReactNode }) => (
14
+ <span
15
+ style={{
16
+ display: 'inline-block',
17
+ padding: '0.25rem 0.5rem',
18
+ background: '#EEF0F2',
19
+ borderRadius: 4,
20
+ }}
21
+ >
22
+ {children}
23
+ </span>
24
+ );
25
+
26
+ export const Primary: Story = {
27
+ render: () => (
28
+ <div style={{ padding: '1rem' }}>
29
+ <HStack>
30
+ <Item>One</Item>
31
+ <Item>Two</Item>
32
+ <Item>Three</Item>
33
+ </HStack>
34
+ </div>
35
+ ),
36
+ };
37
+
38
+
@@ -0,0 +1,17 @@
1
+ import React from 'react';
2
+ import cn from 'classnames';
3
+
4
+ import styles from './hstack.module.scss';
5
+
6
+ const HStack = ({ children, className, ...props }: React.HTMLAttributes<HTMLDivElement>) => {
7
+ return (
8
+ <div
9
+ className={cn(styles.wrapper, className)}
10
+ {...props}
11
+ >
12
+ {children}
13
+ </div>
14
+ );
15
+ };
16
+
17
+ export default HStack;
@@ -0,0 +1,27 @@
1
+ import React from 'react';
2
+ import cn from 'classnames';
3
+ import { Buildings3Icon } from '@navikt/aksel-icons';
4
+ import styles from './styles.module.scss';
5
+
6
+ export type OrgLogoProps = {
7
+ orgLogoSrc?: string | null;
8
+ className?: string;
9
+ } & React.HTMLAttributes<HTMLDivElement>;
10
+
11
+ const OrgLogo = ({ className, orgLogoSrc, ...props }: OrgLogoProps) => {
12
+ return (
13
+ <div className={cn(styles.orgLogo, className)} {...props}>
14
+ {orgLogoSrc ? (
15
+ <img
16
+ aria-hidden
17
+ src={orgLogoSrc}
18
+ alt=''
19
+ />
20
+ ) : (
21
+ <Buildings3Icon aria-hidden />
22
+ )}
23
+ </div>
24
+ );
25
+ };
26
+
27
+ export { OrgLogo };
@@ -0,0 +1,35 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+
3
+ import { OrgLogo } from '.';
4
+
5
+ const meta: Meta<typeof OrgLogo> = {
6
+ component: OrgLogo,
7
+ title: 'OrgLogo',
8
+ parameters: {
9
+ layout: 'centered',
10
+ },
11
+ argTypes: {
12
+ orgLogoSrc: {
13
+ control: 'text',
14
+ description: 'URL of the organization logo image',
15
+ },
16
+ className: {
17
+ control: 'text',
18
+ description: 'Additional CSS classes',
19
+ },
20
+ },
21
+ };
22
+
23
+ export default meta;
24
+ type Story = StoryObj<typeof OrgLogo>;
25
+
26
+ export const Default: Story = {
27
+ args: {
28
+ orgLogoSrc: 'https://orglogo.digdir.no/api/emblem/svg/991825827',
29
+ },
30
+ render: (args) => (
31
+ <div style={{ maxWidth: '2rem', padding: '1rem' }}>
32
+ <OrgLogo {...args} />
33
+ </div>
34
+ ),
35
+ };
@@ -0,0 +1,20 @@
1
+ .orgLogo {
2
+ display: flex;
3
+ width: 1em;
4
+ height: 1em;
5
+ overflow: hidden;
6
+ align-items: center;
7
+ justify-content: center;
8
+ position: relative;
9
+
10
+ svg,
11
+ img {
12
+ width: 100%;
13
+ height: 100%;
14
+ object-fit: contain;
15
+ }
16
+
17
+ svg {
18
+ color: var(--color-text);
19
+ }
20
+ }
@@ -0,0 +1,101 @@
1
+ import React, { type ReactNode } from 'react';
2
+ import {
3
+ Fieldset,
4
+ Radio,
5
+ ValidationMessage,
6
+ type UseRadioGroupProps,
7
+ useRadioGroup,
8
+ } from '@digdir/designsystemet-react';
9
+
10
+ export interface RadioGroupProps extends Omit<UseRadioGroupProps, 'value' | 'onChange'> {
11
+ /**
12
+ * The legend text for the fieldset
13
+ */
14
+ legend?: ReactNode;
15
+ /**
16
+ * Optional description text for the fieldset
17
+ */
18
+ description?: ReactNode;
19
+ /**
20
+ * Array of radio options
21
+ */
22
+ options: Array<{
23
+ value: string;
24
+ label: string;
25
+ description?: string;
26
+ disabled?: boolean;
27
+ }>;
28
+ /**
29
+ * Controlled value - the selected radio value
30
+ */
31
+ value?: string;
32
+ /**
33
+ * Callback when the selected value changes
34
+ */
35
+ onChange?: (value: string) => void;
36
+ /**
37
+ * Optional className for the fieldset
38
+ */
39
+ className?: string;
40
+ }
41
+
42
+ /**
43
+ * RadioGroup component that wraps multiple Radio components in a Fieldset.
44
+ * Handles state management, onChange, and value automatically.
45
+ *
46
+ * @example
47
+ * <RadioGroup
48
+ * legend="Hvilken iskremsmak er best?"
49
+ * description="Velg din favorittsmak"
50
+ * options={[
51
+ * { value: 'vanilje', label: 'Vanilje' },
52
+ * { value: 'jordbær', label: 'Jordbær' },
53
+ * { value: 'sjokolade', label: 'Sjokolade' },
54
+ * ]}
55
+ * value={selectedValue}
56
+ * onChange={(value) => setSelectedValue(value)}
57
+ * />
58
+ */
59
+ export const RadioGroup = ({
60
+ legend,
61
+ description,
62
+ options,
63
+ value,
64
+ onChange,
65
+ error,
66
+ disabled,
67
+ readOnly,
68
+ required,
69
+ name,
70
+ className,
71
+ }: RadioGroupProps) => {
72
+ const { getRadioProps, validationMessageProps } = useRadioGroup({
73
+ value,
74
+ onChange: onChange ? (nextValue) => onChange(nextValue) : undefined,
75
+ error,
76
+ disabled,
77
+ readOnly,
78
+ required,
79
+ name,
80
+ });
81
+
82
+ return (
83
+ <Fieldset className={className}>
84
+ {legend && <Fieldset.Legend>{legend}</Fieldset.Legend>}
85
+ {description && <Fieldset.Description>{description}</Fieldset.Description>}
86
+ {options.map((option) => (
87
+ <Radio
88
+ key={option.value}
89
+ label={option.label}
90
+ description={option.description}
91
+ disabled={option.disabled}
92
+ {...getRadioProps(option.value)}
93
+ />
94
+ ))}
95
+ {error && <ValidationMessage {...validationMessageProps} />}
96
+ </Fieldset>
97
+ );
98
+ };
99
+
100
+ export default RadioGroup;
101
+
@@ -0,0 +1,141 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { useState } from 'react';
3
+ import RadioGroup from '.';
4
+
5
+ const meta: Meta<typeof RadioGroup> = {
6
+ component: RadioGroup,
7
+ title: 'RadioGroup',
8
+ argTypes: {
9
+ legend: {
10
+ control: 'text',
11
+ description: 'The legend text for the fieldset',
12
+ },
13
+ description: {
14
+ control: 'text',
15
+ description: 'Optional description text',
16
+ },
17
+ value: {
18
+ control: 'text',
19
+ description: 'Controlled value - the selected radio value',
20
+ },
21
+ error: {
22
+ control: 'text',
23
+ description: 'Error message to display',
24
+ },
25
+ disabled: {
26
+ control: 'boolean',
27
+ description: 'Disable all radios',
28
+ },
29
+ readOnly: {
30
+ control: 'boolean',
31
+ description: 'Make all radios read-only',
32
+ },
33
+ required: {
34
+ control: 'boolean',
35
+ description: 'Make all radios required',
36
+ },
37
+ },
38
+ };
39
+
40
+ export default meta;
41
+ type Story = StoryObj<typeof RadioGroup>;
42
+
43
+ const defaultOptions = [
44
+ { value: 'vanilje', label: 'Vanilje' },
45
+ { value: 'jordbær', label: 'Jordbær' },
46
+ { value: 'sjokolade', label: 'Sjokolade' },
47
+ { value: 'spiser-ikke-is', label: 'Jeg spiser ikke iskrem' },
48
+ ];
49
+
50
+ export const Primary: Story = {
51
+ render: () => {
52
+ const [value, setValue] = useState('vanilje');
53
+ return (
54
+ <RadioGroup
55
+ legend="Hvilken iskremsmak er best?"
56
+ description="Velg din favorittsmak blant alternativene."
57
+ options={defaultOptions}
58
+ value={value}
59
+ onChange={setValue}
60
+ />
61
+ );
62
+ },
63
+ };
64
+
65
+ export const Uncontrolled: Story = {
66
+ render: () => (
67
+ <RadioGroup
68
+ legend="Hvilken iskremsmak er best?"
69
+ description="Velg din favorittsmak blant alternativene."
70
+ options={defaultOptions}
71
+ />
72
+ ),
73
+ };
74
+
75
+ export const WithError: Story = {
76
+ render: () => (
77
+ <RadioGroup
78
+ legend="Hvilken bydel bor du i?"
79
+ description="Trondheim er delt inn i fire bydeler"
80
+ options={[
81
+ { value: 'ostbyen', label: 'Østbyen' },
82
+ { value: 'lerkendal', label: 'Lerkendal' },
83
+ { value: 'heimdal', label: 'Heimdal' },
84
+ { value: 'midtbyen', label: 'Midtbyen' },
85
+ ]}
86
+ error="Du må velge en bydel før du kan fortsette."
87
+ required
88
+ />
89
+ ),
90
+ };
91
+
92
+ export const Disabled: Story = {
93
+ render: () => (
94
+ <RadioGroup
95
+ legend="Hvilken iskremsmak er best?"
96
+ options={defaultOptions}
97
+ disabled
98
+ />
99
+ ),
100
+ };
101
+
102
+ export const ReadOnly: Story = {
103
+ render: () => (
104
+ <RadioGroup
105
+ legend="Hvilken bydel bor du i?"
106
+ options={[
107
+ { value: 'ostbyen', label: 'Østbyen' },
108
+ { value: 'lerkendal', label: 'Lerkendal' },
109
+ { value: 'heimdal', label: 'Heimdal', disabled: true },
110
+ { value: 'midtbyen', label: 'Midtbyen' },
111
+ ]}
112
+ value="heimdal"
113
+ readOnly
114
+ />
115
+ ),
116
+ };
117
+
118
+ export const WithDescriptions: Story = {
119
+ render: () => {
120
+ const [value, setValue] = useState('');
121
+ return (
122
+ <RadioGroup
123
+ legend="Velg pizza"
124
+ description="Alle pizzaene er laget på våre egne nybakte bunner"
125
+ options={[
126
+ { value: 'ost', label: 'Bare ost' },
127
+ {
128
+ value: 'dobbeldekker',
129
+ label: 'Dobbeldekker',
130
+ description: 'Chorizo spesial med kokkens luksuskylling',
131
+ },
132
+ { value: 'flammen', label: 'Flammen' },
133
+ { value: 'snadder', label: 'Snadder' },
134
+ ]}
135
+ value={value}
136
+ onChange={setValue}
137
+ />
138
+ );
139
+ },
140
+ };
141
+
@@ -0,0 +1,39 @@
1
+ import React from 'react';
2
+ import cn from 'classnames';
3
+ import { Tag } from '@digdir/designsystemet-react';
4
+ import styles from './styles.module.scss';
5
+
6
+ export type TagListProps = {
7
+ maxTags?: number;
8
+ 'data-size'?: string;
9
+ };
10
+
11
+ const TagList = ({ children, className, maxTags, 'data-size': dataSize, ...props }: TagListProps & React.HTMLAttributes<HTMLUListElement>) => {
12
+ const childArray = React.Children.toArray(children);
13
+ if (!childArray.length) return null;
14
+ return (
15
+ <ul
16
+ className={cn(styles.tagList, className)}
17
+ {...props}
18
+ >
19
+ {childArray
20
+ .filter((child) => Boolean(child))
21
+ .slice(0, maxTags ?? childArray.length)
22
+ .map((child, i) => {
23
+ const key = React.isValidElement(child) && child.key != null
24
+ ? child.key
25
+ : i;
26
+ return <li key={key}>{child}</li>;
27
+ })}
28
+ {maxTags && maxTags < childArray.length && (
29
+ <li>
30
+ <Tag data-size={dataSize || 'sm'}>
31
+ <span>+{childArray.length - maxTags}</span>
32
+ </Tag>
33
+ </li>
34
+ )}
35
+ </ul>
36
+ );
37
+ };
38
+
39
+ export default TagList;
@@ -0,0 +1,8 @@
1
+ .tagList {
2
+ display: flex;
3
+ gap: 0.5rem;
4
+ font-weight: normal;
5
+ font-size: 0.8em;
6
+ align-items: center;
7
+ flex-wrap: wrap;
8
+ }
@@ -0,0 +1,107 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+
3
+ import { Tag } from '@digdir/designsystemet-react';
4
+ import TagList from '.';
5
+
6
+ const meta: Meta<typeof TagList> = {
7
+ component: TagList,
8
+ title: 'TagList',
9
+ };
10
+
11
+ export default meta;
12
+ type Story = StoryObj<typeof TagList>;
13
+
14
+ export const Primary: Story = {
15
+ parameters: {
16
+ nextjs: {
17
+ appDirectory: true,
18
+ },
19
+ },
20
+ render: () => (
21
+ <>
22
+ <div style={{ padding: '1rem' }}>
23
+ <TagList>
24
+ <Tag data-size='sm'>My tag</Tag>
25
+ <Tag
26
+ data-size='sm'
27
+ color='success'
28
+ >
29
+ My tag
30
+ </Tag>
31
+ <Tag
32
+ data-size='sm'
33
+ color='neutral'
34
+ >
35
+ My tag
36
+ </Tag>
37
+ <Tag
38
+ data-size='sm'
39
+ color='warning'
40
+ >
41
+ My tag
42
+ </Tag>
43
+ <Tag
44
+ data-size='sm'
45
+ color='danger'
46
+ >
47
+ My tag
48
+ </Tag>
49
+ </TagList>
50
+ </div>
51
+ </>
52
+ ),
53
+ };
54
+
55
+ export const WithMaxTags: Story = {
56
+ parameters: {
57
+ nextjs: {
58
+ appDirectory: true,
59
+ },
60
+ },
61
+ render: () => (
62
+ <>
63
+ <div style={{ padding: '1rem' }}>
64
+ <TagList maxTags={3}>
65
+ <Tag data-size='sm'>Tag 1</Tag>
66
+ <Tag
67
+ data-size='sm'
68
+ color='success'
69
+ >
70
+ Tag 2
71
+ </Tag>
72
+ <Tag
73
+ data-size='sm'
74
+ color='neutral'
75
+ >
76
+ Tag 3
77
+ </Tag>
78
+ <Tag
79
+ data-size='sm'
80
+ color='warning'
81
+ >
82
+ Tag 4
83
+ </Tag>
84
+ <Tag
85
+ data-size='sm'
86
+ color='danger'
87
+ >
88
+ Tag 5
89
+ </Tag>
90
+ <Tag data-size='sm'>Tag 6</Tag>
91
+ <Tag
92
+ data-size='sm'
93
+ color='success'
94
+ >
95
+ Tag 7
96
+ </Tag>
97
+ <Tag
98
+ data-size='sm'
99
+ color='neutral'
100
+ >
101
+ Tag 8
102
+ </Tag>
103
+ </TagList>
104
+ </div>
105
+ </>
106
+ ),
107
+ };
@@ -0,0 +1,16 @@
1
+ import React from 'react';
2
+
3
+ import styles from './vstack.module.scss';
4
+
5
+ const VStack = ({ children, ...props }: React.HTMLAttributes<HTMLDivElement>) => {
6
+ return (
7
+ <div
8
+ className={styles.wrapper}
9
+ {...props}
10
+ >
11
+ {children}
12
+ </div>
13
+ );
14
+ };
15
+
16
+ export default VStack;
@@ -0,0 +1,5 @@
1
+ .wrapper {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: 0.5rem;
5
+ }
@@ -0,0 +1,37 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+
3
+ import VStack from '.';
4
+
5
+ const meta: Meta<typeof VStack> = {
6
+ component: VStack,
7
+ title: 'VStack',
8
+ };
9
+
10
+ export default meta;
11
+ type Story = StoryObj<typeof VStack>;
12
+
13
+ const Item = ({ children }: { children: React.ReactNode }) => (
14
+ <div
15
+ style={{
16
+ padding: '0.25rem 0.5rem',
17
+ background: '#EEF0F2',
18
+ borderRadius: 4,
19
+ }}
20
+ >
21
+ {children}
22
+ </div>
23
+ );
24
+
25
+ export const Primary: Story = {
26
+ render: () => (
27
+ <div style={{ padding: '1rem' }}>
28
+ <VStack>
29
+ <Item>Row 1</Item>
30
+ <Item>Row 2</Item>
31
+ <Item>Row 3</Item>
32
+ </VStack>
33
+ </div>
34
+ ),
35
+ };
36
+
37
+
package/src/index.ts ADDED
@@ -0,0 +1,11 @@
1
+ export * from './components/app-bar';
2
+ export * from './components/org-logo';
3
+ export { default as CopyButton } from './components/copy-button';
4
+ export { default as HStack } from './components/hstack';
5
+ export { default as VStack } from './components/vstack';
6
+ export { HelpText } from './components/helptext';
7
+ export { default as TagList } from './components/tag-list';
8
+ export { default as RadioGroup } from './components/radio-group';
9
+ export { default as CheckboxGroup } from './components/checkbox-group';
10
+ export type { RadioGroupProps } from './components/radio-group';
11
+ export type { CheckboxGroupProps } from './components/checkbox-group';
@@ -0,0 +1,34 @@
1
+ @layer reset, fds;
2
+ @import '../components/core/reset.css';
3
+ @import '@digdir/designsystemet-theme';
4
+ @import '@digdir/designsystemet-css';
5
+
6
+ html { font-size: 16px; }
7
+ body { font-size: 1rem; }
8
+
9
+ html, body, #storybook-root {
10
+ height: 100%;
11
+ }
12
+
13
+ /* Place for additional global styles */
14
+ body {
15
+ font-family:
16
+ system-ui,
17
+ -apple-system,
18
+ BlinkMacSystemFont,
19
+ 'Segoe UI',
20
+ Roboto,
21
+ 'Helvetica Neue',
22
+ Arial,
23
+ sans-serif;
24
+ background: var(--color-fdk-navy-dark);
25
+ color: var(--color-text);
26
+
27
+ main {
28
+ background: white;
29
+ }
30
+ }
31
+
32
+ * {
33
+ font-family: inherit;
34
+ }