frey-ui 1.0.13 → 1.0.14

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 (58) hide show
  1. package/dist/cjs/Accordion/accordion.css +13 -0
  2. package/dist/cjs/Accordion/accordion.module.css.cjs +1 -1
  3. package/dist/cjs/Accordion/index.cjs +40 -13
  4. package/dist/cjs/Checkbox/checkbox.css +1 -1
  5. package/dist/cjs/Checkbox/checkbox.module.css.cjs +1 -1
  6. package/dist/cjs/Checkbox/index.cjs +4 -14
  7. package/dist/cjs/Combobox/combobox.css +140 -0
  8. package/dist/cjs/Combobox/combobox.module.css.cjs +10 -0
  9. package/dist/cjs/Combobox/index.cjs +191 -0
  10. package/dist/cjs/Dialog/dialog.css +13 -0
  11. package/dist/cjs/Dialog/index.cjs +2 -9
  12. package/dist/cjs/Popover/index.cjs +1 -1
  13. package/dist/cjs/Popover/popover.css +6 -0
  14. package/dist/cjs/Progress/index.cjs +1 -1
  15. package/dist/cjs/Progress/progress.css +13 -0
  16. package/dist/cjs/RadioGroup/index.cjs +3 -6
  17. package/dist/cjs/Spinner/index.cjs +1 -1
  18. package/dist/cjs/Switch/index.cjs +6 -3
  19. package/dist/cjs/Switch/switch.css +8 -1
  20. package/dist/cjs/Switch/switch.module.css.cjs +1 -1
  21. package/dist/cjs/Tabs/index.cjs +4 -8
  22. package/dist/cjs/TextInput/textinput.css +1 -1
  23. package/dist/cjs/ThemeProvider/index.cjs +10 -1
  24. package/dist/cjs/Toast/toast.css +11 -0
  25. package/dist/cjs/hooks/useControllableState.cjs +10 -2
  26. package/dist/cjs/index.cjs +46 -43
  27. package/dist/esm/Accordion/accordion.css +13 -0
  28. package/dist/esm/Accordion/accordion.module.css.mjs +1 -1
  29. package/dist/esm/Accordion/index.mjs +41 -14
  30. package/dist/esm/Checkbox/checkbox.css +1 -1
  31. package/dist/esm/Checkbox/checkbox.module.css.mjs +1 -1
  32. package/dist/esm/Checkbox/index.mjs +4 -14
  33. package/dist/esm/Combobox/combobox.css +140 -0
  34. package/dist/esm/Combobox/combobox.module.css.mjs +6 -0
  35. package/dist/esm/Combobox/index.mjs +187 -0
  36. package/dist/esm/Dialog/dialog.css +13 -0
  37. package/dist/esm/Dialog/index.mjs +3 -10
  38. package/dist/esm/Popover/index.mjs +1 -1
  39. package/dist/esm/Popover/popover.css +6 -0
  40. package/dist/esm/Progress/index.mjs +1 -1
  41. package/dist/esm/Progress/progress.css +13 -0
  42. package/dist/esm/RadioGroup/index.mjs +3 -6
  43. package/dist/esm/Spinner/index.mjs +1 -1
  44. package/dist/esm/Switch/index.mjs +6 -3
  45. package/dist/esm/Switch/switch.css +8 -1
  46. package/dist/esm/Switch/switch.module.css.mjs +1 -1
  47. package/dist/esm/Tabs/index.mjs +5 -9
  48. package/dist/esm/TextInput/textinput.css +1 -1
  49. package/dist/esm/ThemeProvider/index.mjs +11 -3
  50. package/dist/esm/Toast/toast.css +11 -0
  51. package/dist/esm/hooks/useControllableState.mjs +10 -3
  52. package/dist/esm/index.mjs +2 -1
  53. package/dist/index.d.ts +30 -2
  54. package/dist/types/src/Combobox/index.d.ts +25 -0
  55. package/dist/types/src/ThemeProvider/index.d.ts +1 -0
  56. package/dist/types/src/hooks/useControllableState.d.ts +5 -0
  57. package/dist/types/src/index.d.ts +4 -2
  58. package/package.json +14 -9
@@ -0,0 +1,187 @@
1
+ 'use client';
2
+ import { jsx, jsxs } from 'react/jsx-runtime';
3
+ import clsx from 'clsx';
4
+ import React from 'react';
5
+ import Field from '../Field/index.mjs';
6
+ import { useControllableValue } from '../hooks/useControllableState.mjs';
7
+ import { ChevronDownIcon } from '../Icons/ChevronDownIcon.mjs';
8
+ import { computeAriaProps } from '../utils/aria.mjs';
9
+ import styles from './combobox.module.css.mjs';
10
+
11
+ const SizeClassMap = {
12
+ sm: styles.combobox_input_sm,
13
+ md: styles.combobox_input_md,
14
+ lg: styles.combobox_input_lg
15
+ };
16
+ function findNextEnabledOptionIndex(options, startIndex, direction) {
17
+ if (options.length === 0) {
18
+ return -1;
19
+ }
20
+ let currentIndex = startIndex;
21
+ for (const _option of options) {
22
+ currentIndex = (currentIndex + direction + options.length) % options.length;
23
+ if (!options[currentIndex]?.disabled) {
24
+ return currentIndex;
25
+ }
26
+ }
27
+ return -1;
28
+ }
29
+ function dispatchInputChangeEvent(input, nextValue) {
30
+ const nativeInputValueSetter = Object.getOwnPropertyDescriptor(globalThis.HTMLInputElement.prototype, 'value')?.set;
31
+ const setInputValue = nativeInputValueSetter ??
32
+ function setInputValueFallback(value) {
33
+ this.value = value;
34
+ };
35
+ setInputValue.call(input, nextValue);
36
+ input.dispatchEvent(new Event('input', { bubbles: true }));
37
+ }
38
+ const Combobox = React.forwardRef(function Combobox({ label, options, hideLabel = false, error, helperText, size = 'md', value, defaultValue, onChange, noResultsText = 'No results found', className, style, id, disabled = false, required = false, autoComplete, onFocus, onBlur, onClick, onKeyDown, 'aria-describedby': ariaDescribedBy, 'aria-invalid': ariaInvalid, ...inputProps }, ref) {
39
+ const [currentValue, setCurrentValue] = useControllableValue(value, defaultValue ?? '');
40
+ const [open, setOpen] = React.useState(false);
41
+ const [activeIndex, setActiveIndex] = React.useState(-1);
42
+ const rootRef = React.useRef(null);
43
+ const inputRef = React.useRef(null);
44
+ const suppressOpenOnNextInputChangeRef = React.useRef(false);
45
+ const query = currentValue.trim().toLowerCase();
46
+ const filteredOptions = React.useMemo(() => {
47
+ if (!query) {
48
+ return options;
49
+ }
50
+ return options.filter((option) => {
51
+ const candidate = `${option.label} ${option.value}`.toLowerCase();
52
+ return candidate.includes(query);
53
+ });
54
+ }, [options, query]);
55
+ const closeOptions = React.useCallback(() => {
56
+ setOpen(false);
57
+ setActiveIndex(-1);
58
+ }, []);
59
+ const selectOption = React.useCallback((option) => {
60
+ setCurrentValue(option.label);
61
+ closeOptions();
62
+ if (onChange && inputRef.current) {
63
+ suppressOpenOnNextInputChangeRef.current = true;
64
+ dispatchInputChangeEvent(inputRef.current, option.label);
65
+ }
66
+ }, [closeOptions, onChange, setCurrentValue]);
67
+ React.useEffect(() => {
68
+ if (disabled) {
69
+ closeOptions();
70
+ }
71
+ }, [closeOptions, disabled]);
72
+ React.useEffect(() => {
73
+ if (!open) {
74
+ return undefined;
75
+ }
76
+ const handlePointerDown = (event) => {
77
+ if (rootRef.current &&
78
+ event.target instanceof Node &&
79
+ !rootRef.current.contains(event.target)) {
80
+ closeOptions();
81
+ }
82
+ };
83
+ document.addEventListener('mousedown', handlePointerDown);
84
+ return () => {
85
+ document.removeEventListener('mousedown', handlePointerDown);
86
+ };
87
+ }, [closeOptions, open]);
88
+ return (jsx(Field, { label: label, hideLabel: hideLabel, error: error, helperText: helperText, disabled: disabled, required: required, id: id, className: className, style: style, children: ({ inputId, describedBy, hasError }) => {
89
+ const listboxId = `${inputId}-listbox`;
90
+ const activeOptionId = activeIndex >= 0 && activeIndex < filteredOptions.length
91
+ ? `${inputId}-option-${activeIndex}`
92
+ : undefined;
93
+ const isPopupVisible = open && !disabled;
94
+ const isListboxVisible = isPopupVisible && filteredOptions.length > 0;
95
+ const handleInputChange = (event) => {
96
+ const shouldKeepClosed = suppressOpenOnNextInputChangeRef.current;
97
+ suppressOpenOnNextInputChangeRef.current = false;
98
+ setCurrentValue(event.target.value);
99
+ if (!shouldKeepClosed) {
100
+ setOpen(true);
101
+ setActiveIndex(-1);
102
+ }
103
+ onChange?.(event);
104
+ };
105
+ const handleInputFocus = (event) => {
106
+ onFocus?.(event);
107
+ if (!event.defaultPrevented && !disabled) {
108
+ setOpen(true);
109
+ }
110
+ };
111
+ const handleInputBlur = (event) => {
112
+ onBlur?.(event);
113
+ const nextFocused = event.relatedTarget;
114
+ if (!nextFocused ||
115
+ (nextFocused instanceof Node &&
116
+ rootRef.current &&
117
+ !rootRef.current.contains(nextFocused))) {
118
+ closeOptions();
119
+ }
120
+ };
121
+ const handleInputClick = (event) => {
122
+ onClick?.(event);
123
+ if (!event.defaultPrevented && !disabled) {
124
+ setOpen(true);
125
+ }
126
+ };
127
+ const handleInputKeyDown = (event) => {
128
+ onKeyDown?.(event);
129
+ if (event.defaultPrevented || disabled) {
130
+ return;
131
+ }
132
+ if (event.key === 'ArrowDown') {
133
+ event.preventDefault();
134
+ if (!open) {
135
+ setOpen(true);
136
+ }
137
+ setActiveIndex((previousIndex) => findNextEnabledOptionIndex(filteredOptions, previousIndex, 1));
138
+ return;
139
+ }
140
+ if (event.key === 'ArrowUp') {
141
+ event.preventDefault();
142
+ if (!open) {
143
+ setOpen(true);
144
+ }
145
+ setActiveIndex((previousIndex) => findNextEnabledOptionIndex(filteredOptions, previousIndex, -1));
146
+ return;
147
+ }
148
+ if (event.key === 'Enter' && open && activeIndex >= 0) {
149
+ event.preventDefault();
150
+ const selectedOption = filteredOptions[activeIndex];
151
+ if (selectedOption) {
152
+ selectOption(selectedOption);
153
+ }
154
+ return;
155
+ }
156
+ if (event.key === 'Escape') {
157
+ event.preventDefault();
158
+ closeOptions();
159
+ }
160
+ };
161
+ return (jsxs("div", { ref: rootRef, className: styles.combobox_root, children: [jsxs("div", { className: styles.combobox_input_wrapper, children: [jsx("input", { ref: (node) => {
162
+ inputRef.current = node;
163
+ if (typeof ref === 'function') {
164
+ ref(node);
165
+ return;
166
+ }
167
+ if (ref) {
168
+ ref.current = node;
169
+ }
170
+ }, id: inputId, type: 'text', role: 'combobox', value: currentValue, disabled: disabled, required: required, autoComplete: autoComplete ?? 'off', "aria-autocomplete": 'list', "aria-haspopup": 'listbox', "aria-expanded": isListboxVisible, "aria-controls": isListboxVisible ? listboxId : undefined, "aria-activedescendant": isListboxVisible ? activeOptionId : undefined, className: clsx(styles.combobox_input, SizeClassMap[size], hasError && styles.combobox_input_error), onChange: handleInputChange, onFocus: handleInputFocus, onBlur: handleInputBlur, onClick: handleInputClick, onKeyDown: handleInputKeyDown, ...computeAriaProps(hasError, describedBy, ariaDescribedBy, ariaInvalid), ...inputProps }), jsx(ChevronDownIcon, { className: styles.combobox_icon, size: 16 })] }), isPopupVisible && !isListboxVisible && (jsx("div", { role: 'status', "aria-live": 'polite', className: styles.combobox_listbox, children: jsx("p", { className: styles.combobox_empty_state, children: noResultsText }) })), isListboxVisible && (jsx("div", { id: listboxId, role: 'listbox', className: styles.combobox_listbox, children: filteredOptions.map((option, index) => {
171
+ const isActive = activeIndex === index;
172
+ const isDisabled = Boolean(option.disabled);
173
+ return (jsx("button", { id: `${inputId}-option-${index}`, type: 'button', role: 'option', disabled: isDisabled, tabIndex: -1, "aria-selected": isActive, "aria-disabled": isDisabled || undefined, className: clsx(styles.combobox_option, {
174
+ [styles.combobox_option_active]: isActive,
175
+ [styles.combobox_option_disabled]: isDisabled
176
+ }), onMouseEnter: isDisabled ? undefined : () => setActiveIndex(index), onMouseDown: isDisabled
177
+ ? undefined
178
+ : (event) => {
179
+ event.preventDefault();
180
+ }, onClick: isDisabled ? undefined : () => selectOption(option), children: option.label }, option.value));
181
+ }) }))] }));
182
+ } }));
183
+ });
184
+ Combobox.displayName = 'Combobox';
185
+
186
+ export { Combobox as default };
187
+ //# sourceMappingURL=index.mjs.map
@@ -142,3 +142,16 @@
142
142
  .dialog_body_39a5839d {
143
143
  margin-top: 0.875rem;
144
144
  }
145
+
146
+ @media (prefers-reduced-motion: reduce) {
147
+ .dialog_root_1c624aa8,
148
+ .dialog_root_1c624aa8::backdrop,
149
+ .dialog_content_ee12e897,
150
+ .dialog_close_e5b07e60 {
151
+ transition: none;
152
+ }
153
+
154
+ .dialog_content_ee12e897 {
155
+ transform: none;
156
+ }
157
+ }
@@ -1,7 +1,8 @@
1
1
  'use client';
2
2
  import { jsx, jsxs } from 'react/jsx-runtime';
3
3
  import clsx from 'clsx';
4
- import React, { createContext, useContext, useId, useState } from 'react';
4
+ import React, { createContext, useContext, useId } from 'react';
5
+ import { useControllableState } from '../hooks/useControllableState.mjs';
5
6
  import { CloseIcon } from '../Icons/CloseIcon.mjs';
6
7
  import { mergeRefs } from '../utils/mergeRefs.mjs';
7
8
  import Portal from '../utils/Portal.mjs';
@@ -18,15 +19,7 @@ function useDialogContext() {
18
19
  }
19
20
  const DialogRoot = function Dialog({ open, defaultOpen = false, onOpenChange, children }) {
20
21
  const idPrefix = useId();
21
- const isControlled = open !== undefined;
22
- const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
23
- const currentOpen = isControlled ? open : uncontrolledOpen;
24
- const handleOpenChange = React.useCallback((nextOpen) => {
25
- if (!isControlled) {
26
- setUncontrolledOpen(nextOpen);
27
- }
28
- onOpenChange?.(nextOpen);
29
- }, [isControlled, onOpenChange]);
22
+ const [currentOpen, handleOpenChange] = useControllableState(open, defaultOpen, onOpenChange);
30
23
  const contextValue = React.useMemo(() => ({ open: currentOpen, onOpenChange: handleOpenChange, idPrefix }), [currentOpen, handleOpenChange, idPrefix]);
31
24
  return (jsx(DialogContext.Provider, { value: contextValue, children: children }));
32
25
  };
@@ -115,7 +115,7 @@ const PopoverContent = React.forwardRef(function PopoverContent({ className, sty
115
115
  const floatingProps = getFloatingProps(props);
116
116
  if (!open)
117
117
  return null;
118
- return (jsx(Portal, { children: jsx(FloatingFocusManager, { context: floatingContext, modal: true, returnFocus: true, outsideElementsInert: false, initialFocus: 0, children: jsx("div", { id: `${idPrefix}-content`, ref: mergeRefs(ref, setFloating), "aria-live": 'polite', className: clsx(styles.popover_content, className), style: {
118
+ return (jsx(Portal, { children: jsx(FloatingFocusManager, { context: floatingContext, modal: true, returnFocus: true, outsideElementsInert: false, initialFocus: 0, children: jsx("div", { id: `${idPrefix}-content`, ref: mergeRefs(ref, setFloating), className: clsx(styles.popover_content, className), style: {
119
119
  ...floatingStyles,
120
120
  ...style
121
121
  }, ...floatingProps, children: children }) }) }));
@@ -23,3 +23,9 @@
23
23
  transform: translateY(0) scale(1);
24
24
  }
25
25
  }
26
+
27
+ @media (prefers-reduced-motion: reduce) {
28
+ .popover_content_0199c0e3 {
29
+ animation: none;
30
+ }
31
+ }
@@ -17,7 +17,7 @@ const Progress = React.forwardRef(function Progress({ value = 0, max = 100, inde
17
17
  const safeValue = Number.isFinite(value) ? clampValue(value, safeMax) : 0;
18
18
  const percent = (safeValue / safeMax) * 100;
19
19
  const valueText = `${Math.round(percent)}%`;
20
- return (jsxs("div", { ref: ref, className: clsx(styles.progress_root, className), style: style, ...rootProps, children: [(label || showValue) && (jsxs("div", { className: styles.progress_header, children: [label && jsx("span", { className: styles.progress_label, children: label }), showValue && !indeterminate && (jsx("span", { className: styles.progress_value, children: valueText }))] })), jsx("div", { className: clsx(styles.progress_track_wrapper, SizeClassMap[size]), children: jsx("progress", { className: clsx(styles.progress_track, indeterminate && styles.progress_track_indeterminate, barClassName), "aria-label": label ?? 'Progress', value: indeterminate ? undefined : safeValue, max: safeMax }) })] }));
20
+ return (jsxs("div", { ref: ref, className: clsx(styles.progress_root, className), style: style, ...rootProps, children: [(label || showValue) && (jsxs("div", { className: styles.progress_header, children: [label && jsx("span", { className: styles.progress_label, children: label }), showValue && !indeterminate && (jsx("span", { className: styles.progress_value, children: valueText }))] })), jsx("div", { className: clsx(styles.progress_track_wrapper, SizeClassMap[size]), children: jsx("progress", { className: clsx(styles.progress_track, indeterminate && styles.progress_track_indeterminate, barClassName), "aria-label": label ?? 'Progress', "aria-busy": indeterminate || undefined, "aria-valuenow": indeterminate ? undefined : safeValue, "aria-valuemax": safeMax, "aria-valuetext": indeterminate ? 'Loading' : valueText, value: indeterminate ? undefined : safeValue, max: safeMax }) })] }));
21
21
  });
22
22
  Progress.displayName = 'Progress';
23
23
 
@@ -84,3 +84,16 @@
84
84
  transform: translateX(320%);
85
85
  }
86
86
  }
87
+
88
+ @media (prefers-reduced-motion: reduce) {
89
+ .progress_track_d33f2771::-webkit-progress-value,
90
+ .progress_track_d33f2771::-moz-progress-bar {
91
+ transition: none;
92
+ }
93
+
94
+ .progress_track_indeterminate_50b31de5::before {
95
+ animation: none;
96
+ width: 60%;
97
+ transform: translateX(0);
98
+ }
99
+ }
@@ -3,6 +3,7 @@ import { jsx, jsxs } from 'react/jsx-runtime';
3
3
  import clsx from 'clsx';
4
4
  import React from 'react';
5
5
  import Field from '../Field/index.mjs';
6
+ import { useControllableValue } from '../hooks/useControllableState.mjs';
6
7
  import styles from './radiogroup.module.css.mjs';
7
8
 
8
9
  const OrientationClassMap = {
@@ -12,13 +13,9 @@ const OrientationClassMap = {
12
13
  const RadioGroup = React.forwardRef(function RadioGroup({ label, options, value, defaultValue, onChange, hideLabel = false, helperText, error, disabled = false, required = false, orientation = 'vertical', name, id, className, style, ...groupProps }, ref) {
13
14
  const generatedName = React.useId();
14
15
  const groupName = name ?? generatedName;
15
- const isControlled = typeof value === 'string';
16
- const [internalValue, setInternalValue] = React.useState(defaultValue ?? '');
17
- const selectedValue = isControlled ? value : internalValue;
16
+ const [selectedValue, setSelectedValue] = useControllableValue(value, defaultValue ?? '');
18
17
  const handleChange = (event) => {
19
- if (!isControlled) {
20
- setInternalValue(event.target.value);
21
- }
18
+ setSelectedValue(event.target.value);
22
19
  onChange?.(event);
23
20
  };
24
21
  return (jsx(Field, { label: label, hideLabel: hideLabel, helperText: helperText, error: error, disabled: disabled, required: required, id: id, className: className, style: style, labelElement: 'span', children: ({ inputId, labelId, describedBy, hasError }) => (jsx("div", { ref: ref, id: inputId, role: 'radiogroup', "aria-labelledby": labelId, "aria-describedby": describedBy, "aria-invalid": hasError || undefined, className: clsx(styles.radio_group, OrientationClassMap[orientation], disabled && styles.radio_group_disabled), ...groupProps, children: options.map((option, index) => {
@@ -15,7 +15,7 @@ function resolveSize(size) {
15
15
  }
16
16
  return SpinnerSizeMap[size ?? 'md'];
17
17
  }
18
- const Spinner = React.forwardRef(function Spinner({ size = 'md', label = 'Loading', className, style }, ref) {
18
+ const Spinner = React.forwardRef(function Spinner({ size, label = 'Loading', className, style }, ref) {
19
19
  const resolvedSize = resolveSize(size);
20
20
  return (jsxs("output", { ref: ref, className: clsx(styles.spinner_root, className), style: {
21
21
  '--spinner-size': `${resolvedSize}px`,
@@ -2,6 +2,7 @@
2
2
  import { jsxs, jsx } from 'react/jsx-runtime';
3
3
  import clsx from 'clsx';
4
4
  import React, { useId } from 'react';
5
+ import { useControllableState } from '../hooks/useControllableState.mjs';
5
6
  import styles from './switch.module.css.mjs';
6
7
 
7
8
  const SizeClassMap = {
@@ -9,10 +10,12 @@ const SizeClassMap = {
9
10
  md: styles['switch-md'],
10
11
  lg: styles['switch-lg']
11
12
  };
12
- const Switch = React.forwardRef(function Switch({ label, hideLabel = false, size = 'md', className, style, id, disabled = false, onChange, onKeyDown, ...inputProps }, ref) {
13
+ const Switch = React.forwardRef(function Switch({ label, hideLabel = false, size = 'md', className, style, id, disabled = false, onChange, onKeyDown, checked, defaultChecked, ...inputProps }, ref) {
13
14
  const generatedId = useId();
14
15
  const inputId = id ?? generatedId;
16
+ const [currentChecked, setCurrentChecked] = useControllableState(checked, Boolean(defaultChecked));
15
17
  const handleChange = (event) => {
18
+ setCurrentChecked(event.target.checked);
16
19
  onChange?.(event);
17
20
  };
18
21
  const handleKeyDown = (event) => {
@@ -24,8 +27,8 @@ const Switch = React.forwardRef(function Switch({ label, hideLabel = false, size
24
27
  };
25
28
  return (jsxs("div", { className: clsx(styles['switch-container'], className), style: style, children: [jsxs("span", { className: clsx(styles.switch, SizeClassMap[size], {
26
29
  [styles['switch-disabled']]: disabled
27
- }), "aria-disabled": disabled || undefined, children: [jsx("input", { type: 'checkbox', role: 'switch', id: inputId, onChange: handleChange, onKeyDown: handleKeyDown, disabled: disabled, "aria-checked": inputProps.checked, ref: ref, ...inputProps }), jsx("span", { className: styles.slider, "aria-hidden": 'true' })] }), jsx("label", { htmlFor: inputId, className: clsx(styles.label, {
28
- [styles['visually-hidden']]: hideLabel
30
+ }), "aria-disabled": disabled || undefined, children: [jsx("input", { type: 'checkbox', role: 'switch', id: inputId, checked: currentChecked, onChange: handleChange, onKeyDown: handleKeyDown, disabled: disabled, "aria-checked": currentChecked, ref: ref, ...inputProps }), jsx("span", { className: styles.slider, "aria-hidden": 'true' })] }), jsx("label", { htmlFor: inputId, className: clsx(styles.label, {
31
+ [styles.visually_hidden]: hideLabel
29
32
  }), children: label })] }));
30
33
  });
31
34
  Switch.displayName = 'Switch';
@@ -110,7 +110,7 @@
110
110
  cursor: not-allowed;
111
111
  }
112
112
 
113
- .visually-hidden_75ccb710 {
113
+ .visually_hidden_3bca186b {
114
114
  position: absolute;
115
115
  width: 1px;
116
116
  height: 1px;
@@ -121,3 +121,10 @@
121
121
  white-space: nowrap;
122
122
  border: 0;
123
123
  }
124
+
125
+ @media (prefers-reduced-motion: reduce) {
126
+ .slider_5603991a,
127
+ .slider_5603991a::before {
128
+ transition: none;
129
+ }
130
+ }
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
  import './switch.css';
3
- var styles = {"switch-container":"switch-container_63905009","switch":"switch_011a552d","switch-sm":"switch-sm_3a85b947","switch-md":"switch-md_e3005d26","switch-lg":"switch-lg_daceb03b","slider":"slider_5603991a","switch-disabled":"switch-disabled_f3161357","label":"label_3314af1a","visually-hidden":"visually-hidden_75ccb710"};
3
+ var styles = {"switch-container":"switch-container_63905009","switch":"switch_011a552d","switch-sm":"switch-sm_3a85b947","switch-md":"switch-md_e3005d26","switch-lg":"switch-lg_daceb03b","slider":"slider_5603991a","switch-disabled":"switch-disabled_f3161357","label":"label_3314af1a","visually_hidden":"visually_hidden_3bca186b"};
4
4
 
5
5
  export { styles as default };
6
6
  //# sourceMappingURL=switch.module.css.mjs.map
@@ -1,7 +1,8 @@
1
1
  'use client';
2
2
  import { jsx } from 'react/jsx-runtime';
3
3
  import clsx from 'clsx';
4
- import React, { createContext, useId, useState, useContext } from 'react';
4
+ import React, { createContext, useId, useContext } from 'react';
5
+ import { useControllableValue } from '../hooks/useControllableState.mjs';
5
6
  import { useRovingCollection } from '../hooks/useRovingCollection.mjs';
6
7
  import { mergeRefs } from '../utils/mergeRefs.mjs';
7
8
  import styles from './tabs.module.css.mjs';
@@ -17,15 +18,10 @@ function useTabsContext() {
17
18
  const TabsRoot = React.forwardRef(function Tabs({ value, defaultValue, onValueChange, className, ...props }, ref) {
18
19
  const idPrefix = useId();
19
20
  const triggerCollection = useRovingCollection();
20
- const isControlled = value !== undefined;
21
- const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue ?? '');
22
- const currentValue = isControlled ? value : uncontrolledValue;
21
+ const [currentValue, setCurrentValue] = useControllableValue(value, defaultValue ?? '', onValueChange);
23
22
  const handleValueChange = React.useCallback((nextValue) => {
24
- if (!isControlled) {
25
- setUncontrolledValue(nextValue);
26
- }
27
- onValueChange?.(nextValue);
28
- }, [isControlled, onValueChange]);
23
+ setCurrentValue(nextValue);
24
+ }, [setCurrentValue]);
29
25
  const contextValue = React.useMemo(() => ({
30
26
  value: currentValue,
31
27
  onValueChange: handleValueChange,
@@ -80,7 +80,7 @@
80
80
  margin: 0;
81
81
  }
82
82
 
83
- .visually-hidden_2e7218f5 {
83
+ .visually_hidden_6fce66d0 {
84
84
  position: absolute;
85
85
  width: 1px;
86
86
  height: 1px;
@@ -1,15 +1,23 @@
1
1
  'use client';
2
2
  import { jsx } from 'react/jsx-runtime';
3
3
  import clsx from 'clsx';
4
- import { createContext, useState, useEffect, useMemo } from 'react';
4
+ import { createContext, useState, useEffect, useMemo, useContext } from 'react';
5
5
  import styles from './themeprovider.module.css.mjs';
6
6
 
7
7
  const ThemeContext = createContext(null);
8
+ function useTheme() {
9
+ const context = useContext(ThemeContext);
10
+ if (!context) {
11
+ throw new Error('useTheme must be used within a ThemeProvider.');
12
+ }
13
+ return context;
14
+ }
8
15
  function ThemeProvider({ children, theme = 'light', highContrast = false, id, className, style }) {
9
16
  const [systemTheme, setSystemTheme] = useState('light');
10
17
  useEffect(() => {
11
- if (theme !== 'system')
18
+ if (theme !== 'system' || typeof globalThis.matchMedia !== 'function') {
12
19
  return;
20
+ }
13
21
  const mediaQuery = globalThis.matchMedia('(prefers-color-scheme: dark)');
14
22
  setSystemTheme(mediaQuery.matches ? 'dark' : 'light');
15
23
  const handleChange = (e) => {
@@ -23,5 +31,5 @@ function ThemeProvider({ children, theme = 'light', highContrast = false, id, cl
23
31
  return (jsx(ThemeContext.Provider, { value: contextValue, children: jsx("div", { id: id, className: clsx('frey-theme-provider', styles['frey-theme-provider'], className), style: style, "data-frey-theme": resolvedTheme, "data-frey-high-contrast": highContrast, children: children }) }));
24
32
  }
25
33
 
26
- export { ThemeContext, ThemeProvider as default };
34
+ export { ThemeContext, ThemeProvider as default, useTheme };
27
35
  //# sourceMappingURL=index.mjs.map
@@ -149,3 +149,14 @@
149
149
  transform: translateY(0);
150
150
  }
151
151
  }
152
+
153
+ @media (prefers-reduced-motion: reduce) {
154
+ .toast_ab9f2513 {
155
+ animation: none;
156
+ }
157
+
158
+ .toast_action_ea986326,
159
+ .toast_close_0f117163 {
160
+ transition: none;
161
+ }
162
+ }
@@ -2,10 +2,10 @@
2
2
  import { useState, useCallback } from 'react';
3
3
 
4
4
  /**
5
- * Manages a boolean value that can be either controlled (value supplied by parent)
5
+ * Manages a value that can be either controlled (value supplied by parent)
6
6
  * or uncontrolled (managed internally with an optional callback).
7
7
  */
8
- function useControllableState(controlled, defaultValue, onChange) {
8
+ function useControllableValue(controlled, defaultValue, onChange) {
9
9
  const isControlled = controlled !== undefined;
10
10
  const [uncontrolled, setUncontrolled] = useState(defaultValue);
11
11
  const value = isControlled ? controlled : uncontrolled;
@@ -17,6 +17,13 @@ function useControllableState(controlled, defaultValue, onChange) {
17
17
  }, [isControlled, onChange]);
18
18
  return [value, setValue];
19
19
  }
20
+ /**
21
+ * Manages a boolean value that can be either controlled (value supplied by parent)
22
+ * or uncontrolled (managed internally with an optional callback).
23
+ */
24
+ function useControllableState(controlled, defaultValue, onChange) {
25
+ return useControllableValue(controlled, defaultValue, onChange);
26
+ }
20
27
 
21
- export { useControllableState };
28
+ export { useControllableState, useControllableValue };
22
29
  //# sourceMappingURL=useControllableState.mjs.map
@@ -9,6 +9,7 @@ export { default as Button } from './Button/index.mjs';
9
9
  export { Card } from './Card/index.mjs';
10
10
  export { default as Checkbox } from './Checkbox/index.mjs';
11
11
  export { default as Chip } from './Chip/index.mjs';
12
+ export { default as Combobox } from './Combobox/index.mjs';
12
13
  export { Dialog } from './Dialog/index.mjs';
13
14
  export { DropdownMenu } from './DropdownMenu/index.mjs';
14
15
  export { default as Field } from './Field/index.mjs';
@@ -37,7 +38,7 @@ export { Table } from './Table/index.mjs';
37
38
  export { Tabs } from './Tabs/index.mjs';
38
39
  export { default as Textarea } from './Textarea/index.mjs';
39
40
  export { default as TextInput } from './TextInput/index.mjs';
40
- export { default as ThemeProvider } from './ThemeProvider/index.mjs';
41
+ export { default as ThemeProvider, useTheme } from './ThemeProvider/index.mjs';
41
42
  export { ToastProvider, useToast } from './Toast/index.mjs';
42
43
  export { default as Tooltip } from './Tooltip/index.mjs';
43
44
  //# sourceMappingURL=index.mjs.map
package/dist/index.d.ts CHANGED
@@ -217,6 +217,29 @@ type ChipComponent = (<E extends ChipElement = 'span'>(props: Readonly<ChipProps
217
217
  };
218
218
  declare const Chip: ChipComponent;
219
219
 
220
+ type ComboboxSize = 'sm' | 'md' | 'lg';
221
+ type ComboboxOption = {
222
+ value: string;
223
+ label: string;
224
+ disabled?: boolean;
225
+ };
226
+ type ComboboxProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type' | 'size' | 'className' | 'style' | 'value' | 'defaultValue' | 'onChange'> & {
227
+ label: string;
228
+ options: ReadonlyArray<ComboboxOption>;
229
+ hideLabel?: boolean;
230
+ error?: string;
231
+ helperText?: string;
232
+ size?: ComboboxSize;
233
+ value?: string;
234
+ defaultValue?: string;
235
+ onChange?: React.ChangeEventHandler<HTMLInputElement>;
236
+ noResultsText?: string;
237
+ className?: string;
238
+ style?: React.CSSProperties;
239
+ };
240
+ type ComboboxComponent = React.ForwardRefExoticComponent<Readonly<ComboboxProps> & React.RefAttributes<HTMLInputElement>>;
241
+ declare const Combobox: ComboboxComponent;
242
+
220
243
  type DialogProps = {
221
244
  open?: boolean;
222
245
  defaultOpen?: boolean;
@@ -611,6 +634,11 @@ type ThemeProviderProps = {
611
634
  className?: string;
612
635
  style?: React.CSSProperties;
613
636
  };
637
+ type ThemeContextValue = {
638
+ resolvedTheme: 'light' | 'dark';
639
+ highContrast: boolean;
640
+ };
641
+ declare function useTheme(): ThemeContextValue;
614
642
  declare function ThemeProvider({ children, theme, highContrast, id, className, style }: Readonly<ThemeProviderProps>): React.JSX.Element;
615
643
 
616
644
  type ToastVariant = 'info' | 'success' | 'warning' | 'error';
@@ -659,5 +687,5 @@ type TooltipProps = {
659
687
  };
660
688
  declare function Tooltip({ children, asChild, content, open, defaultOpen, onOpenChange, placement, offset, delay, id, className, style }: Readonly<TooltipProps>): React.JSX.Element;
661
689
 
662
- export { Accordion, Alert, Avatar, Badge, Box, Breadcrumbs, Button, Card, CheckIcon, Checkbox, ChevronDownIcon, Chip, CircleCheckIcon, CircleInfoIcon, CircleXIcon, CloseIcon, Dialog, DropdownMenu, Field, Flex, Grid, Link, MinusIcon, Popover, Progress, RadioGroup, Select, Skeleton, Spinner, Stack, Switch, Table, Tabs, TextInput, Textarea, ThemeProvider, ToastProvider, Tooltip, TriangleAlertIcon, useToast };
663
- export type { AccordionContentProps, AccordionItemProps, AccordionProps, AccordionTriggerProps, AccordionType, AlertProps, AlertVariant, AvatarProps, AvatarSize, AvatarStatus, BadgeProps, BadgeSize, BadgeTone, BadgeVariant, BoxProps, BreadcrumbsCurrentProps, BreadcrumbsItemProps, BreadcrumbsLinkProps, BreadcrumbsListProps, BreadcrumbsProps, ButtonProps, ButtonSize, ButtonVariant, CardContentProps, CardFooterProps, CardHeaderProps, CardProps, CardTitleProps, CheckboxProps, CheckboxSize, ChipElement, ChipProps, ColorToken, DialogBodyProps, DialogContentProps, DialogDescriptionProps, DialogFooterProps, DialogHeaderProps, DialogProps, DialogTitleProps, DialogTriggerProps, DropdownMenuContentProps, DropdownMenuItemProps, DropdownMenuPlacement, DropdownMenuProps, DropdownMenuTriggerProps, FieldProps, FieldRenderProps, FlexProps, FreyTheme, GridProps, IconProps, IconSizeToken, IconStrokeToken, LinkColor, LinkProps, LinkUnderline, PopoverContentProps, PopoverPlacement, PopoverProps, PopoverTriggerProps, ProgressProps, ProgressSize, RadioGroupOrientation, RadioGroupProps, RadioOption, RadiusToken, SelectProps, SelectSize, SkeletonProps, SkeletonShape, SpaceToken, SpinnerProps, SpinnerSize, StackProps, SwitchProps, SwitchSize, TableBodyProps, TableCaptionProps, TableCellProps, TableFooterProps, TableHeadProps, TableHeaderProps, TableProps, TableRowProps, TabsContentProps, TabsListProps, TabsProps, TabsTriggerProps, TextInputProps, TextareaProps, TextareaResize, ThemeProviderProps, ToastAction, ToastOptions, ToastPlacement, ToastProviderProps, ToastVariant, TooltipPlacement, TooltipProps, Variant };
690
+ export { Accordion, Alert, Avatar, Badge, Box, Breadcrumbs, Button, Card, CheckIcon, Checkbox, ChevronDownIcon, Chip, CircleCheckIcon, CircleInfoIcon, CircleXIcon, CloseIcon, Combobox, Dialog, DropdownMenu, Field, Flex, Grid, Link, MinusIcon, Popover, Progress, RadioGroup, Select, Skeleton, Spinner, Stack, Switch, Table, Tabs, TextInput, Textarea, ThemeProvider, ToastProvider, Tooltip, TriangleAlertIcon, useTheme, useToast };
691
+ export type { AccordionContentProps, AccordionItemProps, AccordionProps, AccordionTriggerProps, AccordionType, AlertProps, AlertVariant, AvatarProps, AvatarSize, AvatarStatus, BadgeProps, BadgeSize, BadgeTone, BadgeVariant, BoxProps, BreadcrumbsCurrentProps, BreadcrumbsItemProps, BreadcrumbsLinkProps, BreadcrumbsListProps, BreadcrumbsProps, ButtonProps, ButtonSize, ButtonVariant, CardContentProps, CardFooterProps, CardHeaderProps, CardProps, CardTitleProps, CheckboxProps, CheckboxSize, ChipElement, ChipProps, ColorToken, ComboboxOption, ComboboxProps, ComboboxSize, DialogBodyProps, DialogContentProps, DialogDescriptionProps, DialogFooterProps, DialogHeaderProps, DialogProps, DialogTitleProps, DialogTriggerProps, DropdownMenuContentProps, DropdownMenuItemProps, DropdownMenuPlacement, DropdownMenuProps, DropdownMenuTriggerProps, FieldProps, FieldRenderProps, FlexProps, FreyTheme, GridProps, IconProps, IconSizeToken, IconStrokeToken, LinkColor, LinkProps, LinkUnderline, PopoverContentProps, PopoverPlacement, PopoverProps, PopoverTriggerProps, ProgressProps, ProgressSize, RadioGroupOrientation, RadioGroupProps, RadioOption, RadiusToken, SelectProps, SelectSize, SkeletonProps, SkeletonShape, SpaceToken, SpinnerProps, SpinnerSize, StackProps, SwitchProps, SwitchSize, TableBodyProps, TableCaptionProps, TableCellProps, TableFooterProps, TableHeadProps, TableHeaderProps, TableProps, TableRowProps, TabsContentProps, TabsListProps, TabsProps, TabsTriggerProps, TextInputProps, TextareaProps, TextareaResize, ThemeContextValue, ThemeProviderProps, ToastAction, ToastOptions, ToastPlacement, ToastProviderProps, ToastVariant, TooltipPlacement, TooltipProps, Variant };
@@ -0,0 +1,25 @@
1
+ import React from 'react';
2
+ export type ComboboxSize = 'sm' | 'md' | 'lg';
3
+ export type ComboboxOption = {
4
+ value: string;
5
+ label: string;
6
+ disabled?: boolean;
7
+ };
8
+ export type ComboboxProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type' | 'size' | 'className' | 'style' | 'value' | 'defaultValue' | 'onChange'> & {
9
+ label: string;
10
+ options: ReadonlyArray<ComboboxOption>;
11
+ hideLabel?: boolean;
12
+ error?: string;
13
+ helperText?: string;
14
+ size?: ComboboxSize;
15
+ value?: string;
16
+ defaultValue?: string;
17
+ onChange?: React.ChangeEventHandler<HTMLInputElement>;
18
+ noResultsText?: string;
19
+ className?: string;
20
+ style?: React.CSSProperties;
21
+ };
22
+ type ComboboxComponent = React.ForwardRefExoticComponent<Readonly<ComboboxProps> & React.RefAttributes<HTMLInputElement>>;
23
+ declare const Combobox: ComboboxComponent;
24
+ export default Combobox;
25
+ //# sourceMappingURL=index.d.ts.map
@@ -13,6 +13,7 @@ export type ThemeContextValue = {
13
13
  highContrast: boolean;
14
14
  };
15
15
  export declare const ThemeContext: React.Context<ThemeContextValue | null>;
16
+ export declare function useTheme(): ThemeContextValue;
16
17
  declare function ThemeProvider({ children, theme, highContrast, id, className, style }: Readonly<ThemeProviderProps>): React.JSX.Element;
17
18
  export default ThemeProvider;
18
19
  //# sourceMappingURL=index.d.ts.map
@@ -1,3 +1,8 @@
1
+ /**
2
+ * Manages a value that can be either controlled (value supplied by parent)
3
+ * or uncontrolled (managed internally with an optional callback).
4
+ */
5
+ export declare function useControllableValue<Value>(controlled: Value | undefined, defaultValue: Value, onChange?: (value: Value) => void): [Value, (next: Value) => void];
1
6
  /**
2
7
  * Manages a boolean value that can be either controlled (value supplied by parent)
3
8
  * or uncontrolled (managed internally with an optional callback).
@@ -19,6 +19,8 @@ export type { CheckboxProps, CheckboxSize } from './Checkbox';
19
19
  export { default as Checkbox } from './Checkbox';
20
20
  export type { ChipElement, ChipProps, Variant } from './Chip';
21
21
  export { default as Chip } from './Chip';
22
+ export type { ComboboxOption, ComboboxProps, ComboboxSize } from './Combobox';
23
+ export { default as Combobox } from './Combobox';
22
24
  export type { DialogBodyProps, DialogContentProps, DialogDescriptionProps, DialogFooterProps, DialogHeaderProps, DialogProps, DialogTitleProps, DialogTriggerProps } from './Dialog';
23
25
  export { default as Dialog } from './Dialog';
24
26
  export type { DropdownMenuContentProps, DropdownMenuItemProps, DropdownMenuPlacement, DropdownMenuProps, DropdownMenuTriggerProps } from './DropdownMenu';
@@ -57,8 +59,8 @@ export type { TextareaProps, TextareaResize } from './Textarea';
57
59
  export { default as Textarea } from './Textarea';
58
60
  export type { TextInputProps } from './TextInput';
59
61
  export { default as TextInput } from './TextInput';
60
- export type { FreyTheme, ThemeProviderProps } from './ThemeProvider';
61
- export { default as ThemeProvider } from './ThemeProvider';
62
+ export type { FreyTheme, ThemeContextValue, ThemeProviderProps } from './ThemeProvider';
63
+ export { default as ThemeProvider, useTheme } from './ThemeProvider';
62
64
  export type { ToastAction, ToastOptions, ToastPlacement, ToastProviderProps, ToastVariant } from './Toast';
63
65
  export { ToastProvider, useToast } from './Toast';
64
66
  export type { TooltipPlacement, TooltipProps } from './Tooltip';