@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.
- package/dist/components/datetime-picker/DateTimePicker.module.css +0 -5
- package/dist/components/filter-field/FilterField.js +1 -1
- package/dist/components/forms/checkbox/Checkbox.d.ts +2 -1
- package/dist/components/forms/checkbox/Checkbox.js +3 -2
- package/dist/components/forms/input/Input.module.css +11 -8
- package/dist/components/forms/typeahead/Typeahead.js +39 -7
- package/dist/components/menu/Menu.d.ts +3 -0
- package/dist/components/menu/Menu.js +21 -2
- package/dist/components/menu/Menu.module.css +12 -0
- package/dist/components/split-pane/SplitPane.js +2 -2
- package/dist/components/split-pane/provider/SplitPaneContext.d.ts +2 -1
- package/dist/components/split-pane/provider/SplitPaneContext.js +48 -11
- package/dist/hooks/useTimeDuration.d.ts +5 -1
- package/dist/hooks/useTimeDuration.js +25 -6
- package/package.json +1 -1
|
@@ -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 : '
|
|
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 &&
|
|
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 .
|
|
77
|
-
padding-inline-end: calc(var(--spacing-
|
|
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-
|
|
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-
|
|
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-
|
|
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: {
|
|
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
|
-
|
|
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 : '
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
|
53
|
-
|
|
54
|
-
if (!Number.isFinite(total) || total <= 0)
|
|
72
|
+
const maxPrimary = getMaxPrimary();
|
|
73
|
+
if (maxPrimary === null)
|
|
55
74
|
return;
|
|
56
|
-
const
|
|
57
|
-
setPrimarySize(
|
|
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
|
-
}, [
|
|
82
|
+
}, [getMaxPrimary, initialPrimarySize, manualPrimarySize]);
|
|
64
83
|
useEffect(() => {
|
|
65
84
|
if (!storageKey)
|
|
66
85
|
return;
|
|
67
|
-
|
|
68
|
-
|
|
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 || !
|
|
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,
|
|
41
|
-
return { started, ended, duration, hydrated };
|
|
59
|
+
}, [start, end, fallback, hydrated, formatDuration, tick]);
|
|
60
|
+
return { started, ended, isFinished: !!start && !!end, duration, hydrated };
|
|
42
61
|
}
|