@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fellesdatakatalog/ui",
3
- "version": "1.0.15",
3
+ "version": "1.0.16",
4
4
  "description": "React UI components for Felles Datakatalog",
5
5
  "main": "dist/shared-ui.cjs",
6
6
  "module": "dist/shared-ui.es.js",
@@ -15,10 +15,19 @@
15
15
  "import": "./dist/shared-ui.es.js",
16
16
  "require": "./dist/shared-ui.cjs"
17
17
  },
18
- "./dist/styles.css": "./dist/styles.css"
18
+ "./dist/styles.css": "./dist/styles.css",
19
+ "./src/components/core/tokens.scss": "./src/components/core/tokens.scss",
20
+ "./src/components/core/tokens": "./src/components/core/tokens.scss",
21
+ "./src/components/core/mixins.scss": "./src/components/core/mixins.scss",
22
+ "./src/components/core/mixins": "./src/components/core/mixins.scss",
23
+ "./src/components/core/ds-overrides.scss": "./src/components/core/ds-overrides.scss",
24
+ "./src/components/core/ds-overrides": "./src/components/core/ds-overrides.scss",
25
+ "./src/components/core/reset.css": "./src/components/core/reset.css",
26
+ "./src/components/core/reset": "./src/components/core/reset.css"
19
27
  },
20
28
  "files": [
21
29
  "dist",
30
+ "src",
22
31
  "README.md"
23
32
  ],
24
33
  "scripts": {
@@ -0,0 +1,22 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+
3
+ import { AppBar } from '.';
4
+
5
+ const meta: Meta<typeof AppBar> = {
6
+ component: AppBar,
7
+ title: 'AppBar',
8
+ parameters: {
9
+ layout: 'centered',
10
+ },
11
+ };
12
+
13
+ export default meta;
14
+ type Story = StoryObj<typeof AppBar>;
15
+
16
+ export const Default: Story = {
17
+ render: () => (
18
+ <div style={{ padding: '1rem' }}>
19
+ <AppBar />
20
+ </div>
21
+ ),
22
+ };
@@ -0,0 +1,12 @@
1
+ import { OrgLogo } from '../org-logo';
2
+ import styles from './styles.module.scss';
3
+
4
+ const AppBar = () => {
5
+ return (
6
+ <div className={styles.appBar}>
7
+ <OrgLogo />
8
+ </div>
9
+ );
10
+ };
11
+
12
+ export { AppBar };
@@ -0,0 +1,7 @@
1
+ .appBar {
2
+ display: flex;
3
+ flex-direction: row;
4
+ align-items: center;
5
+ justify-content: space-between;
6
+ padding: 1rem;
7
+ }
@@ -0,0 +1,143 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { useState } from 'react';
3
+ import CheckboxGroup from '.';
4
+
5
+ const meta: Meta<typeof CheckboxGroup> = {
6
+ component: CheckboxGroup,
7
+ title: 'CheckboxGroup',
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
+ error: {
18
+ control: 'text',
19
+ description: 'Error message to display',
20
+ },
21
+ disabled: {
22
+ control: 'boolean',
23
+ description: 'Disable all checkboxes',
24
+ },
25
+ readOnly: {
26
+ control: 'boolean',
27
+ description: 'Make all checkboxes read-only',
28
+ },
29
+ required: {
30
+ control: 'boolean',
31
+ description: 'Make all checkboxes required',
32
+ },
33
+ },
34
+ };
35
+
36
+ export default meta;
37
+ type Story = StoryObj<typeof CheckboxGroup>;
38
+
39
+ const defaultOptions = [
40
+ { value: 'epost', label: 'E-post' },
41
+ { value: 'telefon', label: 'Telefon' },
42
+ { value: 'sms', label: 'SMS' },
43
+ ];
44
+
45
+ export const Primary: Story = {
46
+ render: () => {
47
+ const [value, setValue] = useState<string[]>(['epost']);
48
+ return (
49
+ <CheckboxGroup
50
+ legend="Hvordan vil du helst at vi skal kontakte deg?"
51
+ description="Velg alle alternativene som er relevante for deg."
52
+ options={defaultOptions}
53
+ value={value}
54
+ onChange={setValue}
55
+ />
56
+ );
57
+ },
58
+ };
59
+
60
+ export const Uncontrolled: Story = {
61
+ render: () => (
62
+ <CheckboxGroup
63
+ legend="Hvordan vil du helst at vi skal kontakte deg?"
64
+ description="Velg alle alternativene som er relevante for deg."
65
+ options={defaultOptions}
66
+ />
67
+ ),
68
+ };
69
+
70
+ export const WithError: Story = {
71
+ render: () => (
72
+ <CheckboxGroup
73
+ legend="Hvordan vil du helst at vi skal kontakte deg?"
74
+ options={defaultOptions}
75
+ error="Du må velge minst ett alternativ."
76
+ required
77
+ />
78
+ ),
79
+ };
80
+
81
+ export const Disabled: Story = {
82
+ render: () => (
83
+ <CheckboxGroup
84
+ legend="Hvordan vil du helst at vi skal kontakte deg?"
85
+ options={defaultOptions}
86
+ disabled
87
+ />
88
+ ),
89
+ };
90
+
91
+ export const ReadOnly: Story = {
92
+ render: () => (
93
+ <CheckboxGroup
94
+ legend="Hvordan vil du helst at vi skal kontakte deg?"
95
+ options={defaultOptions}
96
+ value={['epost', 'telefon']}
97
+ readOnly
98
+ />
99
+ ),
100
+ };
101
+
102
+ export const WithIndeterminate: Story = {
103
+ render: () => {
104
+ const [value, setValue] = useState<string[]>([]);
105
+ return (
106
+ <CheckboxGroup
107
+ legend="Velg kommunikasjonskanaler"
108
+ options={[
109
+ {
110
+ value: 'all',
111
+ label: 'Velg alle',
112
+ allowIndeterminate: true,
113
+ },
114
+ { value: 'epost', label: 'E-post' },
115
+ { value: 'telefon', label: 'Telefon' },
116
+ { value: 'sms', label: 'SMS' },
117
+ ]}
118
+ value={value}
119
+ onChange={setValue}
120
+ />
121
+ );
122
+ },
123
+ };
124
+
125
+ export const WithDescriptions: Story = {
126
+ render: () => {
127
+ const [value, setValue] = useState<string[]>([]);
128
+ return (
129
+ <CheckboxGroup
130
+ legend="Velg dine interesser"
131
+ description="Du kan velge flere alternativer"
132
+ options={[
133
+ { value: 'sport', label: 'Sport', description: 'Fotball, håndball, og mer' },
134
+ { value: 'musikk', label: 'Musikk', description: 'Konserter og festivaler' },
135
+ { value: 'mat', label: 'Mat og drikke', description: 'Restauranter og oppskrifter' },
136
+ ]}
137
+ value={value}
138
+ onChange={setValue}
139
+ />
140
+ );
141
+ },
142
+ };
143
+
@@ -0,0 +1,105 @@
1
+ import React, { type ReactNode } from 'react';
2
+ import {
3
+ Fieldset,
4
+ Checkbox,
5
+ ValidationMessage,
6
+ type UseCheckboxGroupProps,
7
+ useCheckboxGroup,
8
+ } from '@digdir/designsystemet-react';
9
+
10
+ export interface CheckboxGroupProps extends Omit<UseCheckboxGroupProps, '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 checkbox options
21
+ */
22
+ options: Array<{
23
+ value: string;
24
+ label: string;
25
+ description?: string;
26
+ disabled?: boolean;
27
+ allowIndeterminate?: boolean;
28
+ }>;
29
+ /**
30
+ * Controlled value - array of selected checkbox values
31
+ */
32
+ value?: string[];
33
+ /**
34
+ * Callback when the selected values change
35
+ */
36
+ onChange?: (value: string[]) => void;
37
+ /**
38
+ * Optional className for the fieldset
39
+ */
40
+ className?: string;
41
+ }
42
+
43
+ /**
44
+ * CheckboxGroup component that wraps multiple Checkbox components in a Fieldset.
45
+ * Handles state management, onChange, and value automatically.
46
+ *
47
+ * @example
48
+ * <CheckboxGroup
49
+ * legend="Hvordan vil du kontaktes?"
50
+ * description="Velg alle relevante alternativer"
51
+ * options={[
52
+ * { value: 'epost', label: 'E-post' },
53
+ * { value: 'telefon', label: 'Telefon' },
54
+ * { value: 'sms', label: 'SMS' },
55
+ * ]}
56
+ * value={selectedValues}
57
+ * onChange={(values) => setSelectedValues(values)}
58
+ * />
59
+ */
60
+ export const CheckboxGroup = ({
61
+ legend,
62
+ description,
63
+ options,
64
+ value,
65
+ onChange,
66
+ error,
67
+ disabled,
68
+ readOnly,
69
+ required,
70
+ name,
71
+ className,
72
+ }: CheckboxGroupProps) => {
73
+ const { getCheckboxProps, validationMessageProps } = useCheckboxGroup({
74
+ value,
75
+ onChange: onChange ? (nextValue) => onChange(nextValue) : undefined,
76
+ error,
77
+ disabled,
78
+ readOnly,
79
+ required,
80
+ name,
81
+ });
82
+
83
+ return (
84
+ <Fieldset className={className}>
85
+ {legend && <Fieldset.Legend>{legend}</Fieldset.Legend>}
86
+ {description && <Fieldset.Description>{description}</Fieldset.Description>}
87
+ {options.map((option) => (
88
+ <Checkbox
89
+ key={option.value}
90
+ label={option.label}
91
+ description={option.description}
92
+ disabled={option.disabled}
93
+ {...getCheckboxProps({
94
+ value: option.value,
95
+ allowIndeterminate: option.allowIndeterminate,
96
+ })}
97
+ />
98
+ ))}
99
+ {error && <ValidationMessage {...validationMessageProps} />}
100
+ </Fieldset>
101
+ );
102
+ };
103
+
104
+ export default CheckboxGroup;
105
+
@@ -0,0 +1,28 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+
3
+ import CopyButton from '.';
4
+
5
+ const meta: Meta<typeof CopyButton> = {
6
+ component: CopyButton,
7
+ title: 'CopyButton',
8
+ };
9
+
10
+ export default meta;
11
+ type Story = StoryObj<typeof CopyButton>;
12
+
13
+ export const Primary: Story = {
14
+ parameters: {
15
+ nextjs: {
16
+ appDirectory: true,
17
+ },
18
+ },
19
+ render: () => (
20
+ <div style={{ padding: '1rem' }}>
21
+ <CopyButton
22
+ copyLabel={'Kopier'}
23
+ copiedLabel={'Kopiert!'}
24
+ copyOnClick={'Tekst å kopiere'}
25
+ />
26
+ </div>
27
+ ),
28
+ };
@@ -0,0 +1,54 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { Button, type ButtonProps, Tooltip } from '@digdir/designsystemet-react';
5
+ import { FilesIcon, CheckmarkCircleFillIcon } from '@navikt/aksel-icons';
6
+ import HStack from '../hstack';
7
+
8
+ type CopyButtonProps = {
9
+ copyLabel: string;
10
+ copiedLabel: string;
11
+ copyOnClick: string;
12
+ } & ButtonProps & React.HTMLAttributes<HTMLButtonElement>;
13
+
14
+ const CopyButton = ({ copyLabel, copiedLabel, copyOnClick, ...props }: CopyButtonProps) => {
15
+ const [clicked, setClicked] = useState(false);
16
+
17
+ const copyToClipboard = (text: string) => {
18
+ setClicked(true);
19
+ navigator.clipboard.writeText(text);
20
+ };
21
+
22
+ return (
23
+ <Tooltip
24
+ // @ts-expect-error: ignore complaint that Element cannot be set as content
25
+ content={
26
+ clicked ? (
27
+ <HStack>
28
+ <CheckmarkCircleFillIcon
29
+ color='#D1F4E1'
30
+ fontSize='1.2em'
31
+ />
32
+ {copiedLabel}
33
+ </HStack>
34
+ ) : (
35
+ copyLabel
36
+ )
37
+ }
38
+ placement='top'
39
+ >
40
+ <Button
41
+ data-size='sm'
42
+ variant='secondary'
43
+ onClick={() => copyToClipboard(copyOnClick)}
44
+ onMouseOver={() => setClicked(false)}
45
+ {...props}
46
+ >
47
+ <FilesIcon aria-hidden />
48
+ <span className={'sr-only'}>{copyLabel}</span>
49
+ </Button>
50
+ </Tooltip>
51
+ );
52
+ };
53
+
54
+ export default CopyButton;
@@ -0,0 +1,105 @@
1
+ @use './mixins' as core-mixins;
2
+
3
+ /*
4
+ Remove borders when table only has one row and data-border is false or missing
5
+ */
6
+ .ds-table {
7
+ &:not([data-border='true']) {
8
+ tr:only-child {
9
+ td {
10
+ border: 0;
11
+ }
12
+ }
13
+ }
14
+ }
15
+
16
+ /*
17
+ Give Alerts proper content spacing
18
+ */
19
+ .ds-alert {
20
+ @include core-mixins.article();
21
+ }
22
+
23
+ /*
24
+ Alert disables icon when first-child is heading, this fix also removes
25
+ left extra left padding when icon is disabled/hidden
26
+ */
27
+ .ds-alert {
28
+ &:has(> :is(h1, h2, h3, h4, h5, h6):first-child) {
29
+ padding-inline-start: var(--ds-size-6);
30
+ }
31
+ }
32
+
33
+ /*
34
+ Remove dark link focus style on link button focus
35
+ */
36
+ .ds-link {
37
+ &.ds-button {
38
+ &:focus-visible {
39
+ // Set appropriate background on various buttons
40
+ &[data-variant='primary'] {
41
+ background: var(--ds-color-accent-base-default);
42
+ }
43
+ &[data-variant='secondary'],
44
+ &[data-variant='tertiary'] {
45
+ background: transparent;
46
+ color: var(--ds-color-accent-text-subtle);
47
+ }
48
+ }
49
+ }
50
+ }
51
+
52
+ /*
53
+ Remove link styling for links missing href attribute
54
+ */
55
+ .ds-link {
56
+ &:not([href]) {
57
+ color: var(--fdk-color-text);
58
+ text-decoration: none;
59
+ }
60
+ }
61
+
62
+ /*
63
+ DS override: Disable :visited link styling for links and link buttons
64
+ */
65
+ .ds-link {
66
+ &:not(.ds-button) {
67
+ // Target only passive state
68
+ &:visited:not(:hover):not(:focus):not(:focus-visible):not(:active) {
69
+ color: var(--ds-color-text-subtle);
70
+ }
71
+ }
72
+ &.ds-button {
73
+ // Target only passive state
74
+ &:visited:not(:hover):not(:focus):not(:focus-visible):not(:active) {
75
+ color: var(--dsc-button-color);
76
+ }
77
+ }
78
+ }
79
+
80
+ /*
81
+ Make icons in links have same color as text
82
+ */
83
+ .ds-link {
84
+ svg {
85
+ fill: currentColor;
86
+ }
87
+ }
88
+
89
+ /*
90
+ Disable pointer events on SVG icons inside buttons to prevent click interference
91
+ */
92
+ .ds-button {
93
+ svg {
94
+ pointer-events: none;
95
+ }
96
+ }
97
+
98
+ /*
99
+ Hack to prevent underline on links inside tags
100
+ */
101
+ .ds-tag {
102
+ a:not(:hover) {
103
+ text-decoration: none;
104
+ }
105
+ }
@@ -0,0 +1,143 @@
1
+ @mixin sr-only {
2
+ border: 0;
3
+ clip: rect(0 0 0 0);
4
+ height: 1px;
5
+ overflow: hidden;
6
+ padding: 0;
7
+ position: absolute;
8
+ white-space: nowrap;
9
+ width: 1px;
10
+ }
11
+
12
+ @mixin focus-inline {
13
+ background: var(--ds-color-focus-outer);
14
+ color: var(--ds-color-focus-inner);
15
+ outline: 2px solid transparent;
16
+ }
17
+
18
+ @mixin focus-block {
19
+ box-shadow: var(--_ds--focus, var(--dsc-focus-boxShadow));
20
+ outline: var(--_ds--focus, var(--dsc-focus-outline));
21
+ outline-offset: var(--_ds--focus, var(--ds-border-width-focus));
22
+ }
23
+
24
+ /*
25
+ Remember to set data-color-scheme='dark' on element or container
26
+ so that focus-block color tokens are correct.
27
+ */
28
+ @mixin focus-block-dark {
29
+ @include focus-block();
30
+ background: transparent;
31
+ color: var(--fdk-color-link-negative);
32
+ }
33
+
34
+ @mixin article {
35
+ line-height: 1.5em;
36
+
37
+ > * + * {
38
+ margin-top: 1rem;
39
+ }
40
+
41
+ > * + div {
42
+ margin: 2rem 0;
43
+
44
+ &:last-child {
45
+ margin-bottom: 0;
46
+ }
47
+ }
48
+
49
+ > h1,
50
+ > h2,
51
+ > h3,
52
+ > h4,
53
+ > h5,
54
+ > h6 {
55
+ font-weight: 500;
56
+ margin-bottom: 1em;
57
+ line-height: 1.25em;
58
+ }
59
+
60
+ > h1 {
61
+ font-size: 2rem;
62
+ }
63
+
64
+ > h2 {
65
+ font-size: 1.5rem;
66
+ }
67
+
68
+ > h3 {
69
+ font-size: 1.25rem;
70
+ }
71
+
72
+ > h4 {
73
+ font-size: 1.125rem;
74
+ }
75
+
76
+ > h1,
77
+ > h2,
78
+ > h3,
79
+ > h4,
80
+ > h5 {
81
+ &:not(:first-child) {
82
+ margin-top: 1.5em;
83
+ }
84
+ }
85
+
86
+ ul,
87
+ ol {
88
+ &:not([class]) {
89
+ list-style: none;
90
+ padding: 0 0 0 1.25rem;
91
+ }
92
+ }
93
+
94
+ ul,
95
+ ol {
96
+ &:not([class]) {
97
+ li {
98
+ margin-bottom: 0.5rem;
99
+ }
100
+ }
101
+ }
102
+
103
+ ul {
104
+ &:not([class]) {
105
+ list-style: disc;
106
+ }
107
+ }
108
+
109
+ ol {
110
+ &:not([class]) {
111
+ list-style: decimal;
112
+ }
113
+ }
114
+
115
+ hr {
116
+ height: 1px;
117
+ border: 0;
118
+ background: var(--fds-semantic-border-neutral-subtle);
119
+ }
120
+
121
+ img {
122
+ max-width: 100%;
123
+ padding: 0.5rem;
124
+ border: 1px solid #eee;
125
+ }
126
+
127
+ code {
128
+ &:not([class]) {
129
+ background: rgba(0, 0, 0, 0.05);
130
+ font-size: 0.85em;
131
+ padding: 0.2em 0.4em;
132
+ white-space: break-spaces;
133
+ border-radius: 6px;
134
+ color: #444;
135
+ }
136
+ }
137
+
138
+ pre {
139
+ > div:focus-visible {
140
+ @include focus-block();
141
+ }
142
+ }
143
+ }