@rebasepro/ui 0.3.0 → 0.5.0

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rebasepro/ui",
3
3
  "type": "module",
4
- "version": "0.3.0",
4
+ "version": "0.5.0",
5
5
  "description": "Awesome Firebase/Firestore-based headless open-source CMS",
6
6
  "funding": {
7
7
  "url": "https://github.com/sponsors/rebaseco"
@@ -67,12 +67,10 @@
67
67
  "fast-equals": "6.0.0",
68
68
  "lucide-react": "1.14.0",
69
69
  "markdown-it": "^14.1.1",
70
- "react-compiler-runtime": "1.0.0",
70
+ "tailwind-merge": "^2.6.1",
71
71
  "react-dropzone": "^14.4.1",
72
72
  "react-use-measure": "^2.1.7",
73
- "react-window": "^1.8.11",
74
- "tailwind-merge": "^2.6.1",
75
- "@rebasepro/types": "0.3.0"
73
+ "react-window": "^1.8.11"
76
74
  },
77
75
  "peerDependencies": {
78
76
  "react": ">=19.0.0",
@@ -86,9 +86,10 @@ export const BooleanSwitchWithLabel = function BooleanSwitchWithLabel({
86
86
  )}
87
87
  onClick={disabled ? undefined : (e) => {
88
88
  if (props.allowIndeterminate) {
89
- if (value === null || value === undefined) onValueChange?.(true)
90
- else if (value) onValueChange?.(false)
91
- else onValueChange?.(null as unknown as boolean); // SAFETY: null represents indeterminate state when allowIndeterminate is true
89
+ const onChange = onValueChange as ((newValue: boolean | null) => void) | undefined;
90
+ if (value === null || value === undefined) onChange?.(true)
91
+ else if (value) onChange?.(false)
92
+ else onChange?.(null);
92
93
  } else {
93
94
  onValueChange?.(!value);
94
95
  }
@@ -32,7 +32,7 @@ const ButtonInner = React.memo(React.forwardRef<
32
32
  }: ButtonProps<React.ElementType>, ref) => {
33
33
 
34
34
  const baseClasses =
35
- "typography-button h-fit rounded-md whitespace-nowrap inline-flex items-center justify-center p-1.5 px-3 focus:outline-none transition-all ease-in-out duration-150 gap-1.5 active:scale-[0.98]";
35
+ "typography-button h-fit rounded-md whitespace-nowrap inline-flex items-center justify-center p-2 px-4 focus:outline-none transition-all ease-in-out duration-150 gap-2 active:scale-[0.98]";
36
36
 
37
37
  const buttonClasses = cls({
38
38
  "w-full": fullWidth,
@@ -68,11 +68,11 @@ const ButtonInner = React.memo(React.forwardRef<
68
68
 
69
69
  const sizeClasses = cls(
70
70
  {
71
- "py-0.5 px-1.5": size === "small",
72
- "py-1.5 px-3": size === "medium",
73
- "py-2 px-4": size === "large",
74
- "py-2.5 px-5": size === "xl",
75
- "py-3 px-6": size === "2xl"
71
+ "py-1 px-2": size === "small",
72
+ "py-2 px-4": size === "medium",
73
+ "py-2.5 px-5": size === "large",
74
+ "py-3 px-6": size === "xl",
75
+ "py-4 px-10": size === "2xl"
76
76
  }
77
77
  );
78
78
 
@@ -8,7 +8,7 @@ type CardProps = {
8
8
  style?: React.CSSProperties;
9
9
  onClick?: (e?: React.MouseEvent) => void;
10
10
  className?: string;
11
- };
11
+ } & React.HTMLAttributes<HTMLDivElement>;
12
12
 
13
13
  const Card = React.forwardRef<HTMLDivElement, CardProps>(({
14
14
  children,
@@ -1,33 +1,76 @@
1
1
  "use client";
2
- import React, { ChangeEvent, useCallback, useDeferredValue, useEffect } from "react";
2
+ import React, { useCallback, useEffect, useRef, useState } from "react";
3
3
  import { TextField, TextFieldProps } from "./index";
4
4
 
5
- export function DebouncedTextField<T extends string | number>(props: TextFieldProps<T>) {
5
+ type TextFieldChangeEvent = React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>;
6
6
 
7
- const previousEventRef = React.useRef<ChangeEvent<any>>(undefined);
8
- const [internalValue, setInternalValue] = React.useState(props.value);
7
+ export function DebouncedTextField<T extends string | number>(props: TextFieldProps<T>) {
9
8
 
10
- const deferredValue = useDeferredValue(internalValue);
9
+ const [internalValue, setInternalValue] = useState<T | string>(props.value ?? "");
10
+ const lastSentValueRef = useRef<T | string>(props.value ?? "");
11
+ const timerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
11
12
 
13
+ // Sync state with props.value when it changes externally
12
14
  useEffect(() => {
13
- setInternalValue(props.value);
15
+ const externalValue = props.value ?? "";
16
+ if (externalValue !== lastSentValueRef.current) {
17
+ setInternalValue(externalValue);
18
+ lastSentValueRef.current = externalValue;
19
+ }
14
20
  }, [props.value]);
15
21
 
22
+ // Cleanup timer on unmount
16
23
  useEffect(() => {
17
- const emptyInitialValue = !props.value;
18
- if (emptyInitialValue && !deferredValue)
19
- return;
20
- if (deferredValue !== props.value && previousEventRef.current && props.onChange) {
21
- props.onChange(previousEventRef.current);
24
+ return () => {
25
+ if (timerRef.current) clearTimeout(timerRef.current);
26
+ };
27
+ }, []);
28
+
29
+ const flushChange = useCallback((value: T | string, event?: TextFieldChangeEvent) => {
30
+ if (timerRef.current) {
31
+ clearTimeout(timerRef.current);
32
+ timerRef.current = undefined;
33
+ }
34
+ if (value !== props.value && props.onChange) {
35
+ lastSentValueRef.current = value;
36
+ const e = {
37
+ ...event,
38
+ target: {
39
+ ...event?.target,
40
+ value: value,
41
+ name: props.name
42
+ }
43
+ } as TextFieldChangeEvent;
44
+ props.onChange(e);
22
45
  }
23
- }, [deferredValue, props.value]);
46
+ }, [props.value, props.onChange, props.name]);
24
47
 
25
- const internalOnChange = useCallback((event: ChangeEvent<any>) => {
26
- previousEventRef.current = event;
27
- setInternalValue(event.target.value);
28
- }, []);
48
+ const internalOnChange = useCallback((event: TextFieldChangeEvent) => {
49
+ const newValue = event.target.value;
50
+ setInternalValue(newValue);
51
+
52
+ if (timerRef.current) clearTimeout(timerRef.current);
53
+
54
+ const eventCopy = {
55
+ ...event,
56
+ target: {
57
+ ...event?.target,
58
+ name: event?.target?.name
59
+ }
60
+ };
61
+
62
+ timerRef.current = setTimeout(() => {
63
+ flushChange(newValue, eventCopy as TextFieldChangeEvent);
64
+ }, 150);
65
+ }, [flushChange]);
66
+
67
+ const internalOnBlur = useCallback((event: React.FocusEvent<HTMLInputElement>) => {
68
+ flushChange(internalValue, event as TextFieldChangeEvent);
69
+ props.onBlur?.(event);
70
+ }, [internalValue, flushChange, props.onBlur]);
29
71
 
30
72
  return <TextField {...props}
31
73
  onChange={internalOnChange}
32
- value={internalValue}/>
74
+ onBlur={internalOnBlur}
75
+ value={internalValue as T}/>;
33
76
  }
@@ -1,7 +1,7 @@
1
1
  import React from "react";
2
2
  import { cls } from "../util";
3
3
 
4
- export interface FilterChipProps {
4
+ export interface FilterChipProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "children"> {
5
5
  /**
6
6
  * The text label displayed on the chip.
7
7
  */
@@ -10,10 +10,6 @@ export interface FilterChipProps {
10
10
  * Whether the chip is currently in an active/selected state.
11
11
  */
12
12
  active?: boolean;
13
- /**
14
- * Callback when the chip is clicked.
15
- */
16
- onClick?: () => void;
17
13
  /**
18
14
  * Optional icon rendered before the label.
19
15
  */
@@ -23,10 +19,6 @@ export interface FilterChipProps {
23
19
  * @default "medium"
24
20
  */
25
21
  size?: "small" | "medium";
26
- /**
27
- * Additional class names.
28
- */
29
- className?: string;
30
22
  /**
31
23
  * Whether the chip is disabled.
32
24
  */
@@ -47,17 +39,19 @@ const sizeClasses = {
47
39
  *
48
40
  * @group Interactive components
49
41
  */
50
- export function FilterChip({
42
+ export const FilterChip = React.forwardRef<HTMLButtonElement, FilterChipProps>(function FilterChip({
51
43
  children,
52
44
  active = false,
53
45
  onClick,
54
46
  icon,
55
47
  size = "medium",
56
48
  className,
57
- disabled = false
58
- }: FilterChipProps) {
49
+ disabled = false,
50
+ ...rest
51
+ }: FilterChipProps, ref) {
59
52
  return (
60
53
  <button
54
+ ref={ref}
61
55
  type="button"
62
56
  onClick={onClick}
63
57
  disabled={disabled}
@@ -76,9 +70,10 @@ export function FilterChip({
76
70
  disabled && "opacity-50 cursor-not-allowed",
77
71
  className
78
72
  )}
73
+ {...rest}
79
74
  >
80
75
  {icon}
81
76
  {children}
82
77
  </button>
83
78
  );
84
- }
79
+ });
@@ -19,10 +19,10 @@ const buttonClasses = "hover:bg-surface-accent-200 hover:bg-opacity-75 hover:bg-
19
19
  const baseClasses = "inline-flex items-center justify-center p-2 text-sm font-medium focus:outline-none transition-colors ease-in-out duration-150";
20
20
  const colorClasses = "text-surface-accent-500 visited:text-surface-accent-500 dark:text-surface-accent-300 dark:visited:text-surface-300";
21
21
  const sizeClasses = {
22
- medium: "w-9 !h-9 min-w-9 min-h-9",
23
- small: "w-7 !h-7 min-w-7 min-h-7",
24
- smallest: "w-5 !h-5 min-w-5 min-h-5",
25
- large: "w-10 !h-10 min-w-10 min-h-10"
22
+ medium: "w-10 !h-10 min-w-10 min-h-10",
23
+ small: "w-8 !h-8 min-w-8 min-h-8",
24
+ smallest: "w-6 !h-6 min-w-6 min-h-6",
25
+ large: "w-12 !h-12 min-w-12 min-h-12"
26
26
  }
27
27
  const shapeClasses = {
28
28
  circular: "rounded-full",
@@ -107,7 +107,7 @@ export function Tabs({
107
107
  msOverflowStyle: "none" }}
108
108
  >
109
109
  <TabsPrimitive.List className={cls(
110
- variant === "standard" && "inline-flex h-10 items-center justify-start rounded-md bg-surface-50 p-1 text-surface-600 dark:bg-surface-900 dark:text-surface-400 gap-2 border",
110
+ variant === "standard" && "inline-flex h-9 items-center justify-start rounded-md bg-surface-50 p-1 text-surface-600 dark:bg-surface-900 dark:text-surface-400 gap-2 border",
111
111
  variant === "standard" && defaultBorderMixin,
112
112
  variant === "boxy" && "flex items-center h-full",
113
113
  variant === "pill" && "flex items-center gap-0.5",
@@ -161,7 +161,7 @@ export function Tab({
161
161
  "inline-flex items-center justify-center whitespace-nowrap text-sm font-medium ring-offset-white transition-all",
162
162
  "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-surface-400 focus-visible:ring-offset-2",
163
163
  "disabled:pointer-events-none disabled:opacity-50",
164
- variant === "standard" && "rounded-sm px-3 py-1.5 data-[state=active]:bg-white data-[state=active]:text-surface-900 data-[state=active]:shadow-sm dark:data-[state=active]:bg-surface-900 dark:data-[state=active]:text-surface-50",
164
+ variant === "standard" && "rounded-sm px-3 py-1 data-[state=active]:bg-white data-[state=active]:text-surface-900 data-[state=active]:shadow-sm dark:data-[state=active]:bg-surface-900 dark:data-[state=active]:text-surface-50",
165
165
  variant === "boxy" && cls(
166
166
  "flex-shrink-0 flex items-center gap-1.5 px-3.5 h-9 border-r border-surface-200 dark:border-surface-800 cursor-pointer text-[12px] font-medium transition-colors group relative box-border overflow-hidden",
167
167
  "border-b-2 border-b-transparent",
@@ -212,9 +212,10 @@ export const VirtualTableHeaderRow = ({
212
212
  className={cls(defaultBorderMixin, "z-20 sticky min-w-full flex w-fit flex-row top-0 left-0 border-b bg-surface-50 dark:bg-surface-900")}
213
213
  style={{ height: headerHeight }}>
214
214
  {columns.map((column, columnIndex) => {
215
+ const columnFilter = column && filter && filter[column.key] ? filter[column.key] : undefined;
215
216
  const filterForThisProperty: [VirtualTableWhereFilterOp, unknown] | undefined =
216
- column && filter && filter[column.key]
217
- ? filter[column.key]
217
+ columnFilter
218
+ ? (Array.isArray(columnFilter[0]) ? (columnFilter as [VirtualTableWhereFilterOp, unknown][])[0] : (columnFilter as [VirtualTableWhereFilterOp, unknown]))
218
219
  : undefined;
219
220
 
220
221
  const isDraggable = !column.frozen && !!onColumnsOrderChange;
@@ -274,6 +274,25 @@ export type OnVirtualTableColumnResizeParams = {
274
274
  column: VirtualTableColumn
275
275
  };
276
276
 
277
+ /**
278
+ * @see Table
279
+ * @group Components
280
+ */
281
+ export type WhereFilterOp =
282
+ | "<"
283
+ | "<="
284
+ | "=="
285
+ | "!="
286
+ | ">="
287
+ | ">"
288
+ | "array-contains"
289
+ | "in"
290
+ | "not-in"
291
+ | "array-contains-any";
292
+
293
+ export type FilterValues<Key extends string> =
294
+ Partial<Record<Key, [WhereFilterOp, unknown] | [WhereFilterOp, unknown][]>>;
295
+
277
296
  /**
278
297
  * @see Table
279
298
  * @group Components
@@ -284,7 +303,7 @@ export type VirtualTableSort = "asc" | "desc" | undefined;
284
303
  * @see Table
285
304
  * @group Components
286
305
  */
287
- export type VirtualTableFilterValues<Key extends string> = Partial<Record<Key, [VirtualTableWhereFilterOp, unknown]>>;
306
+ export type VirtualTableFilterValues<Key extends string> = FilterValues<Key>;
288
307
 
289
308
  /**
290
309
  * Filter conditions in a `Query.where()` clause are specified using the
@@ -292,14 +311,4 @@ export type VirtualTableFilterValues<Key extends string> = Partial<Record<Key, [
292
311
  * @see Table
293
312
  * @group Models
294
313
  */
295
- export type VirtualTableWhereFilterOp =
296
- | "<"
297
- | "<="
298
- | "=="
299
- | "!="
300
- | ">="
301
- | ">"
302
- | "array-contains"
303
- | "in"
304
- | "not-in"
305
- | "array-contains-any";
314
+ export type VirtualTableWhereFilterOp = WhereFilterOp;
package/src/index.css CHANGED
@@ -117,7 +117,7 @@
117
117
  }
118
118
 
119
119
  .typography-button {
120
- @apply text-[13px] font-semibold;
120
+ @apply text-sm font-semibold;
121
121
  }
122
122
 
123
123
  :focus-visible {