@firecms/ui 3.3.0 → 3.4.0-canary.f6a889a

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 (49) hide show
  1. package/dist/components/Card.d.ts +2 -3
  2. package/dist/components/Checkbox.d.ts +2 -1
  3. package/dist/components/Chip.d.ts +5 -1
  4. package/dist/components/Dialog.d.ts +2 -1
  5. package/dist/components/FilterChip.d.ts +34 -0
  6. package/dist/components/IconButton.d.ts +1 -0
  7. package/dist/components/Popover.d.ts +3 -1
  8. package/dist/components/Select.d.ts +1 -1
  9. package/dist/components/TextField.d.ts +2 -2
  10. package/dist/components/index.d.ts +1 -0
  11. package/dist/hooks/index.d.ts +1 -0
  12. package/dist/hooks/useDebounceCallback.d.ts +13 -0
  13. package/dist/index.css +43 -32
  14. package/dist/index.es.js +982 -530
  15. package/dist/index.es.js.map +1 -1
  16. package/dist/index.umd.js +981 -529
  17. package/dist/index.umd.js.map +1 -1
  18. package/dist/styles.d.ts +1 -1
  19. package/package.json +6 -6
  20. package/src/components/Alert.tsx +7 -7
  21. package/src/components/Avatar.tsx +1 -1
  22. package/src/components/BooleanSwitch.tsx +6 -3
  23. package/src/components/Button.tsx +17 -13
  24. package/src/components/Card.tsx +3 -1
  25. package/src/components/CenteredView.tsx +1 -1
  26. package/src/components/Checkbox.tsx +10 -4
  27. package/src/components/Chip.tsx +70 -11
  28. package/src/components/Collapse.tsx +2 -0
  29. package/src/components/Dialog.tsx +8 -5
  30. package/src/components/ExpandablePanel.tsx +3 -2
  31. package/src/components/FilterChip.tsx +79 -0
  32. package/src/components/IconButton.tsx +11 -6
  33. package/src/components/InfoLabel.tsx +1 -1
  34. package/src/components/InputLabel.tsx +2 -2
  35. package/src/components/Label.tsx +1 -1
  36. package/src/components/LoadingButton.tsx +1 -1
  37. package/src/components/Menu.tsx +2 -0
  38. package/src/components/Popover.tsx +9 -3
  39. package/src/components/RadioGroup.tsx +1 -1
  40. package/src/components/Select.tsx +8 -6
  41. package/src/components/Separator.tsx +2 -2
  42. package/src/components/Skeleton.tsx +25 -8
  43. package/src/components/TextField.tsx +27 -13
  44. package/src/components/ToggleButtonGroup.tsx +4 -2
  45. package/src/components/index.tsx +1 -0
  46. package/src/hooks/index.ts +1 -0
  47. package/src/hooks/useDebounceCallback.tsx +47 -0
  48. package/src/index.css +43 -32
  49. package/src/styles.ts +1 -1
@@ -10,7 +10,7 @@ export type InputLabelProps = {
10
10
  } & React.LabelHTMLAttributes<HTMLLabelElement>;
11
11
 
12
12
  const defaultClasses = {
13
- root: "origin-left transition-transform block whitespace-nowrap overflow-hidden text-overflow-ellipsis max-w-full",
13
+ root: "origin-left transition-transform block whitespace-nowrap overflow-hidden text-ellipsis max-w-full",
14
14
  shrink: "transform translate-y-[2px] scale-75 translate-x-[12px]",
15
15
  expanded: "translate-x-[16px] top-0 transform translate-y-[16px] scale-100"
16
16
  };
@@ -30,7 +30,7 @@ export const InputLabel = React.forwardRef<HTMLLabelElement, InputLabelProps>(fu
30
30
 
31
31
  return (
32
32
  <label
33
- className={cls("text-sm font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
33
+ className={cls("text-sm font-medium peer-disabled:cursor-not-allowed",
34
34
  defaultBorderMixin, computedClassName)}
35
35
  data-shrink={shrink}
36
36
  ref={ref}
@@ -20,7 +20,7 @@ const Label = React.forwardRef<
20
20
  <LabelPrimitive.Root
21
21
  ref={ref}
22
22
  onClick={onClick}
23
- className={cls("text-sm font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
23
+ className={cls("text-sm font-medium peer-disabled:cursor-not-allowed",
24
24
  border && "border border-surface-300 dark:border-surface-700 rounded-md px-3 py-1.5",
25
25
  onClick && "hover:cursor-pointer hover:bg-surface-200 dark:hover:bg-surface-800",
26
26
  defaultBorderMixin, className)}
@@ -19,7 +19,7 @@ export function LoadingButton<P extends React.ElementType = "button">({
19
19
  <Button
20
20
  disabled={loading || disabled}
21
21
  onClick={onClick}
22
- component={props.component as any}
22
+ component={props.component as React.ElementType}
23
23
  {...props}
24
24
  >
25
25
  {loading && (
@@ -102,3 +102,5 @@ export const MenuItem = React.memo(({
102
102
  </DropdownMenu.Item>
103
103
  );
104
104
  });
105
+
106
+ MenuItem.displayName = "MenuItem";
@@ -27,6 +27,8 @@ export interface PopoverProps {
27
27
  modal?: boolean;
28
28
  className?: string;
29
29
  portalContainer?: HTMLElement | null;
30
+ onMouseEnter?: React.MouseEventHandler<HTMLDivElement>;
31
+ onMouseLeave?: React.MouseEventHandler<HTMLDivElement>;
30
32
  }
31
33
 
32
34
  export function Popover({
@@ -45,7 +47,9 @@ export function Popover({
45
47
  enabled = true,
46
48
  modal = false,
47
49
  portalContainer,
48
- className
50
+ className,
51
+ onMouseEnter,
52
+ onMouseLeave
49
53
  }: PopoverProps) {
50
54
 
51
55
  useInjectStyles("Popover", popoverStyles);
@@ -78,10 +82,12 @@ export function Popover({
78
82
  arrowPadding={arrowPadding}
79
83
  sticky={sticky}
80
84
  hideWhenDetached={hideWhenDetached}
81
- avoidCollisions={avoidCollisions}>
85
+ avoidCollisions={avoidCollisions}
86
+ onMouseEnter={onMouseEnter}
87
+ onMouseLeave={onMouseLeave}>
82
88
 
83
89
  {children}
84
- <PopoverPrimitive.Arrow className="fill-white dark:fill-surface-accent-950"/>
90
+ <PopoverPrimitive.Arrow className="fill-white dark:fill-surface-800"/>
85
91
  </PopoverPrimitive.Content>
86
92
  </PopoverPrimitive.Portal>
87
93
  </PopoverPrimitive.Root>;
@@ -58,7 +58,7 @@ const RadioGroupItem = React.forwardRef<
58
58
  <RadioGroupPrimitive.Item
59
59
  ref={ref}
60
60
  className={cls(
61
- "aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
61
+ "aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
62
62
  className
63
63
  )}
64
64
  {...props}
@@ -36,7 +36,7 @@ export type SelectProps<T extends SelectValue = string> = {
36
36
  error?: boolean,
37
37
  position?: "item-aligned" | "popper",
38
38
  endAdornment?: React.ReactNode,
39
- inputRef?: React.RefObject<HTMLButtonElement | null>,
39
+ inputRef?: React.Ref<HTMLButtonElement>,
40
40
  padding?: boolean,
41
41
  invisible?: boolean,
42
42
  children?: React.ReactNode;
@@ -136,7 +136,7 @@ export const Select = forwardRef<HTMLDivElement, SelectProps>(({
136
136
  {...props}>
137
137
  {typeof label === "string" ? <SelectInputLabel error={error}>{label}</SelectInputLabel> : label}
138
138
  <div className={cls(
139
- "select-none rounded-md text-sm",
139
+ "select-none rounded-lg text-sm",
140
140
  invisible ? fieldBackgroundInvisibleMixin : fieldBackgroundMixin,
141
141
  disabled ? fieldBackgroundDisabledMixin : fieldBackgroundHoverMixin,
142
142
  "relative flex items-center",
@@ -155,6 +155,8 @@ export const Select = forwardRef<HTMLDivElement, SelectProps>(({
155
155
  id={id}
156
156
  asChild={false}
157
157
  type="button"
158
+ aria-label={typeof label === "string" ? label : (typeof renderValue === "string" ? renderValue : "Select an option")}
159
+ aria-invalid={error || undefined}
158
160
  className={cls(
159
161
  "h-full",
160
162
  padding ? {
@@ -164,7 +166,7 @@ export const Select = forwardRef<HTMLDivElement, SelectProps>(({
164
166
  } : "",
165
167
  "outline-hidden focus:outline-hidden",
166
168
  "outline-none focus:outline-none",
167
- "select-none rounded-md text-sm",
169
+ "select-none rounded-lg text-sm",
168
170
  error ? "text-red-500 dark:text-red-600" : "focus:text-text-primary dark:focus:text-text-primary-dark",
169
171
  error ? "border border-red-500 dark:border-red-600" : "",
170
172
  disabled ? "text-surface-accent-600 dark:text-surface-accent-400" : "text-surface-accent-800 dark:text-white",
@@ -232,7 +234,7 @@ export const Select = forwardRef<HTMLDivElement, SelectProps>(({
232
234
  {/* Pass the calculated finalContainer */}
233
235
  <SelectPrimitive.Portal container={finalContainer}>
234
236
  <SelectPrimitive.Content position={position}
235
- className={cls(focusedDisabled, "z-50 relative overflow-hidden border bg-white dark:bg-surface-900 p-2 rounded-lg", defaultBorderMixin)}>
237
+ className={cls(focusedDisabled, "z-50 relative overflow-hidden border bg-white dark:bg-surface-800 p-2 rounded-lg shadow-lg", defaultBorderMixin)}>
236
238
  <SelectPrimitive.Viewport className={cls("p-1", viewportClassName)}
237
239
  style={{ maxHeight: "var(--radix-select-content-available-height)" }}>
238
240
  {children}
@@ -270,8 +272,8 @@ export const SelectItem = React.memo(function SelectItem<T extends SelectValue =
270
272
  "w-full",
271
273
  "relative flex items-center p-2 rounded-md text-sm text-surface-accent-700 dark:text-surface-accent-300",
272
274
  "focus:z-10",
273
- "data-[state=checked]:bg-surface-accent-100 data-[state=checked]:dark:bg-surface-accent-800 focus:bg-surface-accent-100 dark:focus:bg-surface-950",
274
- "data-[state=checked]:focus:bg-surface-accent-200 data-[state=checked]:dark:focus:bg-surface-950",
275
+ "data-[state=checked]:bg-surface-accent-100 data-[state=checked]:dark:bg-surface-accent-800 focus:bg-surface-accent-100 dark:focus:bg-surface-900",
276
+ "data-[state=checked]:focus:bg-surface-accent-200 data-[state=checked]:dark:focus:bg-surface-900",
275
277
  disabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer",
276
278
  "[&>*]:w-full",
277
279
  "overflow-visible",
@@ -15,12 +15,12 @@ export function Separator({
15
15
  <SeparatorPrimitive.Root
16
16
  decorative={decorative}
17
17
  orientation="horizontal"
18
- className={cls("dark:bg-opacity-80 dark:bg-surface-800 dark:bg-surface-800/80 bg-surface-100 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px my-4", className)}/>
18
+ className={cls("dark:bg-surface-700 bg-surface-200 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px my-4", className)}/>
19
19
  );
20
20
  else
21
21
  return (
22
22
  <SeparatorPrimitive.Root
23
- className={cls("dark:bg-opacity-80 dark:bg-surface-800 dark:bg-surface-800/80 bg-surface-100 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px mx-4", className)}
23
+ className={cls("dark:bg-surface-700 bg-surface-200 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px mx-4", className)}
24
24
  decorative={decorative}
25
25
  orientation="vertical"
26
26
  />
@@ -1,5 +1,6 @@
1
1
  import React from "react";
2
2
  import { cls } from "../util";
3
+ import { useInjectStyles } from "../hooks";
3
4
 
4
5
  export type SkeletonProps = {
5
6
  width?: number;
@@ -7,24 +8,40 @@ export type SkeletonProps = {
7
8
  className?: string;
8
9
  }
9
10
 
11
+ const styles = `
12
+ @keyframes shimmer {
13
+ 0% {
14
+ transform: translateX(-150%);
15
+ }
16
+ 100% {
17
+ transform: translateX(150%);
18
+ }
19
+ }
20
+ `;
21
+
10
22
  export function Skeleton({
11
23
  width,
12
24
  height,
13
25
  className
14
26
  }: SkeletonProps) {
27
+
28
+ useInjectStyles("Skeleton", styles);
29
+
15
30
  return <span
16
31
  style={{
17
- width: width !== undefined ? `${width}px` : undefined,
18
- height: height !== undefined ? `${height}px` : undefined
32
+ width: width ? `${width}px` : "100%",
33
+ height: height ? `${height}px` : "12px"
19
34
  }}
20
35
  className={
21
36
  cls(
22
- "block",
23
- "bg-surface-accent-200 dark:bg-surface-accent-800 rounded-md",
24
- "animate-pulse",
37
+ "block relative overflow-hidden",
38
+ "bg-surface-accent-50 dark:bg-surface-accent-800 rounded-md",
25
39
  "max-w-full max-h-full",
26
- width === undefined ? "w-full" : "",
27
- height === undefined ? "h-3" : "",
28
40
  className)
29
- }/>;
41
+ }>
42
+ <span
43
+ className="absolute inset-0 bg-gradient-to-r from-transparent via-white/50 dark:via-white/8 to-transparent"
44
+ style={{ animation: "shimmer 1.8s ease-in-out infinite" }}
45
+ />
46
+ </span>;
30
47
  }
@@ -1,12 +1,12 @@
1
1
  "use client";
2
- import React, { ForwardedRef, forwardRef, useEffect, useRef } from "react";
2
+ import React, { ForwardedRef, forwardRef, useEffect, useId, useRef } from "react";
3
3
 
4
4
  import {
5
5
  fieldBackgroundDisabledMixin,
6
6
  fieldBackgroundHoverMixin,
7
7
  fieldBackgroundInvisibleMixin,
8
8
  fieldBackgroundMixin,
9
- focusedInvisibleMixin,
9
+ focusedInvisibleMixin
10
10
  } from "../styles";
11
11
  import { InputLabel } from "./InputLabel";
12
12
  import { cls } from "../util";
@@ -53,7 +53,7 @@ export type TextFieldProps<T extends string | number> = {
53
53
  * @default 1
54
54
  */
55
55
  minRows?: number | string;
56
- } & Omit<React.InputHTMLAttributes<HTMLInputElement>, "size">;
56
+ } & Omit<React.InputHTMLAttributes<HTMLInputElement>, "size" | "value">;
57
57
 
58
58
  export const TextField = forwardRef<HTMLDivElement, TextFieldProps<string | number>>(
59
59
  <T extends string | number>(
@@ -82,14 +82,18 @@ export const TextField = forwardRef<HTMLDivElement, TextFieldProps<string | numb
82
82
  ref: ForwardedRef<HTMLDivElement>
83
83
  ) => {
84
84
 
85
- const inputRef = inputRefProp ?? useRef(null);
85
+ const fallbackRef = useRef(null);
86
+ const inputRef = inputRefProp ?? fallbackRef;
87
+ const autoId = useId();
88
+ const inputId = inputProps.id ?? autoId;
89
+ const labelId = `${inputId}-label`;
86
90
 
87
91
  const [focused, setFocused] = React.useState(false);
88
92
  const hasValue = value !== undefined && value !== null && value !== "";
89
93
 
90
94
  useEffect(() => {
91
- // @ts-ignore
92
- if (inputRef.current && document.activeElement === inputRef.current) {
95
+ const element = inputRef && "current" in inputRef ? inputRef.current : null;
96
+ if (element && document.activeElement === element) {
93
97
  setFocused(true);
94
98
  }
95
99
  }, []);
@@ -111,8 +115,12 @@ export const TextField = forwardRef<HTMLDivElement, TextFieldProps<string | numb
111
115
 
112
116
  const input = multiline ? (
113
117
  <textarea
114
- {...(inputProps as any)}
118
+ {...(inputProps as React.TextareaHTMLAttributes<HTMLTextAreaElement>)}
115
119
  ref={inputRef}
120
+ id={inputId}
121
+ aria-labelledby={label ? labelId : undefined}
122
+ aria-invalid={error || undefined}
123
+ aria-disabled={disabled || undefined}
116
124
  placeholder={focused || hasValue || !label ? placeholder : undefined}
117
125
  autoFocus={autoFocus}
118
126
  rows={typeof minRows === "string" ? parseInt(minRows) : (minRows ?? 3)}
@@ -123,7 +131,7 @@ export const TextField = forwardRef<HTMLDivElement, TextFieldProps<string | numb
123
131
  style={inputStyle}
124
132
  className={cls(
125
133
  invisible ? focusedInvisibleMixin : "",
126
- "rounded-md resize-none w-full outline-none text-base bg-transparent min-h-[64px] px-3",
134
+ "rounded-lg resize-none w-full outline-none text-base bg-transparent min-h-[64px] px-3",
127
135
  label ? "pt-8 pb-2" : "py-2",
128
136
  disabled && "outline-none opacity-50 text-surface-accent-600 dark:text-surface-accent-500",
129
137
  inputClassName
@@ -133,11 +141,15 @@ export const TextField = forwardRef<HTMLDivElement, TextFieldProps<string | numb
133
141
  <input
134
142
  {...inputProps}
135
143
  ref={inputRef}
144
+ id={inputId}
145
+ aria-labelledby={label ? labelId : undefined}
146
+ aria-invalid={error || undefined}
147
+ aria-disabled={disabled || undefined}
136
148
  disabled={disabled}
137
149
  style={inputStyle}
138
150
  className={cls(
139
151
  "w-full outline-none bg-transparent leading-normal px-3",
140
- "rounded-md",
152
+ "rounded-lg",
141
153
  "focused:text-text-primary focused:dark:text-text-primary-dark",
142
154
  invisible ? focusedInvisibleMixin : "",
143
155
  disabled ? fieldBackgroundDisabledMixin : fieldBackgroundHoverMixin,
@@ -145,7 +157,7 @@ export const TextField = forwardRef<HTMLDivElement, TextFieldProps<string | numb
145
157
  "min-h-[28px]": size === "smallest",
146
158
  "min-h-[32px]": size === "small",
147
159
  "min-h-[44px]": size === "medium",
148
- "min-h-[64px]": size === "large",
160
+ "min-h-[64px]": size === "large"
149
161
  },
150
162
  label
151
163
  ? size === "large"
@@ -175,7 +187,7 @@ export const TextField = forwardRef<HTMLDivElement, TextFieldProps<string | numb
175
187
  <div
176
188
  ref={ref}
177
189
  className={cls(
178
- "rounded-md relative max-w-full",
190
+ "rounded-lg relative max-w-full",
179
191
  invisible ? fieldBackgroundInvisibleMixin : fieldBackgroundMixin,
180
192
  disabled ? fieldBackgroundDisabledMixin : fieldBackgroundHoverMixin,
181
193
  error ? "border border-red-500 dark:border-red-600" : "",
@@ -183,7 +195,7 @@ export const TextField = forwardRef<HTMLDivElement, TextFieldProps<string | numb
183
195
  "min-h-[28px]": size === "smallest",
184
196
  "min-h-[32px]": size === "small",
185
197
  "min-h-[44px]": size === "medium",
186
- "min-h-[64px]": size === "large",
198
+ "min-h-[64px]": size === "large"
187
199
  },
188
200
  className
189
201
  )}
@@ -191,6 +203,8 @@ export const TextField = forwardRef<HTMLDivElement, TextFieldProps<string | numb
191
203
  >
192
204
  {label && (
193
205
  <InputLabel
206
+ id={labelId}
207
+ htmlFor={inputId}
194
208
  className={cls(
195
209
  "pointer-events-none absolute",
196
210
  size === "large" ? "top-1" : "top-[-1px]",
@@ -216,7 +230,7 @@ export const TextField = forwardRef<HTMLDivElement, TextFieldProps<string | numb
216
230
  {
217
231
  "mr-4": size === "large",
218
232
  "mr-3": size === "medium",
219
- "mr-2": size === "small" || size === "smallest",
233
+ "mr-2": size === "small" || size === "smallest"
220
234
  }
221
235
  )}
222
236
  >
@@ -38,11 +38,13 @@ export function ToggleButtonGroup<T extends string = string>({
38
38
  className
39
39
  }: ToggleButtonGroupProps<T>) {
40
40
  return (
41
- <div className={cls("inline-flex flex-row bg-surface-100 dark:bg-surface-800 rounded-lg p-1 gap-1", className)}>
41
+ <div role="group" aria-label="Toggle options" className={cls("inline-flex flex-row bg-surface-100 dark:bg-surface-900 rounded-lg p-1 gap-1", className)}>
42
42
  {options.map((option) => (
43
43
  <button
44
44
  key={option.value}
45
45
  type="button"
46
+ aria-pressed={value === option.value}
47
+ aria-disabled={option.disabled || undefined}
46
48
  onClick={(e) => {
47
49
  e.stopPropagation();
48
50
  if (!option.disabled) {
@@ -53,7 +55,7 @@ export function ToggleButtonGroup<T extends string = string>({
53
55
  className={cls(
54
56
  "flex flex-row items-center justify-center gap-2 py-3 px-4 rounded-md transition-colors",
55
57
  value === option.value
56
- ? "bg-white dark:bg-surface-950 text-primary dark:text-primary-300"
58
+ ? "bg-white dark:bg-surface-900 text-primary dark:text-primary-300 shadow-sm"
57
59
  : "text-surface-500 dark:text-surface-400 hover:bg-surface-100 dark:hover:bg-surface-700",
58
60
  option.disabled && "opacity-50 cursor-not-allowed"
59
61
  )}
@@ -11,6 +11,7 @@ export * from "./Collapse";
11
11
  export * from "./CircularProgress";
12
12
  export * from "./Checkbox";
13
13
  export * from "./Chip";
14
+ export * from "./FilterChip";
14
15
  export * from "./ColorPicker";
15
16
  export * from "./DateTimeField";
16
17
  export * from "./Dialog";
@@ -1,5 +1,6 @@
1
1
  export * from "./useInjectStyles";
2
2
  export * from "./useOutsideAlerter";
3
3
  export * from "./useDebounceValue";
4
+ export * from "./useDebounceCallback";
4
5
  export * from "./useIconStyles";
5
6
  export * from "./PortalContainerContext";
@@ -0,0 +1,47 @@
1
+ import { useCallback, useEffect, useRef } from "react";
2
+
3
+ /**
4
+ * Returns a debounced version of the provided callback. The returned function
5
+ * keeps the same signature as the input callback; each invocation resets the
6
+ * timer, so the wrapped callback only runs once calls stop for `delay` ms.
7
+ *
8
+ * Unlike {@link useDebounceValue}, which debounces a *value*, this debounces a
9
+ * *function call* — useful for search inputs, autosave, resize handlers, etc.
10
+ *
11
+ * @param callback the function to debounce. May be undefined.
12
+ * @param delay debounce delay in milliseconds. Defaults to 200ms.
13
+ * @group Hooks
14
+ */
15
+ export function useDebounceCallback<T extends (...args: any[]) => unknown>(
16
+ callback?: T,
17
+ delay?: number
18
+ ): T {
19
+ const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
20
+
21
+ // Keep the latest callback without recreating the debounced function.
22
+ const callbackRef = useRef(callback);
23
+ useEffect(() => {
24
+ callbackRef.current = callback;
25
+ }, [callback]);
26
+
27
+ // Clear any pending timer on unmount.
28
+ useEffect(() => {
29
+ return () => {
30
+ if (timeoutRef.current !== null) {
31
+ clearTimeout(timeoutRef.current);
32
+ }
33
+ };
34
+ }, []);
35
+
36
+ const debouncedCallback = useCallback((...args: Parameters<T>) => {
37
+ if (timeoutRef.current !== null) {
38
+ clearTimeout(timeoutRef.current);
39
+ }
40
+
41
+ timeoutRef.current = setTimeout(() => {
42
+ callbackRef.current?.(...args);
43
+ }, delay ?? 200);
44
+ }, [delay]);
45
+
46
+ return debouncedCallback as T;
47
+ }
package/src/index.css CHANGED
@@ -19,26 +19,26 @@
19
19
  --color-field-disabled: rgb(224 224 226);
20
20
  --color-field-disabled-dark: rgb(35 35 37);
21
21
 
22
- /* Text Colors */
23
- --color-text-primary: rgba(0, 0, 0, 0.87);
24
- --color-text-secondary: rgba(0, 0, 0, 0.52);
25
- --color-text-disabled: rgba(0, 0, 0, 0.38);
22
+ /* Text Colors — using opaque values so icon strokes render solidly */
23
+ --color-text-primary: #212121;
24
+ --color-text-secondary: #757575;
25
+ --color-text-disabled: #9e9e9e;
26
26
  --color-text-primary-dark: #ffffff;
27
- --color-text-secondary-dark: rgba(255, 255, 255, 0.6);
28
- --color-text-disabled-dark: rgba(255, 255, 255, 0.48);
27
+ --color-text-secondary-dark: #a0a0a9;
28
+ --color-text-disabled-dark: #757580;
29
29
 
30
30
  /* Surface Colors */
31
- --color-surface-50: #f8f8fc;
32
- --color-surface-100: #e7e7eb;
33
- --color-surface-200: #cfcfd6;
34
- --color-surface-300: #b7b7bf;
35
- --color-surface-400: #a0a0a9;
36
- --color-surface-500: #87878f;
37
- --color-surface-600: #6b6b74;
38
- --color-surface-700: #454552;
39
- --color-surface-800: #292934;
40
- --color-surface-900: #18181c;
41
- --color-surface-950: #101013;
31
+ --color-surface-50: #fafafa;
32
+ --color-surface-100: #f5f5f5;
33
+ --color-surface-200: #e5e5e5;
34
+ --color-surface-300: #d4d4d4;
35
+ --color-surface-400: #a3a3a3;
36
+ --color-surface-500: #737373;
37
+ --color-surface-600: #404040;
38
+ --color-surface-700: #2e2e33;
39
+ --color-surface-800: #1f1f23;
40
+ --color-surface-900: #131316;
41
+ --color-surface-950: #0d0d0f;
42
42
 
43
43
  /* Surface Accent Colors */
44
44
  --color-surface-accent-50: #f8fafc;
@@ -50,8 +50,18 @@
50
50
  --color-surface-accent-600: #475569;
51
51
  --color-surface-accent-700: #334155;
52
52
  --color-surface-accent-800: #1e293b;
53
- --color-surface-accent-900: #0f172a;
54
- --color-surface-accent-950: #020617;
53
+ --color-surface-accent-900: #172033;
54
+ --color-surface-accent-950: #0f172a;
55
+ }
56
+
57
+ /* Native UI (scrollbars, form controls) must follow the active theme,
58
+ otherwise Chrome renders light scrollbars in dark mode */
59
+ html {
60
+ color-scheme: light;
61
+ }
62
+
63
+ html.dark {
64
+ color-scheme: dark;
55
65
  }
56
66
 
57
67
  /* Chrome, Safari and Opera */
@@ -65,50 +75,51 @@
65
75
  }
66
76
 
67
77
  .typography-h1 {
68
- @apply text-6xl font-headers font-light tracking-tight;
78
+ @apply text-4xl font-headers font-semibold tracking-tight;
69
79
  }
70
80
 
71
81
  .typography-h2 {
72
- @apply text-5xl font-headers font-light tracking-tight;
82
+ @apply text-3xl font-headers font-semibold tracking-tight;
73
83
  }
74
84
 
75
85
  .typography-h3 {
76
- @apply text-4xl font-headers font-normal tracking-tight;
86
+ @apply text-2xl font-headers font-semibold tracking-tight;
77
87
  }
78
88
 
79
89
  .typography-h4 {
80
- @apply text-3xl font-headers font-normal tracking-tight;
90
+ @apply text-xl font-headers font-semibold tracking-[-0.01em];
81
91
  }
82
92
 
83
93
  .typography-h5 {
84
- @apply text-2xl font-headers font-normal;
94
+ @apply text-lg font-headers font-semibold tracking-[-0.01em];
85
95
  }
86
96
 
87
97
  .typography-h6 {
88
- @apply text-xl font-headers font-medium;
98
+ @apply text-base font-headers font-semibold tracking-[-0.01em];
89
99
  }
90
100
 
91
101
  .typography-subtitle1 {
92
- @apply text-lg font-headers font-medium;
102
+ @apply text-sm font-headers font-semibold tracking-[-0.01em];
93
103
  }
94
104
 
95
105
  .typography-subtitle2 {
96
- @apply text-base font-headers font-medium;
106
+ @apply text-sm font-headers font-medium;
97
107
  }
98
108
 
99
109
  .typography-body1 {
110
+ @apply text-sm;
100
111
  }
101
112
 
102
113
  .typography-body2 {
103
- @apply text-sm;
114
+ @apply text-xs;
104
115
  }
105
116
 
106
117
  .typography-caption {
107
- @apply text-xs;
118
+ @apply text-[11px] leading-[1.4];
108
119
  }
109
120
 
110
121
  .typography-label {
111
- @apply text-sm font-medium;
122
+ @apply text-xs font-medium tracking-wide;
112
123
  }
113
124
 
114
125
  .typography-inherit {
@@ -116,10 +127,10 @@
116
127
  }
117
128
 
118
129
  .typography-button {
119
- @apply text-sm font-semibold uppercase;
130
+ @apply text-sm font-semibold tracking-wide;
120
131
  }
121
132
 
122
133
  :focus-visible {
123
- @apply outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-transparent
134
+ @apply outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-0 focus-visible:ring-offset-transparent
124
135
  }
125
136
 
package/src/styles.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export const focusedDisabled = "focus-visible:ring-0 focus-visible:ring-offset-0";
2
2
  export const focusedInvisibleMixin = "focus:bg-opacity-70 focus:bg-surface-accent-100 focus:dark:bg-surface-800 focus:dark:bg-opacity-60 focus:bg-surface-accent-100/70 dark:focus:bg-surface-800/60";
3
3
  export const focusedClasses = "z-30 outline-hidden outline-none ring-2 ring-primary ring-opacity-75 ring-primary/75 ring-offset-2 ring-offset-transparent ";
4
- export const fieldBackgroundMixin = "bg-opacity-50 bg-surface-accent-200 bg-surface-accent-200/50 dark:bg-surface-800 dark:bg-opacity-60 dark:bg-surface-800/60";
4
+ export const fieldBackgroundMixin = "bg-opacity-50 bg-surface-accent-200 bg-surface-accent-200/50 dark:bg-surface-800 dark:bg-opacity-90 dark:bg-surface-800/90";
5
5
  export const fieldBackgroundInvisibleMixin = "bg-opacity-0 bg-surface-accent-100 dark:bg-surface-800 dark:bg-opacity-0 bg-surface-accent-200/0 dark:bg-surface-800/0";
6
6
  export const fieldBackgroundDisabledMixin = "dark:bg-surface-800 bg-opacity-50 dark:bg-opacity-90 bg-surface-accent-200/50 dark:bg-surface-800/90";
7
7
  export const fieldBackgroundHoverMixin = "hover:bg-opacity-70 dark:hover:bg-surface-700 dark:hover:bg-opacity-40 hover:bg-surface-accent-200/70 hover:dark:bg-surface-700/40";