@dbcdk/react-components 0.0.47 → 0.0.49

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.
@@ -9,11 +9,6 @@
9
9
 
10
10
  .compactTriggerField {
11
11
  padding-block: 2px;
12
- padding-inline-end: calc(var(--spacing-xs) + 24px);
13
- }
14
-
15
- .compactTriggerField.fieldWithIcon {
16
- padding-inline-start: calc(var(--icon-size-md) + var(--spacing-sm));
17
12
  }
18
13
 
19
14
  .panel {
@@ -166,7 +166,7 @@ export function FilterField({ field, control, operator, value, onChange, operato
166
166
  scheduleEmitValue(next);
167
167
  }, onBlur: () => {
168
168
  flushPendingValue();
169
- }, fullWidth: true, inputSize: size, placeholder: placeholder, disabled: disabled, autoComplete: (_b = inputProps.autoComplete) !== null && _b !== void 0 ? _b : 'off', autoCorrect: (_c = inputProps.autoCorrect) !== null && _c !== void 0 ? _c : 'off', autoCapitalize: (_d = inputProps.autoCapitalize) !== null && _d !== void 0 ? _d : 'off', spellCheck: (_e = inputProps.spellCheck) !== null && _e !== void 0 ? _e : false, onClear: () => {
169
+ }, fullWidth: true, inputSize: size, placeholder: placeholder, disabled: disabled, autoComplete: (_b = inputProps.autoComplete) !== null && _b !== void 0 ? _b : 'off', autoCorrect: (_c = inputProps.autoCorrect) !== null && _c !== void 0 ? _c : 'off', autoCapitalize: (_d = inputProps.autoCapitalize) !== null && _d !== void 0 ? _d : 'none', spellCheck: (_e = inputProps.spellCheck) !== null && _e !== void 0 ? _e : false, onClear: () => {
170
170
  clearDebounce();
171
171
  pendingValueRef.current = '';
172
172
  setLocalValue('');
@@ -9,6 +9,7 @@ interface CheckboxProps {
9
9
  disabled?: boolean;
10
10
  modified?: boolean;
11
11
  label?: ReactNode;
12
+ labelAs?: 'label' | 'span';
12
13
  size?: Size;
13
14
  containerLabel?: string;
14
15
  error?: string;
@@ -21,5 +22,5 @@ interface CheckboxProps {
21
22
  id?: string;
22
23
  'data-cy'?: string;
23
24
  }
24
- export declare function Checkbox({ checked: controlled, onChange, variant, disabled, label, size, modified, containerLabel, error, helpText, orientation, labelWidth, fullWidth, required, noContainer, id, 'data-cy': dataCy, }: CheckboxProps): JSX.Element;
25
+ export declare function Checkbox({ checked: controlled, onChange, variant, disabled, label, labelAs, size, modified, containerLabel, error, helpText, orientation, labelWidth, fullWidth, required, noContainer, id, 'data-cy': dataCy, }: CheckboxProps): JSX.Element;
25
26
  export {};
@@ -4,7 +4,7 @@ import { Check } from 'lucide-react';
4
4
  import { useId, useState } from 'react';
5
5
  import styles from './Checkbox.module.css';
6
6
  import { InputContainer } from '../input-container/InputContainer';
7
- export function Checkbox({ checked: controlled, onChange, variant = 'default', disabled, label, size = 'md', modified, containerLabel, error, helpText, orientation = 'horizontal', labelWidth = '160px', fullWidth = false, required = false, noContainer = false, id, 'data-cy': dataCy, }) {
7
+ export function Checkbox({ checked: controlled, onChange, variant = 'default', disabled, label, labelAs = 'label', size = 'md', modified, containerLabel, error, helpText, orientation = 'horizontal', labelWidth = '160px', fullWidth = false, required = false, noContainer = false, id, 'data-cy': dataCy, }) {
8
8
  const [internal, setInternal] = useState(false);
9
9
  const isChecked = controlled !== null && controlled !== void 0 ? controlled : internal;
10
10
  const generatedId = useId();
@@ -18,7 +18,8 @@ export function Checkbox({ checked: controlled, onChange, variant = 'default', d
18
18
  };
19
19
  const content = (_jsxs("span", { className: styles.container, "data-cy": dataCy, children: [_jsx("button", { id: controlId, disabled: disabled, type: "button", role: "checkbox", "aria-checked": isChecked, "aria-disabled": disabled || undefined, "aria-invalid": Boolean(error) || undefined, onClick: toggle, className: [styles.checkbox, isChecked ? styles.checked : '', styles[variant], styles[size]]
20
20
  .filter(Boolean)
21
- .join(' '), children: isChecked && _jsx(Check, { className: styles.icon }) }), label && (_jsx("label", { className: styles.label, htmlFor: controlId, children: label }))] }));
21
+ .join(' '), children: isChecked && _jsx(Check, { className: styles.icon }) }), label &&
22
+ (labelAs === 'label' ? (_jsx("label", { className: styles.label, htmlFor: controlId, children: label })) : (_jsx("span", { className: styles.label, children: label })))] }));
22
23
  if (noContainer)
23
24
  return content;
24
25
  return (_jsx(InputContainer, { modified: modified, label: containerLabel, htmlFor: controlId, error: error, helpText: helpText, orientation: orientation, labelWidth: labelWidth, fullWidth: fullWidth, required: required, children: content }));
@@ -73,8 +73,8 @@
73
73
  border-bottom-right-radius: 0;
74
74
  }
75
75
 
76
- .withClear .field {
77
- padding-inline-end: calc(var(--spacing-md) + 16px);
76
+ .withClear .input {
77
+ padding-inline-end: calc(var(--spacing-xs) + 16px + var(--spacing-xs));
78
78
  }
79
79
 
80
80
  /* Global focus reset - variants own visible focus treatment */
@@ -138,7 +138,6 @@
138
138
  border-color: transparent;
139
139
  border-radius: 0;
140
140
  box-shadow: none;
141
- padding-inline: var(--spacing-xs);
142
141
  padding-block: 0;
143
142
  block-size: 100%;
144
143
  }
@@ -330,17 +329,17 @@
330
329
  }
331
330
 
332
331
  /* Leading icon */
333
- .fieldWithIcon {
334
- padding-inline-start: calc(var(--icon-size-md) + var(--spacing-lg));
332
+ .fieldWithIcon .input {
333
+ padding-inline-start: calc(var(--spacing-sm) + var(--icon-size-md) + var(--spacing-xs));
335
334
  }
336
335
 
337
- .embedded.fieldWithIcon {
338
- padding-inline-start: calc(var(--icon-size-md) + var(--spacing-xl));
336
+ .embedded.fieldWithIcon .input {
337
+ padding-inline-start: calc(var(--spacing-xs) + var(--icon-size-md) + var(--spacing-xs));
339
338
  }
340
339
 
341
340
  .icon {
342
341
  position: absolute;
343
- inset-inline-start: var(--spacing-md);
342
+ inset-inline-start: var(--spacing-sm);
344
343
  top: 50%;
345
344
  transform: translateY(-50%);
346
345
  display: inline-flex;
@@ -353,6 +352,10 @@
353
352
  transition: color var(--transition-fast) var(--ease-standard);
354
353
  }
355
354
 
355
+ .embedded .icon {
356
+ inset-inline-start: var(--spacing-xs);
357
+ }
358
+
356
359
  .field:focus-within .icon {
357
360
  color: var(--color-fg-muted);
358
361
  }
@@ -11,6 +11,7 @@ export function Typeahead({ options, mode = 'single', multiValueDisplayMode = 'c
11
11
  const inputRef = useRef(null);
12
12
  const listboxRef = useRef(null);
13
13
  const optionRefs = useRef([]);
14
+ const interactingWithOptionsRef = useRef(false);
14
15
  const listboxId = useId();
15
16
  const { onFocus: inputPropsOnFocus, onBlur: inputPropsOnBlur, onKeyDown: inputPropsOnKeyDown, onMouseDown: inputPropsOnMouseDown, onClear: inputPropsOnClear, startAdornment: inputPropsStartAdornment, ...passthroughInputProps } = inputProps !== null && inputProps !== void 0 ? inputProps : {};
16
17
  const selectedOption = useMemo(() => {
@@ -39,7 +40,11 @@ export function Typeahead({ options, mode = 'single', multiValueDisplayMode = 'c
39
40
  return option.label;
40
41
  }
41
42
  }, [multiSelectedValueChipContent]);
42
- const multiSelectionAdornment = mode === 'multi' && selectedOptions.length > 0 ? (multiValueDisplayMode === 'count' ? (_jsxs("span", { className: "dbc-muted-text dbc-sm-text", style: { whiteSpace: 'nowrap', flexShrink: 0, marginRight: 6 }, children: ["(", selectedOptions.length, ")"] })) : ((() => {
43
+ const multiSelectionAdornment = mode === 'multi' && selectedOptions.length > 0 ? (multiValueDisplayMode === 'count' ? (_jsxs("span", { className: "dbc-muted-text dbc-sm-text", style: {
44
+ whiteSpace: 'nowrap',
45
+ flexShrink: 0,
46
+ marginRight: 6,
47
+ }, children: ["(", selectedOptions.length, ")"] })) : ((() => {
43
48
  const MAX_CHIPS = 2;
44
49
  const chipsToShow = selectedOptions.slice(0, MAX_CHIPS);
45
50
  const extraCount = selectedOptions.length - MAX_CHIPS;
@@ -112,6 +117,9 @@ export function Typeahead({ options, mode = 'single', multiValueDisplayMode = 'c
112
117
  nextValues.push(option.value);
113
118
  }
114
119
  onChange(nextValues);
120
+ if (filteredOptions.length > 1) {
121
+ return;
122
+ }
115
123
  setInputValue('');
116
124
  setQuery('');
117
125
  return;
@@ -136,6 +144,12 @@ export function Typeahead({ options, mode = 'single', multiValueDisplayMode = 'c
136
144
  };
137
145
  const handleBlur = () => {
138
146
  if (mode === 'multi') {
147
+ if (interactingWithOptionsRef.current) {
148
+ interactingWithOptionsRef.current = false;
149
+ setInputValue('');
150
+ setQuery('');
151
+ return;
152
+ }
139
153
  setInputValue('');
140
154
  setQuery('');
141
155
  setOpen(false);
@@ -166,13 +180,20 @@ export function Typeahead({ options, mode = 'single', multiValueDisplayMode = 'c
166
180
  setOpen(true);
167
181
  setActiveIndex(getSelectedIndex(filteredOptions));
168
182
  }, [getSelectedIndex, filteredOptions]);
183
+ const prepareSingleSearchInput = React.useCallback(() => {
184
+ if (mode !== 'single' || !selectedOption)
185
+ return;
186
+ setInputValue('');
187
+ setQuery('');
188
+ }, [mode, selectedOption]);
169
189
  const handleOpen = React.useCallback(() => {
170
190
  if (mode === 'single' && selectedOption) {
191
+ prepareSingleSearchInput();
171
192
  openWithAllOptions();
172
193
  return;
173
194
  }
174
195
  openWithCurrentFilter();
175
- }, [mode, selectedOption, openWithAllOptions, openWithCurrentFilter]);
196
+ }, [mode, selectedOption, prepareSingleSearchInput, openWithAllOptions, openWithCurrentFilter]);
176
197
  const handleKeyDown = (e) => {
177
198
  switch (e.key) {
178
199
  case 'ArrowDown':
@@ -290,7 +311,7 @@ export function Typeahead({ options, mode = 'single', multiValueDisplayMode = 'c
290
311
  return;
291
312
  }
292
313
  if (isAlreadyFocused && mode === 'single' && selectedOption) {
293
- setQuery('');
314
+ prepareSingleSearchInput();
294
315
  setOpen(true);
295
316
  setActiveIndex(getSelectedIndex(options));
296
317
  }
@@ -308,7 +329,7 @@ export function Typeahead({ options, mode = 'single', multiValueDisplayMode = 'c
308
329
  if (e.defaultPrevented)
309
330
  return;
310
331
  handleKeyDown(e);
311
- }, placeholder: placeholder, variant: variant, inputSize: (_a = inputSize !== null && inputSize !== void 0 ? inputSize : inputProps === null || inputProps === void 0 ? void 0 : inputProps.inputSize) !== null && _a !== void 0 ? _a : 'md', width: width !== null && width !== void 0 ? width : inputProps === null || inputProps === void 0 ? void 0 : inputProps.width, autoComplete: (_b = autoComplete !== null && autoComplete !== void 0 ? autoComplete : inputProps === null || inputProps === void 0 ? void 0 : inputProps.autoComplete) !== null && _b !== void 0 ? _b : 'off', autoCorrect: (_c = autoCorrect !== null && autoCorrect !== void 0 ? autoCorrect : inputProps === null || inputProps === void 0 ? void 0 : inputProps.autoCorrect) !== null && _c !== void 0 ? _c : 'off', autoCapitalize: (_d = autoCapitalize !== null && autoCapitalize !== void 0 ? autoCapitalize : inputProps === null || inputProps === void 0 ? void 0 : inputProps.autoCapitalize) !== null && _d !== void 0 ? _d : 'off', spellCheck: (_e = spellCheck !== null && spellCheck !== void 0 ? spellCheck : inputProps === null || inputProps === void 0 ? void 0 : inputProps.spellCheck) !== null && _e !== void 0 ? _e : false, disabled: disabled, fullWidth: fullWidth, onClear: () => {
332
+ }, placeholder: placeholder, variant: variant, inputSize: (_a = inputSize !== null && inputSize !== void 0 ? inputSize : inputProps === null || inputProps === void 0 ? void 0 : inputProps.inputSize) !== null && _a !== void 0 ? _a : 'md', width: width !== null && width !== void 0 ? width : inputProps === null || inputProps === void 0 ? void 0 : inputProps.width, autoComplete: (_b = autoComplete !== null && autoComplete !== void 0 ? autoComplete : inputProps === null || inputProps === void 0 ? void 0 : inputProps.autoComplete) !== null && _b !== void 0 ? _b : 'off', autoCorrect: (_c = autoCorrect !== null && autoCorrect !== void 0 ? autoCorrect : inputProps === null || inputProps === void 0 ? void 0 : inputProps.autoCorrect) !== null && _c !== void 0 ? _c : 'off', autoCapitalize: (_d = autoCapitalize !== null && autoCapitalize !== void 0 ? autoCapitalize : inputProps === null || inputProps === void 0 ? void 0 : inputProps.autoCapitalize) !== null && _d !== void 0 ? _d : 'none', spellCheck: (_e = spellCheck !== null && spellCheck !== void 0 ? spellCheck : inputProps === null || inputProps === void 0 ? void 0 : inputProps.spellCheck) !== null && _e !== void 0 ? _e : false, disabled: disabled, fullWidth: fullWidth, onClear: () => {
312
333
  setInputValue('');
313
334
  setQuery('');
314
335
  setActiveIndex(-1);
@@ -324,11 +345,22 @@ export function Typeahead({ options, mode = 'single', multiValueDisplayMode = 'c
324
345
  const isSelected = mode === 'multi'
325
346
  ? Array.isArray(selectedValue) && selectedValue.includes(option.value)
326
347
  : option.value === selectedValue;
327
- return (_jsx(Menu.Item, { active: isActive, selected: isSelected, children: _jsx("button", { ref: node => {
348
+ const optionId = `${listboxId}-option-${index}`;
349
+ return (mode === 'multi' ? (_jsx(Menu.CheckItem, { checked: isSelected, active: isActive, interactiveRef: node => {
350
+ optionRefs.current[index] = node;
351
+ }, interactiveProps: {
352
+ id: optionId,
353
+ role: 'option',
354
+ onMouseEnter: () => setActiveIndex(index),
355
+ onMouseDown: e => {
356
+ interactingWithOptionsRef.current = true;
357
+ e.preventDefault();
358
+ },
359
+ }, label: _jsx("span", { children: option.label }), onCheckedChange: () => commitSelection(option) }, option.value)) : (_jsx(Menu.Item, { active: isActive, selected: isSelected, children: _jsx("button", { ref: node => {
328
360
  optionRefs.current[index] = node;
329
- }, id: `${listboxId}-option-${index}`, type: "button", role: "option", "aria-selected": isSelected, onMouseEnter: () => setActiveIndex(index), onMouseDown: e => {
361
+ }, id: optionId, type: "button", role: "option", "aria-selected": isSelected, onMouseEnter: () => setActiveIndex(index), onMouseDown: e => {
330
362
  e.preventDefault();
331
- }, onClick: () => commitSelection(option), children: _jsx("span", { children: option.label }) }) }, option.value));
363
+ }, onClick: () => commitSelection(option), children: _jsx("span", { children: option.label }) }) }, option.value)));
332
364
  })) : (_jsx(Menu.Item, { disabled: true, children: emptyMessage })) }) }), mode === 'multi' &&
333
365
  multiSelectedValuesDisplayMode === 'below-input' &&
334
366
  selectedOptions.length > 0 && (_jsx("div", { style: {
@@ -23,7 +23,10 @@ export interface MenuItemProps extends React.LiHTMLAttributes<HTMLLIElement> {
23
23
  export interface MenuCheckItemProps extends Omit<React.LiHTMLAttributes<HTMLLIElement>, 'onChange'> {
24
24
  label: React.ReactNode;
25
25
  checked: boolean;
26
+ active?: boolean;
26
27
  disabled?: boolean;
28
+ interactiveRef?: React.Ref<HTMLDivElement>;
29
+ interactiveProps?: React.HTMLAttributes<HTMLDivElement>;
27
30
  onCheckedChange?: (checked: boolean) => void;
28
31
  }
29
32
  export interface MenuRadioItemProps extends Omit<React.LiHTMLAttributes<HTMLLIElement>, 'onChange'> {
@@ -63,8 +63,27 @@ const MenuItem = React.forwardRef(({ children, active, selected, disabled, class
63
63
  .join(' '), type: "button", disabled: disabled, children: children }) }));
64
64
  });
65
65
  MenuItem.displayName = 'Menu.Item';
66
- const MenuCheckItem = React.forwardRef(({ label, checked, disabled, onCheckedChange, className, ...liProps }, ref) => {
67
- return (_jsx("li", { ref: ref, role: "none", className: [styles.row, className].filter(Boolean).join(' '), ...liProps, children: _jsx("div", { className: styles.interactiveChild, children: _jsx(Checkbox, { variant: "default", size: "md", noContainer: true, checked: checked, disabled: disabled, label: label, onChange: (next, _e) => onCheckedChange === null || onCheckedChange === void 0 ? void 0 : onCheckedChange(next) }) }) }));
66
+ const MenuCheckItem = React.forwardRef(({ label, checked, active, disabled, interactiveRef, interactiveProps, onCheckedChange, className, ...liProps }, ref) => {
67
+ const isSelected = checked;
68
+ const interactiveClass = [
69
+ styles.interactiveChild,
70
+ styles.item,
71
+ active ? styles.active : '',
72
+ isSelected ? styles.selected : '',
73
+ ]
74
+ .filter(Boolean)
75
+ .join(' ');
76
+ return (_jsx("li", { ref: ref, role: "none", className: [styles.row, className].filter(Boolean).join(' '), ...liProps, children: _jsx("div", { ref: interactiveRef, role: interactiveProps === null || interactiveProps === void 0 ? void 0 : interactiveProps.role, tabIndex: -1, "aria-selected": isSelected || undefined, "aria-disabled": disabled || undefined, className: interactiveClass, ...interactiveProps, onClick: event => {
77
+ var _a;
78
+ (_a = interactiveProps === null || interactiveProps === void 0 ? void 0 : interactiveProps.onClick) === null || _a === void 0 ? void 0 : _a.call(interactiveProps, event);
79
+ if (event.defaultPrevented || disabled)
80
+ return;
81
+ const target = event.target;
82
+ if (target instanceof Element && target.closest('button') !== null) {
83
+ return;
84
+ }
85
+ onCheckedChange === null || onCheckedChange === void 0 ? void 0 : onCheckedChange(!checked);
86
+ }, children: _jsx(Checkbox, { variant: "primary", size: "sm", noContainer: true, checked: checked, disabled: disabled, labelAs: "span", label: label, onChange: (next, _e) => onCheckedChange === null || onCheckedChange === void 0 ? void 0 : onCheckedChange(next) }) }) }));
68
87
  });
69
88
  MenuCheckItem.displayName = 'Menu.CheckItem';
70
89
  const MenuRadioItem = React.forwardRef(({ name, value, checked, disabled, label, onValueChange, className, ...liProps }, ref) => {
@@ -72,6 +72,7 @@
72
72
  padding-block: 2px;
73
73
  padding-inline: var(--spacing-md);
74
74
  border-radius: var(--border-radius-sm);
75
+ cursor: default;
75
76
  }
76
77
 
77
78
  /* NEW: let Checkbox/Radio consume full width so the hover area feels right */
@@ -86,6 +87,17 @@
86
87
  display: flex;
87
88
  align-items: center;
88
89
  gap: var(--spacing-sm);
90
+ inline-size: 100%;
91
+ cursor: default;
92
+ }
93
+
94
+ .row :global(.label) {
95
+ flex: 1 1 auto;
96
+ cursor: default;
97
+ }
98
+
99
+ .row :global(.label) * {
100
+ cursor: default;
89
101
  }
90
102
 
91
103
  /* Hover: support both cases (interactive element, or wrapper child) */
@@ -24,7 +24,7 @@ export function SplitPaneSecondary({ children }) {
24
24
  return _jsx("div", { className: styles.secondary, children: children });
25
25
  }
26
26
  export function SplitPaneGutter() {
27
- const { direction, primarySize, setPrimarySize, minPrimarySize, minSecondarySize, containerRef } = useSplitPaneContext();
27
+ const { direction, primarySize, setPrimarySize, minPrimarySize, minSecondarySize, containerRef, resetDefault, } = useSplitPaneContext();
28
28
  const draggingRef = useRef(false);
29
29
  const pointerIdRef = useRef(null);
30
30
  const startPosRef = useRef(0);
@@ -103,5 +103,5 @@ export function SplitPaneGutter() {
103
103
  }, [computeClamp, direction, minPrimarySize, primarySize, setPrimarySize]);
104
104
  const ariaOrientation = direction === 'horizontal' ? 'vertical' : 'horizontal';
105
105
  const { maxPrimary } = computeClamp();
106
- return (_jsx("div", { className: styles.gutter, children: _jsx("div", { className: styles.resizer, role: "separator", "aria-orientation": ariaOrientation, "aria-valuemin": Math.round(minPrimarySize), "aria-valuemax": Number.isFinite(maxPrimary) ? Math.round(maxPrimary) : undefined, "aria-valuenow": Math.round(primarySize), tabIndex: 0, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: onPointerUp, onPointerCancel: onPointerCancel, onKeyDown: onKeyDown }) }));
106
+ return (_jsx("div", { className: styles.gutter, children: _jsx("div", { className: styles.resizer, role: "separator", "aria-orientation": ariaOrientation, "aria-valuemin": Math.round(minPrimarySize), "aria-valuemax": Number.isFinite(maxPrimary) ? Math.round(maxPrimary) : undefined, "aria-valuenow": Math.round(primarySize), tabIndex: 0, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: onPointerUp, onPointerCancel: onPointerCancel, onDoubleClick: resetDefault, onKeyDown: onKeyDown }) }));
107
107
  }
@@ -3,11 +3,12 @@ export type SplitDirection = 'horizontal' | 'vertical';
3
3
  export interface SplitPaneContextValue {
4
4
  direction: SplitDirection;
5
5
  primarySize: number;
6
- setPrimarySize: React.Dispatch<React.SetStateAction<number>>;
6
+ setPrimarySize: (newSize: number) => void;
7
7
  minPrimarySize: number;
8
8
  minSecondarySize: number;
9
9
  containerRef: React.RefObject<HTMLDivElement>;
10
10
  storageKey?: string;
11
+ resetDefault: () => void;
11
12
  }
12
13
  export declare const SplitPaneContext: React.Context<SplitPaneContextValue | null>;
13
14
  export declare function useSplitPaneContext(): SplitPaneContextValue;
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import React, { useEffect, useMemo, useRef, useState } from 'react';
2
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
3
  export const SplitPaneContext = React.createContext(null);
4
4
  function clamp(n, min, max) {
5
5
  return Math.max(min, Math.min(max, n));
@@ -24,6 +24,14 @@ function writeStoredSize(key, value) {
24
24
  // ignore
25
25
  }
26
26
  }
27
+ function removeStoredSize(key) {
28
+ try {
29
+ localStorage.removeItem(key);
30
+ }
31
+ catch {
32
+ // ignore
33
+ }
34
+ }
27
35
  export function useSplitPaneContext() {
28
36
  const ctx = React.useContext(SplitPaneContext);
29
37
  if (!ctx)
@@ -34,12 +42,24 @@ export function SplitPaneProvider({ children, direction, initialPrimarySize, min
34
42
  const containerRef = useRef(null);
35
43
  // Start with initial to avoid SSR mismatch, then hydrate from storage
36
44
  const [primarySize, setPrimarySize] = useState(initialPrimarySize);
45
+ const [manualPrimarySize, setManualPrimarySize] = useState(null);
46
+ const getMaxPrimary = useCallback(() => {
47
+ const el = containerRef.current;
48
+ if (!el)
49
+ return null;
50
+ const rect = el.getBoundingClientRect();
51
+ const total = direction === 'horizontal' ? rect.width : rect.height;
52
+ if (!Number.isFinite(total) || total <= 0)
53
+ return null;
54
+ return Math.max(minPrimarySize, total - minSecondarySize);
55
+ }, [direction, minPrimarySize, minSecondarySize]);
37
56
  useEffect(() => {
38
57
  if (!storageKey)
39
58
  return;
40
59
  const stored = readStoredSize(storageKey);
41
60
  if (stored === null)
42
61
  return;
62
+ setManualPrimarySize(stored);
43
63
  setPrimarySize(stored);
44
64
  }, [storageKey]);
45
65
  useEffect(() => {
@@ -49,31 +69,48 @@ export function SplitPaneProvider({ children, direction, initialPrimarySize, min
49
69
  if (typeof ResizeObserver === 'undefined')
50
70
  return;
51
71
  const clampToContainer = () => {
52
- const rect = el.getBoundingClientRect();
53
- const total = direction === 'horizontal' ? rect.width : rect.height;
54
- if (!Number.isFinite(total) || total <= 0)
72
+ const maxPrimary = getMaxPrimary();
73
+ if (maxPrimary === null)
55
74
  return;
56
- const maxPrimary = Math.max(minPrimarySize, total - minSecondarySize);
57
- setPrimarySize(prev => clamp(prev, minPrimarySize, maxPrimary));
75
+ const targetSize = manualPrimarySize !== null && manualPrimarySize !== void 0 ? manualPrimarySize : initialPrimarySize;
76
+ setPrimarySize(clamp(targetSize, minPrimarySize, maxPrimary));
58
77
  };
59
78
  clampToContainer();
60
79
  const ro = new ResizeObserver(clampToContainer);
61
80
  ro.observe(el);
62
81
  return () => ro.disconnect();
63
- }, [direction, minPrimarySize, minSecondarySize]);
82
+ }, [getMaxPrimary, initialPrimarySize, manualPrimarySize]);
64
83
  useEffect(() => {
65
84
  if (!storageKey)
66
85
  return;
67
- writeStoredSize(storageKey, primarySize);
68
- }, [storageKey, primarySize]);
86
+ if (manualPrimarySize === null) {
87
+ removeStoredSize(storageKey);
88
+ return;
89
+ }
90
+ writeStoredSize(storageKey, manualPrimarySize);
91
+ }, [manualPrimarySize, storageKey]);
92
+ const handleSizeChange = useCallback((newSize) => {
93
+ setManualPrimarySize(newSize);
94
+ setPrimarySize(newSize);
95
+ }, []);
96
+ const resetDefault = useCallback(() => {
97
+ setManualPrimarySize(null);
98
+ const maxPrimary = getMaxPrimary();
99
+ if (maxPrimary === null) {
100
+ setPrimarySize(initialPrimarySize);
101
+ return;
102
+ }
103
+ setPrimarySize(clamp(initialPrimarySize, minPrimarySize, maxPrimary));
104
+ }, [getMaxPrimary, initialPrimarySize, minPrimarySize]);
69
105
  const value = useMemo(() => ({
70
106
  direction,
71
107
  primarySize,
72
- setPrimarySize,
108
+ setPrimarySize: handleSizeChange,
109
+ resetDefault,
73
110
  minPrimarySize,
74
111
  minSecondarySize,
75
112
  containerRef,
76
113
  storageKey,
77
- }), [direction, primarySize, minPrimarySize, minSecondarySize, storageKey]);
114
+ }), [direction, primarySize, handleSizeChange, resetDefault, minPrimarySize, minSecondarySize, storageKey]);
78
115
  return _jsx(SplitPaneContext.Provider, { value: value, children: children });
79
116
  }
@@ -3,6 +3,8 @@ export type useTimeDurationReturn = {
3
3
  started: string;
4
4
  /** Formatted end date (hydration-safe) */
5
5
  ended: string;
6
+ /** Whether the time duration has finished */
7
+ isFinished?: boolean;
6
8
  /** Formatted duration (hydration-safe) */
7
9
  duration: string;
8
10
  /** Whether client hydration has completed */
@@ -15,8 +17,10 @@ type useTimeDurationArgs = {
15
17
  dateFormat?: Intl.DateTimeFormatOptions;
16
18
  /** Placeholder shown before hydration or when date is missing */
17
19
  fallback?: string;
20
+ /** Whether to update the duration in real time (every second) */
21
+ liveUpdate?: boolean;
18
22
  /** Custom duration formatter if you don’t want the default "1t 2m 3s" */
19
23
  formatDuration?: (ms: number) => string;
20
24
  };
21
- export declare function useTimeDuration({ start, end, dateFormat, fallback, formatDuration, }: useTimeDurationArgs): useTimeDurationReturn;
25
+ export declare function useTimeDuration({ start, end, dateFormat, fallback, liveUpdate, formatDuration, }: useTimeDurationArgs): useTimeDurationReturn;
22
26
  export {};
@@ -14,9 +14,23 @@ function defaultDuration(ms) {
14
14
  }
15
15
  return `${sec}s`;
16
16
  }
17
- export function useTimeDuration({ start, end, dateFormat = { dateStyle: 'short', timeStyle: 'medium' }, fallback = '—', formatDuration = defaultDuration, }) {
17
+ export function useTimeDuration({ start, end, dateFormat = { dateStyle: 'short', timeStyle: 'medium' }, fallback = '—', liveUpdate = false, formatDuration = defaultDuration, }) {
18
18
  const [hydrated, setHydrated] = useState(false);
19
+ const [tick, setTick] = useState(0);
19
20
  useEffect(() => setHydrated(true), []);
21
+ useEffect(() => {
22
+ if (!liveUpdate)
23
+ return;
24
+ if (end && end.getTime() <= Date.now())
25
+ return;
26
+ const timer = setInterval(() => {
27
+ if (end && end.getTime() <= Date.now()) {
28
+ clearInterval(timer);
29
+ }
30
+ setTick(tick => tick + 1);
31
+ }, 1000);
32
+ return () => clearInterval(timer);
33
+ }, [liveUpdate, end]);
20
34
  const started = useMemo(() => {
21
35
  if (!start)
22
36
  return fallback;
@@ -32,11 +46,16 @@ export function useTimeDuration({ start, end, dateFormat = { dateStyle: 'short',
32
46
  return new Intl.DateTimeFormat('da-DK', dateFormat).format(end);
33
47
  }, [end, hydrated, fallback, dateFormat]);
34
48
  const duration = useMemo(() => {
35
- if (!start || !end)
36
- return fallback;
37
- if (!hydrated)
49
+ if (!start || !hydrated)
38
50
  return fallback;
51
+ const now = Date.now();
52
+ if (!end) {
53
+ return formatDuration(now - start.getTime());
54
+ }
55
+ if (end.getTime() > now) {
56
+ return formatDuration(end.getTime() - now);
57
+ }
39
58
  return formatDuration(end.getTime() - start.getTime());
40
- }, [start, end, hydrated, fallback, formatDuration]);
41
- return { started, ended, duration, hydrated };
59
+ }, [start, end, fallback, hydrated, formatDuration, tick]);
60
+ return { started, ended, isFinished: !!start && !!end, duration, hydrated };
42
61
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dbcdk/react-components",
3
- "version": "0.0.47",
3
+ "version": "0.0.49",
4
4
  "description": "Reusable React components for DBC projects",
5
5
  "license": "ISC",
6
6
  "author": "",