@rebasepro/ui 0.7.0 → 0.9.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.7.0",
4
+ "version": "0.9.0",
5
5
  "description": "Awesome Firebase/Firestore-based headless open-source CMS",
6
6
  "funding": {
7
7
  "url": "https://github.com/sponsors/rebaseco"
@@ -288,7 +288,13 @@ export type WhereFilterOp =
288
288
  | "array-contains"
289
289
  | "in"
290
290
  | "not-in"
291
- | "array-contains-any";
291
+ | "array-contains-any"
292
+ | "like"
293
+ | "ilike"
294
+ | "not-like"
295
+ | "not-ilike"
296
+ | "is-null"
297
+ | "is-not-null";
292
298
 
293
299
  export type FilterValues<Key extends string> =
294
300
  Partial<Record<Key, [WhereFilterOp, unknown] | [WhereFilterOp, unknown][]>>;
@@ -0,0 +1,32 @@
1
+ import React from "react";
2
+ import { cls } from "../../../util";
3
+ import { focusedDisabled } from "../../../styles";
4
+ import { DateTimeField } from "../../DateTimeField";
5
+
6
+ export function VirtualTableDateField(props: {
7
+ name?: string;
8
+ error?: Error;
9
+ mode?: "date" | "date_time";
10
+ timezone?: string;
11
+ internalValue: Date | undefined | null;
12
+ updateValue: (newValue: (Date | null)) => void;
13
+ focused: boolean;
14
+ disabled: boolean;
15
+ locale?: string;
16
+ onBlur?: React.FocusEventHandler<HTMLInputElement | HTMLTextAreaElement>;
17
+ }) {
18
+ const { disabled, error, mode, timezone, internalValue, updateValue, locale } = props;
19
+
20
+ return (
21
+ <DateTimeField
22
+ value={internalValue ?? undefined}
23
+ onChange={(dateValue) => updateValue(dateValue ?? null)}
24
+ invisible={true}
25
+ inputClassName={cls("w-full h-full", focusedDisabled)}
26
+ className={cls("w-full h-full", focusedDisabled)}
27
+ mode={mode}
28
+ timezone={timezone}
29
+ locale={locale}
30
+ />
31
+ );
32
+ }
@@ -0,0 +1,82 @@
1
+ import React, { useEffect, useRef, useState } from "react";
2
+ import { useDebouncedCallback } from "../../../hooks/useDebouncedCallback";
3
+ import { focusedDisabled } from "../../../styles";
4
+ import { TextareaAutosize } from "../../TextareaAutosize";
5
+
6
+ export function VirtualTableInput(props: {
7
+ error?: Error;
8
+ value: string;
9
+ multiline?: boolean;
10
+ focused: boolean;
11
+ disabled: boolean;
12
+ updateValue: (newValue: (string | null)) => void;
13
+ onBlur?: () => void;
14
+ }) {
15
+ const ref = React.useRef<HTMLTextAreaElement>(null);
16
+ const { disabled, value, multiline = false, updateValue, focused } = props;
17
+ const prevValue = useRef<string | null>(value);
18
+ const [internalValue, setInternalValue] = useState<typeof value>(value);
19
+ const focusedState = useRef<boolean>(false);
20
+
21
+ useEffect(() => {
22
+ if (prevValue.current !== value && value !== internalValue)
23
+ setInternalValue(value);
24
+ prevValue.current = value;
25
+ }, [value]);
26
+
27
+ const doUpdate = React.useCallback(() => {
28
+ const emptyInitialValue = !value;
29
+ if (emptyInitialValue && !internalValue) return;
30
+ if (internalValue !== value && internalValue !== prevValue.current) {
31
+ prevValue.current = internalValue;
32
+ updateValue(internalValue);
33
+ }
34
+ }, [internalValue, updateValue, value]);
35
+
36
+ useDebouncedCallback(internalValue, doUpdate, !focused, 400);
37
+
38
+ useEffect(() => {
39
+ if (ref.current && focused && !focusedState.current) {
40
+ focusedState.current = true;
41
+ ref.current.focus({ preventScroll: true });
42
+ ref.current.selectionStart = ref.current.value.length;
43
+ ref.current.selectionEnd = ref.current.value.length;
44
+ } else {
45
+ focusedState.current = focused;
46
+ }
47
+ }, [focused, ref]);
48
+
49
+ return (
50
+ <TextareaAutosize
51
+ className={focusedDisabled}
52
+ ref={ref}
53
+ style={{
54
+ padding: 0,
55
+ margin: 0,
56
+ width: "100%",
57
+ color: "unset",
58
+ fontWeight: "unset",
59
+ fontSize: "unset",
60
+ fontFamily: "unset",
61
+ background: "unset",
62
+ border: "unset",
63
+ resize: "none",
64
+ outline: "none"
65
+ }}
66
+ value={internalValue ?? ""}
67
+ onChange={(evt) => {
68
+ const newValue = evt.target.value as string;
69
+ if (multiline || !newValue.endsWith("\n"))
70
+ setInternalValue(newValue);
71
+ }}
72
+ onFocus={() => {
73
+ focusedState.current = true;
74
+ }}
75
+ onBlur={() => {
76
+ focusedState.current = false;
77
+ doUpdate();
78
+ if (props.onBlur) props.onBlur();
79
+ }}
80
+ />
81
+ );
82
+ }
@@ -0,0 +1,71 @@
1
+ import React, { useEffect, useRef, useState } from "react";
2
+ import { useDebouncedCallback } from "../../../hooks/useDebouncedCallback";
3
+ import { cls } from "../../../util";
4
+ import { focusedDisabled } from "../../../styles";
5
+ import { TextField } from "../../TextField";
6
+
7
+ export function VirtualTableNumberInput(props: {
8
+ error?: Error;
9
+ value: number;
10
+ align?: "right" | "left" | "center";
11
+ updateValue: (newValue: (number | null)) => void;
12
+ focused: boolean;
13
+ disabled: boolean;
14
+ }) {
15
+ const { align = "left", value, updateValue, focused } = props;
16
+ const propStringValue = (value && typeof value === "number") ? value.toString() : "";
17
+ const [internalValue, setInternalValue] = useState<string | null>(propStringValue);
18
+ const prevValue = useRef<number | null>(value);
19
+
20
+ useEffect(() => {
21
+ if (prevValue.current !== value && String(value) !== internalValue)
22
+ setInternalValue(value ? value.toString() : null);
23
+ prevValue.current = value;
24
+ }, [value]);
25
+
26
+ const doUpdate = React.useCallback(() => {
27
+ if (internalValue !== propStringValue) {
28
+ if (internalValue !== undefined && internalValue !== null) {
29
+ const numberValue = parseFloat(internalValue);
30
+ if (isNaN(numberValue)) return;
31
+ updateValue(numberValue);
32
+ } else {
33
+ updateValue(null);
34
+ }
35
+ }
36
+ }, [internalValue, value]);
37
+
38
+ useDebouncedCallback(internalValue, doUpdate, !focused, 400);
39
+
40
+ useEffect(() => {
41
+ if (!focused && propStringValue !== internalValue)
42
+ setInternalValue(value !== undefined && value !== null ? value.toString() : null);
43
+ }, [value, focused]);
44
+
45
+ const inputRef = React.useRef<HTMLInputElement>(null);
46
+
47
+ useEffect(() => {
48
+ if (inputRef.current && focused) {
49
+ inputRef.current.focus({ preventScroll: true });
50
+ }
51
+ }, [focused, inputRef]);
52
+
53
+ const regexp = /^-?[0-9]+[,.]?[0-9]*$/;
54
+
55
+ return (
56
+ <TextField
57
+ inputRef={inputRef}
58
+ invisible={true}
59
+ size="small"
60
+ className="w-full"
61
+ inputClassName={cls("p-0 m-0 bg-transparent border-none outline-hidden font-normal leading-normal text-unset", focusedDisabled)}
62
+ inputStyle={{ textAlign: align }}
63
+ value={internalValue ?? ""}
64
+ onChange={(evt) => {
65
+ const newValue = evt.target.value.replace(",", ".");
66
+ if (newValue.length === 0) setInternalValue(null);
67
+ if (regexp.test(newValue) || newValue.startsWith("-")) setInternalValue(newValue);
68
+ }}
69
+ />
70
+ );
71
+ }
@@ -0,0 +1,107 @@
1
+ import React, { useCallback, useEffect, useRef } from "react";
2
+ import { Select, SelectItem } from "../../Select";
3
+ import { MultiSelect, MultiSelectItem } from "../../MultiSelect";
4
+ import { Chip } from "../../Chip";
5
+
6
+ export interface VirtualTableSelectOption {
7
+ value: string | number;
8
+ label: string;
9
+ color?: string;
10
+ }
11
+
12
+ export function VirtualTableSelect(props: {
13
+ options: VirtualTableSelectOption[];
14
+ error?: Error;
15
+ multiple?: boolean;
16
+ disabled: boolean;
17
+ small?: boolean;
18
+ value: string | number | string[] | number[] | undefined;
19
+ updateValue: (newValue: (string | number | string[] | number[] | null)) => void;
20
+ focused: boolean;
21
+ onBlur?: React.FocusEventHandler<HTMLInputElement | HTMLTextAreaElement>;
22
+ }) {
23
+ const { options, value, disabled, small = false, focused, updateValue, multiple = false } = props;
24
+ const validValue = (Array.isArray(value) && multiple) || (!Array.isArray(value) && !multiple);
25
+ const ref = useRef<HTMLButtonElement>(null);
26
+
27
+ useEffect(() => {
28
+ if (ref.current && focused) {
29
+ ref.current?.focus({ preventScroll: true });
30
+ }
31
+ }, [focused, ref]);
32
+
33
+ const onChange = useCallback((updatedValue: string | string[]) => {
34
+ if (multiple) {
35
+ const isNumber = options.some(o => typeof o.value === "number");
36
+ const newValue = (updatedValue as string[]).map((v) => {
37
+ const opt = options.find(o => String(o.value) === v);
38
+ return typeof opt?.value === "number" ? parseFloat(v) : v;
39
+ });
40
+ if (isNumber) {
41
+ updateValue(newValue as number[]);
42
+ } else {
43
+ updateValue(newValue as string[]);
44
+ }
45
+ } else {
46
+ const opt = options.find(o => String(o.value) === updatedValue);
47
+ updateValue(typeof opt?.value === "number" ? parseFloat(updatedValue as string) : (updatedValue as string || null));
48
+ }
49
+ }, [multiple, updateValue, options]);
50
+
51
+ const renderValue = (val?: string | number) => {
52
+ const option = options.find(o => o.value === val);
53
+ return (
54
+ <Chip size={small ? "small" : "medium"}>
55
+ {option ? option.label : String(val)}
56
+ </Chip>
57
+ );
58
+ };
59
+
60
+ const handleOpenChange = useCallback((open: boolean) => {
61
+ if (!open && ref.current) {
62
+ setTimeout(() => {
63
+ ref.current?.focus({ preventScroll: true });
64
+ }, 0);
65
+ }
66
+ }, []);
67
+
68
+ return multiple ? (
69
+ <MultiSelect
70
+ inputRef={ref}
71
+ className="w-full h-full p-0 bg-transparent outline-none"
72
+ position={"item-aligned"}
73
+ disabled={disabled}
74
+ includeClear={false}
75
+ useChips={false}
76
+ value={validValue ? (value as (string | number)[]).map(v => String(v)) : []}
77
+ onValueChange={onChange}
78
+ onOpenChange={handleOpenChange}
79
+ >
80
+ {options.map((opt) => (
81
+ <MultiSelectItem key={String(opt.value)} value={String(opt.value)}>
82
+ <Chip size={small ? "small" : "medium"}>{opt.label}</Chip>
83
+ </MultiSelectItem>
84
+ ))}
85
+ </MultiSelect>
86
+ ) : (
87
+ <Select
88
+ inputRef={ref}
89
+ fullWidth
90
+ className="w-full h-full p-0 bg-transparent outline-none [&_button]:ring-0 [&_button]:ring-offset-0"
91
+ inputClassName="ring-0 ring-offset-0 focus:ring-0 focus-visible:ring-0 outline-none focus:outline-none focus-visible:outline-none focus-visible:ring-offset-0"
92
+ position={"item-aligned"}
93
+ disabled={disabled}
94
+ padding={false}
95
+ value={validValue ? String(value) : ""}
96
+ onValueChange={onChange}
97
+ onOpenChange={handleOpenChange}
98
+ renderValue={renderValue}
99
+ >
100
+ {options.map((opt) => (
101
+ <SelectItem key={String(opt.value)} value={String(opt.value)}>
102
+ <Chip size={small ? "small" : "medium"}>{opt.label}</Chip>
103
+ </SelectItem>
104
+ ))}
105
+ </Select>
106
+ );
107
+ }
@@ -0,0 +1,28 @@
1
+ import React, { useEffect } from "react";
2
+ import { BooleanSwitch } from "../../BooleanSwitch";
3
+
4
+ export function VirtualTableSwitch(props: {
5
+ error?: Error;
6
+ internalValue?: boolean;
7
+ focused: boolean;
8
+ disabled: boolean;
9
+ updateValue: (newValue: (boolean | null)) => void;
10
+ }) {
11
+ const { internalValue, updateValue, focused } = props;
12
+ const ref = React.useRef<HTMLButtonElement>(null);
13
+
14
+ useEffect(() => {
15
+ if (ref.current && focused) {
16
+ ref.current.focus({ preventScroll: true });
17
+ }
18
+ }, [focused, ref]);
19
+
20
+ return (
21
+ <BooleanSwitch
22
+ ref={ref}
23
+ size={"small"}
24
+ value={Boolean(internalValue)}
25
+ onValueChange={updateValue}
26
+ />
27
+ );
28
+ }
@@ -1,3 +1,10 @@
1
1
  export { VirtualTable } from "./VirtualTable";
2
2
  export * from "./VirtualTableProps";
3
3
  export type { FilterFormFieldProps } from "./VirtualTableHeader";
4
+ export * from "./selection/SelectionStore";
5
+ export * from "./selection/SelectionContext";
6
+ export * from "./fields/VirtualTableInput";
7
+ export * from "./fields/VirtualTableNumberInput";
8
+ export * from "./fields/VirtualTableSwitch";
9
+ export * from "./fields/VirtualTableDateField";
10
+ export * from "./fields/VirtualTableSelect";
@@ -0,0 +1,36 @@
1
+ import React, { createContext, useContext } from "react";
2
+ import { VirtualTableSelectionStore, SelectedCell } from "./SelectionStore";
3
+
4
+ export interface VirtualTableSelectionContextProps<T extends SelectedCell = SelectedCell> {
5
+ selectionStore: VirtualTableSelectionStore<T>;
6
+ select: (cell: T | undefined) => void;
7
+ }
8
+
9
+ const SelectionContext = createContext<VirtualTableSelectionContextProps<any> | null>(null);
10
+
11
+ export const VirtualTableSelectionProvider = <T extends SelectedCell = SelectedCell>({
12
+ store,
13
+ children
14
+ }: {
15
+ store: VirtualTableSelectionStore<T>;
16
+ children: React.ReactNode;
17
+ }) => {
18
+ const value = React.useMemo(() => ({
19
+ selectionStore: store,
20
+ select: store.select
21
+ }), [store]);
22
+
23
+ return (
24
+ <SelectionContext.Provider value={value}>
25
+ {children}
26
+ </SelectionContext.Provider>
27
+ );
28
+ };
29
+
30
+ export const useVirtualTableSelection = <T extends SelectedCell = SelectedCell>() => {
31
+ const context = useContext(SelectionContext);
32
+ if (!context) {
33
+ throw new Error("useVirtualTableSelection must be used within a VirtualTableSelectionProvider");
34
+ }
35
+ return context as VirtualTableSelectionContextProps<T>;
36
+ };
@@ -0,0 +1,54 @@
1
+ import { useSyncExternalStore, useCallback, useRef } from "react";
2
+
3
+ export interface SelectedCell {
4
+ columnKey: string;
5
+ id: string | number;
6
+ cellRect?: DOMRect;
7
+ width?: number;
8
+ height?: number;
9
+ }
10
+
11
+ export function createVirtualTableSelectionStore<T extends SelectedCell = SelectedCell>() {
12
+ let selectedCell: T | undefined = undefined;
13
+ const listeners = new Set<() => void>();
14
+
15
+ function getEntity(): T | undefined {
16
+ return selectedCell;
17
+ }
18
+
19
+ function subscribe(listener: () => void): () => void {
20
+ listeners.add(listener);
21
+ return () => listeners.delete(listener);
22
+ }
23
+
24
+ function select(cell: T | undefined) {
25
+ selectedCell = cell;
26
+ listeners.forEach(l => l());
27
+ }
28
+
29
+ return {
30
+ getEntity,
31
+ subscribe,
32
+ select
33
+ };
34
+ }
35
+
36
+ export type VirtualTableSelectionStore<T extends SelectedCell = SelectedCell> = ReturnType<typeof createVirtualTableSelectionStore<T>>;
37
+
38
+ export function useVirtualTableCellSelected<T extends SelectedCell = SelectedCell>(
39
+ store: VirtualTableSelectionStore<T>,
40
+ columnKey: string,
41
+ id: string | number
42
+ ): boolean {
43
+ const selectorRef = useRef({ columnKey, id });
44
+ selectorRef.current = { columnKey, id };
45
+
46
+ const getEntity = useCallback(() => {
47
+ const cell = store.getEntity();
48
+ if (!cell) return false;
49
+ const s = selectorRef.current;
50
+ return cell.columnKey === s.columnKey && cell.id === s.id;
51
+ }, [store]);
52
+
53
+ return useSyncExternalStore(store.subscribe, getEntity, getEntity);
54
+ }
package/src/index.ts CHANGED
@@ -1,7 +1,45 @@
1
+ // =============================================================================
2
+ // Components
3
+ // =============================================================================
1
4
  export * from "./components";
2
- export * from "./styles";
3
- export * from "./util";
4
- export * from "./icons";
5
- export * from "./hooks";
5
+
6
+ // =============================================================================
7
+ // Views
8
+ // =============================================================================
6
9
  export * from "./views";
7
10
 
11
+ // =============================================================================
12
+ // Icons
13
+ // =============================================================================
14
+ export * from "./icons";
15
+
16
+ // =============================================================================
17
+ // Design System Utilities
18
+ // =============================================================================
19
+ export * from "./styles";
20
+ export { cls } from "./util/cls";
21
+ export { CHIP_COLORS, getColorSchemeForKey, getColorSchemeForSeed } from "./util/chip_colors";
22
+
23
+ // =============================================================================
24
+ // Public Hooks
25
+ // =============================================================================
26
+ export { useOutsideAlerter } from "./hooks/useOutsideAlerter";
27
+ export { useDebouncedCallback } from "./hooks/useDebouncedCallback";
28
+ export { useDebounceCallback } from "./hooks/useDebounceCallback";
29
+ export { useDebounceValue } from "./hooks/useDebounceValue";
30
+ export { useInjectStyles } from "./hooks/useInjectStyles";
31
+ export { PortalContainerProvider, usePortalContainer } from "./hooks/PortalContainerContext";
32
+ export type { PortalContainerContextType, PortalContainerProviderProps } from "./hooks/PortalContainerContext";
33
+
34
+ // =============================================================================
35
+ // Lower-level utilities (used by framework packages)
36
+ // =============================================================================
37
+ export { debounce } from "./util/debounce";
38
+ export type { Cancelable } from "./util/debounce";
39
+
40
+ // =============================================================================
41
+ // NOT exported (internal plumbing — import directly if needed):
42
+ //
43
+ // ./util/hash – hashString
44
+ // ./util/key_to_icon_component – keyToIconComponent
45
+ // =============================================================================