@dbcdk/react-components 0.0.10 → 0.0.13

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 (106) hide show
  1. package/dist/components/accordion/Accordion.d.ts +2 -2
  2. package/dist/components/accordion/Accordion.js +34 -41
  3. package/dist/components/accordion/Accordion.module.css +13 -72
  4. package/dist/components/accordion/components/AccordionRow.d.ts +10 -0
  5. package/dist/components/accordion/components/AccordionRow.js +51 -0
  6. package/dist/components/accordion/components/AccordionRow.module.css +82 -0
  7. package/dist/components/breadcrumbs/Breadcrumbs.module.css +0 -1
  8. package/dist/components/button/Button.module.css +7 -7
  9. package/dist/components/card/Card.d.ts +15 -6
  10. package/dist/components/card/Card.js +39 -13
  11. package/dist/components/card/Card.module.css +22 -28
  12. package/dist/components/card/components/CardMeta.d.ts +15 -0
  13. package/dist/components/card/components/CardMeta.js +20 -0
  14. package/dist/components/card/components/CardMeta.module.css +51 -0
  15. package/dist/components/card-container/CardContainer.js +1 -1
  16. package/dist/components/card-container/CardContainer.module.css +3 -1
  17. package/dist/components/chip/Chip.module.css +7 -2
  18. package/dist/components/circle/Circle.d.ts +2 -1
  19. package/dist/components/circle/Circle.js +2 -2
  20. package/dist/components/circle/Circle.module.css +6 -2
  21. package/dist/components/code-block/CodeBlock.js +1 -1
  22. package/dist/components/code-block/CodeBlock.module.css +30 -17
  23. package/dist/components/copy-button/CopyButton.d.ts +1 -0
  24. package/dist/components/copy-button/CopyButton.js +10 -2
  25. package/dist/components/datetime-picker/DateTimePicker.d.ts +33 -8
  26. package/dist/components/datetime-picker/DateTimePicker.js +119 -78
  27. package/dist/components/datetime-picker/DateTimePicker.module.css +2 -0
  28. package/dist/components/datetime-picker/dateTimeHelpers.d.ts +15 -3
  29. package/dist/components/datetime-picker/dateTimeHelpers.js +137 -23
  30. package/dist/components/filter-field/FilterField.js +16 -11
  31. package/dist/components/filter-field/FilterField.module.css +137 -16
  32. package/dist/components/forms/checkbox/Checkbox.d.ts +2 -2
  33. package/dist/components/forms/checkbox-group/CheckboxGroup.js +1 -1
  34. package/dist/components/forms/checkbox-group/CheckboxGroup.module.css +1 -1
  35. package/dist/components/forms/form-select/FormSelect.d.ts +35 -0
  36. package/dist/components/forms/form-select/FormSelect.js +86 -0
  37. package/dist/components/forms/form-select/FormSelect.module.css +236 -0
  38. package/dist/components/forms/input/Input.d.ts +0 -3
  39. package/dist/components/forms/input/Input.js +1 -4
  40. package/dist/components/forms/input/Input.module.css +8 -7
  41. package/dist/components/forms/input-container/InputContainer.module.css +1 -1
  42. package/dist/components/forms/radio-buttons/RadioButtons.module.css +1 -0
  43. package/dist/components/forms/select/Select.js +55 -16
  44. package/dist/components/hyperlink/Hyperlink.d.ts +19 -7
  45. package/dist/components/hyperlink/Hyperlink.js +35 -11
  46. package/dist/components/hyperlink/Hyperlink.module.css +50 -2
  47. package/dist/components/interval-select/IntervalSelect.d.ts +9 -2
  48. package/dist/components/interval-select/IntervalSelect.js +21 -6
  49. package/dist/components/menu/Menu.d.ts +29 -0
  50. package/dist/components/menu/Menu.js +61 -16
  51. package/dist/components/menu/Menu.module.css +73 -5
  52. package/dist/components/overlay/modal/Modal.module.css +4 -3
  53. package/dist/components/overlay/modal/provider/ModalProvider.js +1 -3
  54. package/dist/components/overlay/side-panel/SidePanel.js +18 -1
  55. package/dist/components/overlay/side-panel/SidePanel.module.css +1 -3
  56. package/dist/components/overlay/tooltip/useTooltipTrigger.js +4 -2
  57. package/dist/components/page-layout/PageLayout.d.ts +16 -4
  58. package/dist/components/page-layout/PageLayout.js +57 -28
  59. package/dist/components/page-layout/PageLayout.module.css +153 -33
  60. package/dist/components/popover/Popover.d.ts +17 -4
  61. package/dist/components/popover/Popover.js +147 -65
  62. package/dist/components/popover/Popover.module.css +5 -0
  63. package/dist/components/sidebar/components/expandable-sidebar-item/ExpandableSidebarItem.js +22 -18
  64. package/dist/components/sidebar/providers/SidebarProvider.d.ts +4 -1
  65. package/dist/components/sidebar/providers/SidebarProvider.js +66 -18
  66. package/dist/components/split-button/SplitButton.d.ts +1 -1
  67. package/dist/components/split-button/SplitButton.js +3 -1
  68. package/dist/components/split-button/SplitButton.module.css +4 -4
  69. package/dist/components/split-pane/SplitPane.d.ts +10 -24
  70. package/dist/components/split-pane/SplitPane.js +83 -54
  71. package/dist/components/split-pane/SplitPane.module.css +11 -6
  72. package/dist/components/split-pane/provider/SplitPaneContext.js +5 -11
  73. package/dist/components/state-page/StatePage.module.css +1 -1
  74. package/dist/components/sticky-footer-layout/StickyFooterLayout.d.ts +3 -8
  75. package/dist/components/sticky-footer-layout/StickyFooterLayout.js +57 -20
  76. package/dist/components/table/Table.d.ts +8 -8
  77. package/dist/components/table/Table.js +37 -79
  78. package/dist/components/table/Table.module.css +62 -46
  79. package/dist/components/table/{tanstack.d.ts → TanstackTable.d.ts} +7 -3
  80. package/dist/components/table/TanstackTable.js +84 -0
  81. package/dist/components/table/components/column-resizer/ColumnResizer.js +1 -1
  82. package/dist/components/table/components/column-resizer/ColumnResizer.module.css +17 -7
  83. package/dist/components/table/components/table-settings/TableSettings.d.ts +13 -3
  84. package/dist/components/table/components/table-settings/TableSettings.js +55 -4
  85. package/dist/components/table/table.utils.d.ts +17 -0
  86. package/dist/components/table/table.utils.js +61 -0
  87. package/dist/components/table/tanstackTable.utils.d.ts +22 -0
  88. package/dist/components/table/tanstackTable.utils.js +104 -0
  89. package/dist/components/tabs/Tabs.d.ts +35 -12
  90. package/dist/components/tabs/Tabs.js +114 -26
  91. package/dist/components/tabs/Tabs.module.css +158 -71
  92. package/dist/hooks/useTableSettings.d.ts +23 -4
  93. package/dist/hooks/useTableSettings.js +64 -17
  94. package/dist/index.d.ts +1 -1
  95. package/dist/index.js +1 -1
  96. package/dist/src/styles/styles.css +38 -23
  97. package/dist/styles/animation.d.ts +5 -0
  98. package/dist/styles/animation.js +5 -0
  99. package/dist/styles/styles.css +38 -23
  100. package/dist/styles/themes/dbc/base.css +136 -0
  101. package/dist/styles/themes/dbc/dark.css +39 -202
  102. package/dist/styles/themes/dbc/light.css +17 -174
  103. package/dist/utils/localStorage.utils.d.ts +19 -0
  104. package/dist/utils/localStorage.utils.js +78 -0
  105. package/package.json +4 -4
  106. package/dist/components/table/tanstack.js +0 -162
@@ -61,10 +61,18 @@ function OperatorDropdown({ value, onChange, operators, size = 'sm', disabled, }
61
61
  return (_jsx(Menu.Item, { active: selected, children: _jsxs("button", { type: "button", onClick: () => handleSelect(op), disabled: disabled, children: [_jsx("span", { style: { width: 16, display: 'inline-flex', justifyContent: 'center' }, children: selected ? _jsx(Check, { size: 16 }) : null }), LABELS[op]] }) }, op));
62
62
  }) }) }));
63
63
  }
64
+ function isFilterActive(value) {
65
+ if (Array.isArray(value))
66
+ return value.length > 0;
67
+ if (typeof value === 'string')
68
+ return value.trim().length > 0;
69
+ return value != null;
70
+ }
64
71
  export function FilterField({ field, control, operator, value, onChange, operators, options = [], single = true, size = 'md', label, placeholder = 'Type value…', disabled, 'data-cy': dataCy, ...inputProps }) {
65
72
  var _a, _b;
66
73
  const [selectedOperator, setSelectedOperator] = useState(operator);
67
74
  const ops = useMemo(() => operators !== null && operators !== void 0 ? operators : DEFAULT_TEXT_OPERATORS, [operators]);
75
+ const active = isFilterActive(value);
68
76
  // Local state ONLY for input control (to avoid URL->props lag)
69
77
  const [localValue, setLocalValue] = useState((_a = value) !== null && _a !== void 0 ? _a : '');
70
78
  const debounceRef = useRef(null);
@@ -92,7 +100,7 @@ export function FilterField({ field, control, operator, value, onChange, operato
92
100
  debounceRef.current = setTimeout(() => {
93
101
  isTypingRef.current = false;
94
102
  emit({ value: nextVal });
95
- }, 250); // tweak debounce as needed
103
+ }, 250);
96
104
  };
97
105
  // Sync internal value when parent value changes (e.g. URL updates)
98
106
  useEffect(() => {
@@ -100,7 +108,6 @@ export function FilterField({ field, control, operator, value, onChange, operato
100
108
  if (control !== 'input')
101
109
  return;
102
110
  const incoming = (_a = value) !== null && _a !== void 0 ? _a : '';
103
- // don't fight the user mid-typing; once parent catches up, we allow sync again
104
111
  if (!isTypingRef.current && incoming !== localValue) {
105
112
  setLocalValue(incoming);
106
113
  }
@@ -115,25 +122,23 @@ export function FilterField({ field, control, operator, value, onChange, operato
115
122
  clearTimeout(debounceRef.current);
116
123
  };
117
124
  }, []);
118
- return (_jsxs("div", { ...(dataCy ? { 'data-cy': dataCy } : {}), className: ` ${styles.filterField} ${styles[size]} ${value ? styles.active : ''}`, children: [label ? _jsx("span", { className: `${styles.label} ${styles[size]}`, children: label }) : null, _jsx(OperatorDropdown, { value: selectedOperator, onChange: op => emit({ operator: op }), operators: ops, size: size, disabled: disabled }), _jsx("div", { className: `${control === 'input' ? 'dbc-flex' : styles.valueWrapper}`, children: control === 'input' ? (_jsx(Input, { ...inputProps, value: localValue, onChange: e => {
125
+ return (_jsxs("div", { ...(dataCy ? { 'data-cy': dataCy } : {}), className: `${styles.filterField} ${styles[size]} ${active ? styles.active : ''}`, children: [label ? _jsx("span", { className: `${styles.label} ${styles[size]}`, children: label }) : null, _jsx(OperatorDropdown, { value: selectedOperator, onChange: op => emit({ operator: op }), operators: ops, size: size, disabled: disabled }), _jsx("div", { className: `${control === 'input' ? 'dbc-flex dbc-flex-grow' : styles.valueWrapper}`, children: control === 'input' ? (_jsx(Input, { ...inputProps, value: localValue, onChange: e => {
119
126
  const next = e.currentTarget.value;
120
127
  isTypingRef.current = true;
121
- setLocalValue(next); // immediate UI update
122
- scheduleEmitValue(next); // debounced parent update
123
- }, inputSize: size, placeholder: placeholder, width: "160px", minWidth: "120px", disabled: disabled, onClear: () => {
128
+ setLocalValue(next);
129
+ scheduleEmitValue(next);
130
+ }, fullWidth: true, inputSize: size, placeholder: placeholder, width: "160px", minWidth: "120px", disabled: disabled, onClear: () => {
124
131
  isTypingRef.current = false;
125
132
  if (debounceRef.current)
126
133
  clearTimeout(debounceRef.current);
127
134
  setLocalValue('');
128
- emit({ value: '' }); // clear should be immediate
135
+ emit({ value: '' });
129
136
  } })) : single ? (_jsx(Select, { options: options, selectedValue: (_b = value) !== null && _b !== void 0 ? _b : null, onChange: v => emit({ value: v }), placeholder: placeholder, size: size, variant: "inline", onClear: () => emit({ value: '' }) })) : (_jsx(MultiSelect, { options: options, size: size, variant: "inline", selectedValues: (Array.isArray(value) ? value : []), onChange: v => {
130
137
  const current = new Set((Array.isArray(value) ? value : []));
131
- if (current.has(v)) {
138
+ if (current.has(v))
132
139
  current.delete(v);
133
- }
134
- else {
140
+ else
135
141
  current.add(v);
136
- }
137
142
  emit({ value: Array.from(current) });
138
143
  }, onClear: () => emit({ value: [] }), children: placeholder })) })] }));
139
144
  }
@@ -10,28 +10,34 @@
10
10
  border: var(--border-width-thin) solid var(--color-border-default);
11
11
  border-radius: var(--border-radius-default);
12
12
 
13
+ position: relative;
14
+
13
15
  transition:
14
16
  border-color var(--transition-fast) var(--ease-standard),
15
17
  box-shadow var(--transition-fast) var(--ease-standard),
16
18
  background-color var(--transition-fast) var(--ease-standard);
17
19
  }
18
20
 
19
- /* .filterField.active {
20
- border-color: var(--color-border-selected);
21
- background: var(--color-bg-selected);
22
- } */
21
+ /* More comfortable active state:
22
+ - less "blue outline" noise
23
+ - slightly warmer surface hint
24
+ */
25
+ .filterField.active {
26
+ border-color: color-mix(in srgb, var(--color-border-default) 75%, var(--color-border-selected));
27
+ background: color-mix(in srgb, var(--color-bg-surface) 96%, var(--color-bg-selected));
28
+ }
23
29
 
24
30
  .filterField.sm {
25
- block-size: calc(var(--component-size-sm) + var(--density));
31
+ block-size: var(--component-size-sm);
26
32
  }
27
33
  .filterField.md {
28
- block-size: calc(var(--component-size-md) + var(--density));
34
+ block-size: var(--component-size-md);
29
35
  }
30
36
 
31
37
  .filterField .label {
32
38
  display: inline-flex;
33
39
  align-items: center;
34
- padding-block: calc(var(--spacing-2xs) + var(--density));
40
+ padding-block: var(--spacing-2xs);
35
41
  padding-inline: var(--spacing-sm);
36
42
  font-size: var(--font-size-sm);
37
43
  color: var(--color-fg-muted);
@@ -39,12 +45,13 @@
39
45
  user-select: none;
40
46
  }
41
47
 
48
+ /* Operator trigger */
42
49
  .filterField .operatorTrigger {
43
50
  display: inline-flex;
44
51
  align-items: center;
45
52
  justify-content: center;
46
53
  height: 100%;
47
- padding-block: calc(var(--spacing-2xs) + var(--density));
54
+ padding-block: var(--spacing-2xs);
48
55
  padding-inline: var(--spacing-sm);
49
56
  background: var(--opac-bg-default);
50
57
  color: var(--color-fg-default);
@@ -68,30 +75,139 @@
68
75
  color: var(--color-disabled-fg);
69
76
  background: var(--color-disabled-bg);
70
77
  }
71
-
72
78
  .filterField .operatorText {
73
79
  white-space: nowrap;
74
80
  }
75
81
 
82
+ /* When active, operator is less dominant (calmer + more "token"-like) */
83
+ .filterField.active .operatorTrigger {
84
+ background: transparent;
85
+ color: var(--color-fg-muted);
86
+ }
87
+ .filterField.active .operatorTrigger:hover {
88
+ background: var(--opac-bg-dark);
89
+ color: var(--color-fg-default);
90
+ }
91
+
92
+ /* Wrapper for the select / multiselect control */
76
93
  .filterField .valueWrapper {
77
94
  display: inline-flex;
78
95
  align-items: center;
79
96
  padding: 0;
80
97
  height: 100%;
98
+
99
+ flex: 1;
100
+ min-width: 0;
81
101
  }
82
102
 
83
- .valueWrapper > * {
103
+ /* Ensure the control inside can stretch/shrink */
104
+ .filterField .valueWrapper > * {
84
105
  height: 100%;
85
- & > * {
86
- height: 100% !important;
87
- }
106
+ width: 100%;
107
+ min-width: 0;
88
108
  }
89
-
90
- .valueWrapper button {
109
+ .filterField .valueWrapper > * > * {
91
110
  height: 100% !important;
111
+ }
112
+
113
+ /* Active: emphasize VALUE area (but keep it soft) */
114
+ .filterField.active .valueWrapper {
115
+ background: color-mix(in srgb, var(--color-bg-surface) 88%, var(--color-bg-selected));
116
+ border-left: var(--border-width-thin) solid
117
+ color-mix(in srgb, var(--color-border-default) 80%, var(--color-border-selected));
118
+ border-top-right-radius: calc(var(--border-radius-default) - 1px);
119
+ border-bottom-right-radius: calc(var(--border-radius-default) - 1px);
120
+ }
121
+
122
+ /* =========================
123
+ TRIGGER BUTTON TARGETING
124
+ ========================= */
125
+
126
+ /* Select trigger button */
127
+ .filterField .valueWrapper :global(button[data-forminput]) {
128
+ width: 100%;
129
+ height: 100%;
130
+ border: 0 !important;
131
+
132
+ /* slightly more breathing room than before */
133
+ padding-inline: calc(var(--spacing-sm) + var(--spacing-2xs)) !important;
134
+
135
+ text-align: left;
136
+ justify-content: flex-start;
137
+ }
138
+
139
+ /* MultiSelect trigger button (Popover container is a div; trigger is its direct child button) */
140
+ .filterField .valueWrapper > div > button {
141
+ width: 100%;
142
+ height: 100%;
92
143
  border: 0 !important;
93
- padding-inline: var(--spacing-sm) !important;
144
+ padding-inline: calc(var(--spacing-sm) + var(--spacing-2xs)) !important;
145
+
146
+ text-align: left;
147
+ justify-content: flex-start;
148
+ }
149
+
150
+ /* Slight spacing between clear (×) and chevron for Select + MultiSelect
151
+ (feels less cramped / more intentional) */
152
+ .filterField .valueWrapper :global(button[data-forminput]) :global(.dbc-flex),
153
+ .filterField .valueWrapper > div > button {
154
+ column-gap: var(--spacing-xs);
155
+ }
156
+
157
+ /* Make the internal Select layout behave */
158
+ .filterField .valueWrapper :global(.dbc-flex) {
159
+ width: 100% !important;
160
+ min-width: 0;
161
+ }
162
+
163
+ .filterField .valueWrapper :global(.dbc-flex > span:first-child) {
164
+ flex: 1;
165
+ min-width: 0;
166
+ overflow: hidden;
167
+ text-overflow: ellipsis;
168
+ white-space: nowrap;
169
+ }
170
+
171
+ /* Keep ClearButton + chevron from stretching */
172
+ .filterField .valueWrapper :global(.dbc-flex) > *:not(span:first-child) {
173
+ flex: 0 0 auto;
174
+ }
175
+
176
+ /* For MultiSelect: label + chip should truncate nicely */
177
+ .filterField .valueWrapper > div > button > span:first-child {
178
+ display: inline-flex;
179
+ align-items: center;
180
+ gap: var(--spacing-xxs);
181
+
182
+ flex: 1;
183
+ min-width: 0;
184
+ overflow: hidden;
185
+ text-overflow: ellipsis;
186
+ white-space: nowrap;
187
+ }
188
+
189
+ /* Emphasize chosen value text in active state (but slightly less "boldy") */
190
+ .filterField.active .valueWrapper input {
191
+ font-weight: 550;
94
192
  }
193
+ .filterField.active .valueWrapper :global(button[data-forminput]),
194
+ .filterField.active .valueWrapper > div > button {
195
+ font-weight: 550 !important;
196
+ }
197
+
198
+ /* Icons calmer by default; crisp on hover/focus */
199
+ .filterField.active .valueWrapper svg {
200
+ opacity: 0.72;
201
+ }
202
+ .filterField.active .valueWrapper:hover svg,
203
+ .filterField.active .valueWrapper :global(button[data-forminput]):focus-visible svg,
204
+ .filterField.active .valueWrapper > div > button:focus-visible svg {
205
+ opacity: 1;
206
+ }
207
+
208
+ /* =========================
209
+ INPUT styling
210
+ ========================= */
95
211
 
96
212
  .filterField input {
97
213
  appearance: none;
@@ -103,6 +219,11 @@
103
219
  inline-size: auto;
104
220
  min-inline-size: 10ch;
105
221
  block-size: 100%;
222
+ border-top-left-radius: 0;
223
+ border-bottom-left-radius: 0;
224
+
225
+ /* a tiny bit more comfort */
226
+ padding-block: var(--spacing-3xs);
106
227
  }
107
228
 
108
229
  .filterField button {
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import type { JSX } from 'react';
2
+ import type { JSX, ReactNode } from 'react';
3
3
  type Variant = 'default' | 'primary' | 'outlined';
4
4
  type Size = 'sm' | 'md' | 'lg';
5
5
  interface CheckboxProps {
@@ -8,7 +8,7 @@ interface CheckboxProps {
8
8
  variant?: Variant;
9
9
  disabled?: boolean;
10
10
  modified?: boolean;
11
- label?: string;
11
+ label?: ReactNode;
12
12
  size?: Size;
13
13
  containerLabel?: string;
14
14
  error?: string;
@@ -66,7 +66,7 @@ export function CheckboxGroup({ label, options, selectedValues, onChange, onTogg
66
66
  return (_jsxs("div", { className: wrapperClassName, "data-cy": dataCy, style: { ['--checkboxgroup-action-min-width']: actionMinWidth }, children: [label && _jsx("span", { className: styles.groupLabel, children: label }), _jsx("div", { className: itemsClassName, role: "group", "aria-label": label, children: options.map(opt => {
67
67
  const isChecked = selectedSet.has(opt.value);
68
68
  const isDisabled = disabled || !!opt.disabled;
69
- return (_jsx(Checkbox, { label: opt.label, checked: isChecked, disabled: isDisabled, variant: variant, size: size, onChange: () => {
69
+ return (_jsx(Checkbox, { label: opt.label, checked: isChecked, disabled: isDisabled, variant: variant, size: "sm", onChange: () => {
70
70
  if (isDisabled)
71
71
  return;
72
72
  toggleValue(opt.value);
@@ -30,7 +30,7 @@
30
30
  .contained {
31
31
  padding: 0 var(--spacing-xs);
32
32
  border: var(--border-width-thin) solid var(--color-border-subtle);
33
- border-radius: var(--border-radius-md);
33
+ border-radius: var(--border-radius-default);
34
34
  background: var(--color-bg-surface);
35
35
  }
36
36
 
@@ -0,0 +1,35 @@
1
+ import * as React from 'react';
2
+ import { InputContainer } from '../input-container/InputContainer';
3
+ import { MultiselectOption } from '../multi-select/MultiSelect';
4
+ type InputContainerProps = React.ComponentProps<typeof InputContainer>;
5
+ export type FormSelectProps<T> = Omit<InputContainerProps, 'children' | 'htmlFor' | 'tooltip' | 'tooltipPlacement'> & {
6
+ id?: string;
7
+ name?: string;
8
+ options: MultiselectOption<T>[];
9
+ selectedValue: T | null;
10
+ onChange: (value: T) => void;
11
+ placeholder?: string;
12
+ size?: 'sm' | 'md' | 'lg';
13
+ variant?: 'outlined' | 'filled' | 'standalone';
14
+ onClear?: () => void;
15
+ /**
16
+ * Needed if T is an object and you want to use native select.
17
+ * Native <select> requires string values; we serialize using value[datakey].
18
+ */
19
+ datakey?: string;
20
+ dataCy?: string;
21
+ disabled?: boolean;
22
+ tooltip?: React.ReactNode;
23
+ tooltipPlacement?: 'top' | 'right' | 'bottom' | 'left';
24
+ includePlaceholderOption?: boolean;
25
+ /**
26
+ * Default false. If true, we allow clearing even when required=true:
27
+ * - placeholder becomes selectable
28
+ * - clear button is shown
29
+ *
30
+ * This is "odd" UX in many forms, so it's opt-in.
31
+ */
32
+ allowClearWhenRequired?: boolean;
33
+ };
34
+ export declare function FormSelect<T extends string | number | Record<string, any>>({ label, error, helpText, orientation, labelWidth, fullWidth, required, tooltip, tooltipPlacement, modified, id, name, options, selectedValue, onChange, placeholder, size, variant, onClear, datakey, dataCy, disabled, includePlaceholderOption, allowClearWhenRequired, }: FormSelectProps<T>): React.ReactNode;
35
+ export {};
@@ -0,0 +1,86 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { ChevronDown, X } from 'lucide-react';
4
+ import * as React from 'react';
5
+ import { useTooltipTrigger } from '../../../components/overlay/tooltip/useTooltipTrigger';
6
+ import styles from './FormSelect.module.css';
7
+ import { InputContainer } from '../input-container/InputContainer';
8
+ function isEqualValue(a, b, datakey) {
9
+ if (a === b)
10
+ return true;
11
+ if (!a || !b)
12
+ return false;
13
+ if (typeof a === 'object' && typeof b === 'object' && datakey) {
14
+ return a[datakey] === b[datakey];
15
+ }
16
+ return false;
17
+ }
18
+ function serializeValue(value, datakey) {
19
+ if (typeof value === 'string' || typeof value === 'number')
20
+ return String(value);
21
+ if (datakey && value && typeof value === 'object') {
22
+ const v = value[datakey];
23
+ return v == null ? '' : String(v);
24
+ }
25
+ throw new Error('FormSelect: option value is an object but no `datakey` was provided. Native select requires string/number values.');
26
+ }
27
+ function findSelectedOption(options, selectedValue, datakey) {
28
+ return options.find(o => isEqualValue(o.value, selectedValue, datakey));
29
+ }
30
+ export function FormSelect({
31
+ // InputContainer props
32
+ label, error, helpText, orientation = 'vertical', labelWidth = '160px', fullWidth = true, required, tooltip, tooltipPlacement = 'right', modified = false,
33
+ // FormSelect props
34
+ id, name, options, selectedValue, onChange, placeholder = 'Vælg', size = 'md', variant = 'outlined', onClear, datakey, dataCy, disabled, includePlaceholderOption = true, allowClearWhenRequired = false, }) {
35
+ const generatedId = React.useId();
36
+ const controlId = id !== null && id !== void 0 ? id : `select-${generatedId}`;
37
+ const describedById = `${controlId}-desc`;
38
+ const selected = React.useMemo(() => findSelectedOption(options, selectedValue, datakey), [options, selectedValue, datakey]);
39
+ const tooltipEnabled = Boolean(tooltip);
40
+ const { triggerProps, id: tooltipId } = useTooltipTrigger({
41
+ content: tooltipEnabled ? tooltip : null,
42
+ placement: tooltipPlacement,
43
+ offset: 8,
44
+ });
45
+ const describedBy = (() => {
46
+ const ids = [];
47
+ if (error || helpText)
48
+ ids.push(describedById);
49
+ if (tooltipEnabled)
50
+ ids.push(tooltipId);
51
+ return ids.length ? ids.join(' ') : undefined;
52
+ })();
53
+ const nativeValue = React.useMemo(() => {
54
+ if (selected && selected.value != null)
55
+ return serializeValue(selected.value, datakey);
56
+ return '';
57
+ }, [selected, datakey]);
58
+ const canClear = Boolean(onClear) && !disabled && (allowClearWhenRequired || !required);
59
+ const showClear = canClear && Boolean(selected);
60
+ const placeholderDisabled = Boolean(required) && !allowClearWhenRequired;
61
+ const handleNativeChange = e => {
62
+ const raw = e.target.value;
63
+ if (raw === '') {
64
+ // Clear only if consumer provided onClear
65
+ onClear === null || onClear === void 0 ? void 0 : onClear();
66
+ return;
67
+ }
68
+ const opt = options.find(o => serializeValue(o.value, datakey) === raw);
69
+ if (opt)
70
+ onChange(opt.value);
71
+ };
72
+ return (_jsxs(InputContainer, { label: label, htmlFor: controlId, fullWidth: fullWidth, error: error, helpText: helpText, orientation: orientation, labelWidth: labelWidth, required: required, modified: modified, children: [_jsxs("div", { className: [
73
+ styles.container,
74
+ fullWidth ? styles.fullWidth : '',
75
+ canClear ? styles.withButton : '',
76
+ ]
77
+ .filter(Boolean)
78
+ .join(' '), children: [_jsxs("div", { className: styles.field, children: [_jsxs("select", { ...(tooltipEnabled ? triggerProps : {}), id: controlId, name: name, className: [styles.input, styles[size], styles[variant]].filter(Boolean).join(' '), value: nativeValue, onChange: handleNativeChange, disabled: disabled, "aria-invalid": Boolean(error) || undefined, "aria-describedby": describedBy, "data-cy": dataCy !== null && dataCy !== void 0 ? dataCy : 'formselect', "data-forminput": true, children: [includePlaceholderOption && (_jsx("option", { value: "", disabled: placeholderDisabled, children: placeholder })), options.map(opt => {
79
+ const v = serializeValue(opt.value, datakey);
80
+ return (_jsx("option", { value: v, children: opt.label }, v));
81
+ })] }), _jsx("span", { className: styles.chevron, "aria-hidden": "true", children: _jsx(ChevronDown, { size: 20 }) })] }), canClear && (_jsx("button", { type: "button", className: styles.trailingButton, onClick: e => {
82
+ e.preventDefault();
83
+ e.stopPropagation();
84
+ onClear === null || onClear === void 0 ? void 0 : onClear();
85
+ }, "aria-label": "Ryd valg", disabled: !showClear, "data-cy": dataCy ? `${dataCy}-clear` : 'formselect-clear', children: _jsx(X, { size: 18, "aria-hidden": "true" }) }))] }), (error || helpText) && (_jsx("span", { id: describedById, style: { display: 'none' }, children: error !== null && error !== void 0 ? error : helpText }))] }));
86
+ }
@@ -0,0 +1,236 @@
1
+ /* FormSelect.module.css */
2
+
3
+ /* =========================================
4
+ Root container (select + optional clear)
5
+ ========================================= */
6
+
7
+ .container {
8
+ display: inline-flex;
9
+ align-items: stretch;
10
+ flex-grow: 1;
11
+ gap: 0;
12
+
13
+ inline-size: var(--input-width, auto);
14
+ min-inline-size: var(--input-min-width, 0);
15
+ max-inline-size: var(--input-max-width, none);
16
+
17
+ position: relative;
18
+ border-radius: var(--border-radius-default);
19
+ }
20
+
21
+ /* Full width variant */
22
+ .fullWidth {
23
+ display: flex;
24
+ inline-size: 100%;
25
+ min-inline-size: 0;
26
+ }
27
+
28
+ /* =========================================
29
+ Focus ring (GROUP LEVEL)
30
+ ========================================= */
31
+
32
+ .container:focus-within {
33
+ box-shadow: 0 0 0 2px var(--color-border-selected);
34
+ z-index: 1;
35
+ }
36
+
37
+ /* IMPORTANT:
38
+ When focused, do NOT also turn the select's border blue,
39
+ otherwise you get the vertical seam line next to the button. */
40
+ .container:focus-within .input {
41
+ border-color: var(--color-border-default);
42
+ }
43
+ .container:focus-within .trailingButton {
44
+ border-color: var(--color-border-default);
45
+ }
46
+
47
+ /* =========================================
48
+ Field wrapper (native select + chevron)
49
+ ========================================= */
50
+
51
+ .field {
52
+ position: relative;
53
+ display: flex;
54
+ align-items: center;
55
+ flex: 1 1 auto;
56
+ min-inline-size: 0;
57
+ color: var(--color-fg-default);
58
+ }
59
+
60
+ /* =========================================
61
+ Native select styling
62
+ ========================================= */
63
+
64
+ .input {
65
+ appearance: none;
66
+ -webkit-appearance: none;
67
+ -moz-appearance: none;
68
+
69
+ flex: 1 1 auto;
70
+ min-inline-size: 0;
71
+ inline-size: 100%;
72
+ max-inline-size: 100%;
73
+
74
+ background: var(--color-bg-surface);
75
+ font-family: var(--font-family);
76
+ font-size: var(--font-size-sm);
77
+ line-height: var(--line-height-normal);
78
+ box-sizing: border-box;
79
+ text-overflow: ellipsis;
80
+
81
+ border: var(--border-width-thin) solid var(--color-border-default);
82
+ border-radius: var(--border-radius-default);
83
+
84
+ padding-inline: var(--spacing-sm);
85
+ padding-block: var(--spacing-xs);
86
+
87
+ /* Reserve space for chevron */
88
+ padding-inline-end: calc(var(--spacing-lg) + 28px);
89
+
90
+ transition:
91
+ background-color var(--transition-fast) var(--ease-standard),
92
+ border-color var(--transition-fast) var(--ease-standard),
93
+ box-shadow var(--transition-fast) var(--ease-standard);
94
+ }
95
+
96
+ /* Disabled select */
97
+ .input:disabled {
98
+ background-color: var(--color-disabled-bg);
99
+ color: var(--color-disabled-fg);
100
+ cursor: not-allowed;
101
+ }
102
+
103
+ /* Hover state */
104
+ .input:hover:not(:disabled) {
105
+ border-color: var(--color-border-strong);
106
+ }
107
+
108
+ /* Remove default focus ring (we use group-level ring) */
109
+ .input:focus-visible {
110
+ outline: none;
111
+ /* DO NOT set border-color here */
112
+ }
113
+
114
+ /* =========================================
115
+ Variants
116
+ ========================================= */
117
+
118
+ .filled {
119
+ background-color: var(--color-bg-surface);
120
+ }
121
+
122
+ .standalone {
123
+ border-radius: var(--border-radius-rounded);
124
+ background-color: var(--color-bg-surface);
125
+ box-shadow: var(--shadow-xs), var(--shadow-md);
126
+ }
127
+
128
+ .outlined {
129
+ background-color: transparent;
130
+ }
131
+
132
+ /* =========================================
133
+ Sizes
134
+ ========================================= */
135
+
136
+ .sm {
137
+ block-size: var(--component-size-sm);
138
+ font-size: var(--font-size-sm);
139
+ }
140
+
141
+ .md {
142
+ block-size: var(--component-size-md);
143
+ font-size: var(--font-size-sm);
144
+ }
145
+
146
+ .lg {
147
+ block-size: var(--component-size-lg);
148
+ font-size: var(--font-size-lg);
149
+ }
150
+
151
+ /* =========================================
152
+ Chevron (decorative only)
153
+ ========================================= */
154
+
155
+ .chevron {
156
+ position: absolute;
157
+ inset-inline-end: var(--spacing-sm);
158
+ top: 50%;
159
+ transform: translateY(-50%);
160
+ display: inline-flex;
161
+ align-items: center;
162
+ justify-content: center;
163
+ pointer-events: none;
164
+ color: var(--color-fg-subtle);
165
+ }
166
+
167
+ .chevron svg {
168
+ inline-size: 20px;
169
+ block-size: 20px;
170
+ }
171
+
172
+ /* =========================================
173
+ Clear button integration
174
+ ========================================= */
175
+
176
+ /* Remove right radius from select when button exists */
177
+ .withButton .input {
178
+ border-top-right-radius: 0;
179
+ border-bottom-right-radius: 0;
180
+
181
+ /* Prevent double border seam; the button provides the right side */
182
+ border-right-color: transparent;
183
+ }
184
+
185
+ /* Trailing clear button */
186
+ .trailingButton {
187
+ position: relative;
188
+ z-index: 2;
189
+ flex: 0 0 auto;
190
+
191
+ display: inline-flex;
192
+ align-items: center;
193
+ justify-content: center;
194
+
195
+ padding-inline: var(--spacing-sm);
196
+
197
+ border: var(--border-width-thin) solid var(--color-border-default);
198
+
199
+ /* Seam join: don't draw left border; select covers it */
200
+ border-left-color: transparent;
201
+ margin-left: calc(-1 * var(--border-width-thin));
202
+
203
+ border-top-right-radius: var(--border-radius-default);
204
+ border-bottom-right-radius: var(--border-radius-default);
205
+
206
+ background: var(--color-bg-surface);
207
+ cursor: pointer;
208
+
209
+ transition:
210
+ background-color var(--transition-fast) var(--ease-standard),
211
+ border-color var(--transition-fast) var(--ease-standard),
212
+ box-shadow var(--transition-fast) var(--ease-standard);
213
+ }
214
+
215
+ /* Hover state */
216
+ .trailingButton:hover:not(:disabled) {
217
+ border-color: var(--color-border-strong);
218
+ background-color: var(--color-bg-surface-hover, var(--color-bg-surface));
219
+ }
220
+
221
+ /* Remove individual focus ring (group handles it) */
222
+ .trailingButton:focus-visible {
223
+ outline: none;
224
+ box-shadow: none;
225
+ }
226
+
227
+ /* Disabled clear button:
228
+ keep border/background stable; dim only icon */
229
+ .trailingButton:disabled {
230
+ cursor: default;
231
+ background: var(--color-bg-surface);
232
+ }
233
+
234
+ .trailingButton:disabled svg {
235
+ opacity: 0.4;
236
+ }