@fellesdatakatalog/ui 1.0.15 → 1.0.16

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 (32) hide show
  1. package/package.json +11 -2
  2. package/src/components/app-bar/app-bar.stories.tsx +22 -0
  3. package/src/components/app-bar/index.tsx +12 -0
  4. package/src/components/app-bar/styles.module.scss +7 -0
  5. package/src/components/checkbox-group/checkbox-group.stories.tsx +143 -0
  6. package/src/components/checkbox-group/index.tsx +105 -0
  7. package/src/components/copy-button/copy-button.stories.tsx +28 -0
  8. package/src/components/copy-button/index.tsx +54 -0
  9. package/src/components/core/ds-overrides.scss +105 -0
  10. package/src/components/core/mixins.scss +143 -0
  11. package/src/components/core/reset.css +131 -0
  12. package/src/components/core/tokens.scss +26 -0
  13. package/src/components/helptext/helptext.module.scss +46 -0
  14. package/src/components/helptext/helptext.stories.tsx +42 -0
  15. package/src/components/helptext/index.tsx +37 -0
  16. package/src/components/hstack/hstack.module.scss +5 -0
  17. package/src/components/hstack/hstack.stories.tsx +38 -0
  18. package/src/components/hstack/index.tsx +17 -0
  19. package/src/components/org-logo/index.tsx +27 -0
  20. package/src/components/org-logo/org-logo.stories.tsx +35 -0
  21. package/src/components/org-logo/styles.module.scss +20 -0
  22. package/src/components/radio-group/index.tsx +101 -0
  23. package/src/components/radio-group/radio-group.stories.tsx +141 -0
  24. package/src/components/tag-list/index.tsx +39 -0
  25. package/src/components/tag-list/styles.module.scss +8 -0
  26. package/src/components/tag-list/tag-list.stories.tsx +107 -0
  27. package/src/components/vstack/index.tsx +16 -0
  28. package/src/components/vstack/vstack.module.scss +5 -0
  29. package/src/components/vstack/vstack.stories.tsx +37 -0
  30. package/src/index.ts +11 -0
  31. package/src/styles/storybook.scss +34 -0
  32. package/src/types/scss.d.ts +9 -0
@@ -0,0 +1,131 @@
1
+ /* http://meyerweb.com/eric/tools/css/reset/
2
+ v2.0 | 20110126
3
+ License: none (public domain)
4
+ */
5
+
6
+ /* This is file is used to neutrilize default css properties defined by the browsers (or other parties) */
7
+ @layer reset {
8
+ html,
9
+ body,
10
+ div,
11
+ span,
12
+ applet,
13
+ object,
14
+ iframe,
15
+ h1,
16
+ h2,
17
+ h3,
18
+ h4,
19
+ h5,
20
+ h6,
21
+ p,
22
+ blockquote,
23
+ pre,
24
+ a,
25
+ abbr,
26
+ acronym,
27
+ address,
28
+ big,
29
+ cite,
30
+ code,
31
+ del,
32
+ dfn,
33
+ em,
34
+ img,
35
+ ins,
36
+ kbd,
37
+ q,
38
+ s,
39
+ samp,
40
+ small,
41
+ strike,
42
+ strong,
43
+ sub,
44
+ sup,
45
+ tt,
46
+ var,
47
+ b,
48
+ u,
49
+ i,
50
+ center,
51
+ dl,
52
+ dt,
53
+ dd,
54
+ ol,
55
+ ul,
56
+ li,
57
+ fieldset,
58
+ form,
59
+ label,
60
+ legend,
61
+ table,
62
+ caption,
63
+ tbody,
64
+ tfoot,
65
+ thead,
66
+ tr,
67
+ th,
68
+ td,
69
+ article,
70
+ aside,
71
+ canvas,
72
+ details,
73
+ embed,
74
+ figure,
75
+ figcaption,
76
+ footer,
77
+ header,
78
+ hgroup,
79
+ menu,
80
+ nav,
81
+ output,
82
+ ruby,
83
+ section,
84
+ summary,
85
+ time,
86
+ mark,
87
+ audio,
88
+ video {
89
+ margin: 0;
90
+ padding: 0;
91
+ border: 0;
92
+ font-size: inherit;
93
+ vertical-align: baseline;
94
+ }
95
+ /* HTML5 display-role reset for older browsers */
96
+ article,
97
+ aside,
98
+ details,
99
+ figcaption,
100
+ figure,
101
+ footer,
102
+ header,
103
+ hgroup,
104
+ menu,
105
+ nav,
106
+ section {
107
+ display: block;
108
+ }
109
+ body {
110
+ line-height: 1.5rem;
111
+ }
112
+ ol,
113
+ ul {
114
+ list-style: none;
115
+ }
116
+ blockquote,
117
+ q {
118
+ quotes: none;
119
+ }
120
+ blockquote:before,
121
+ blockquote:after,
122
+ q:before,
123
+ q:after {
124
+ content: '';
125
+ content: none;
126
+ }
127
+ table {
128
+ border-collapse: collapse;
129
+ border-spacing: 0;
130
+ }
131
+ }
@@ -0,0 +1,26 @@
1
+ @import './reset.css';
2
+ @import '@digdir/designsystemet-css';
3
+ @import '@digdir/designsystemet-theme';
4
+ @import './ds-overrides.scss';
5
+
6
+ :root {
7
+ // Colors
8
+ --fdk-color-navy: #1e2b3c;
9
+ --fdk-color-navy-dark: color-mix(in srgb, var(--fdk-color-navy), #000 60%);
10
+ --fdk-color-blue-lightest: #ddeaf6;
11
+ --fdk-color-blue-light: #1e98f5;
12
+ --fdk-color-blue: #0062ba;
13
+ --fdk-color-blue-dark: #004f96;
14
+ --fdk-color-cyan: #8eaacb;
15
+ --fdk-color-orange: var(--ds-color-brand2-base-default);
16
+ --fdk-color-red: #b81a1a;
17
+
18
+ // Other variables
19
+ --fdk-color-interactive: var(--fdk-color-blue);
20
+ --fdk-color-interactive-negative: var(--fdk-color-cyan);
21
+ --fdk-color-text: var(--fdk-color-navy);
22
+ --fdk-color-text-negative: white;
23
+ --fdk-color-link: var(--fdk-color-interactive);
24
+ --fdk-color-link-negative: var(--fdk-color-interactive-negative);
25
+ --custom-helptext-size: 1.25rem;
26
+ }
@@ -0,0 +1,46 @@
1
+ .helptext {
2
+ --dsc-helptext-icon-size: 65%;
3
+ --dsc-helptext-icon-url: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 8 14'%3E%3Cpath fill='%23000' fill-rule='evenodd' d='M4 11a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM4 0c2.2 0 4 1.66 4 3.7 0 .98-.42 1.9-1.17 2.6l-.6.54-.29.29c-.48.46-.87.93-.94 1.41V9.5H3v-.81c0-1.26.84-2.22 1.68-3L5.42 5C5.8 4.65 6 4.2 6 3.7 6 2.66 5.1 1.83 4 1.83c-1.06 0-1.92.75-2 1.7v.15H0C0 1.66 1.8 0 4 0Z' clip-rule='evenodd'/%3E%3C/svg%3E");
4
+ --dsc-helptext-size: var(--custom-helptext-size, var(--ds-size-7));
5
+
6
+ border-radius: var(--ds-border-radius-full);
7
+ border: max(1px, calc(var(--ds-size-1) / 2)) solid; /* Allow border-width to grow with font-size */
8
+ box-sizing: border-box;
9
+ height: var(--dsc-helptext-size);
10
+ min-height: 0;
11
+ min-width: 0;
12
+ padding: 0;
13
+ position: relative;
14
+ width: var(--dsc-helptext-size);
15
+
16
+ @media (forced-colors: active) {
17
+ color: ButtonText;
18
+ }
19
+
20
+ &::before {
21
+ content: '';
22
+ border-radius: inherit;
23
+ background: currentcolor;
24
+ mask-composite: exclude;
25
+ mask-image: var(--dsc-helptext-icon-url);
26
+ mask-position: center;
27
+ mask-repeat: no-repeat;
28
+ mask-size: var(--dsc-helptext-icon-size) var(--dsc-helptext-icon-size), cover;
29
+ scale: 1.1; /* Hide tiny half pixel rendeing bug */
30
+ width: 100%;
31
+ height: 100%;
32
+
33
+ @media (forced-colors: active) {
34
+ background: ButtonText;
35
+ }
36
+ }
37
+
38
+ &:has(+ :popover-open)::before {
39
+ mask-image: var(--dsc-helptext-icon-url), linear-gradient(currentcolor, currentcolor); /* Cut icon out of currentcolor surface */
40
+ }
41
+
42
+ @media print {
43
+ display: none;
44
+ }
45
+ }
46
+
@@ -0,0 +1,42 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+
3
+ import { HelpText } from '.';
4
+
5
+ const meta: Meta<typeof HelpText> = {
6
+ component: HelpText,
7
+ title: 'HelpText',
8
+ parameters: {
9
+ docs: {
10
+ description: {
11
+ component: 'A help text component that displays additional information in a popover when clicked.',
12
+ },
13
+ },
14
+ },
15
+ argTypes: {
16
+ placement: {
17
+ control: 'select',
18
+ options: ['right', 'bottom', 'left', 'top'],
19
+ description: 'Placement of the popover',
20
+ },
21
+ 'aria-label': {
22
+ control: 'text',
23
+ description: 'Required descriptive label for screen readers',
24
+ },
25
+ },
26
+ };
27
+
28
+ export default meta;
29
+ type Story = StoryObj<typeof HelpText>;
30
+
31
+ export const Default: Story = {
32
+ args: {
33
+ 'aria-label': 'Help information',
34
+ placement: 'right',
35
+ children: 'This is helpful information that appears when you click the help icon.',
36
+ },
37
+ render: (args) => (
38
+ <div style={{ padding: '2rem', display: 'flex', alignItems: 'center', gap: '1rem' }}>
39
+ <HelpText {...args} />
40
+ </div>
41
+ ),
42
+ };
@@ -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
+