@firecms/ui 3.0.0-canary.118 → 3.0.0-canary.119

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": "@firecms/ui",
3
3
  "type": "module",
4
- "version": "3.0.0-canary.118",
4
+ "version": "3.0.0-canary.119",
5
5
  "description": "Awesome Firebase/Firestore-based headless open-source CMS",
6
6
  "funding": {
7
7
  "url": "https://github.com/sponsors/firecmsco"
@@ -105,7 +105,7 @@
105
105
  "src",
106
106
  "tailwind.config.js"
107
107
  ],
108
- "gitHead": "91168e34bae600c45320b3092a153bc4d60d0018",
108
+ "gitHead": "22e1ffb78b12816a576df70e3aa7df1ee29e26ad",
109
109
  "publishConfig": {
110
110
  "access": "public"
111
111
  }
@@ -57,7 +57,7 @@ export const Dialog = ({
57
57
  if (!open) {
58
58
  const timeout = setTimeout(() => {
59
59
  setDisplayed(false);
60
- }, 250);
60
+ }, 150);
61
61
  return () => clearTimeout(timeout);
62
62
  } else {
63
63
  setDisplayed(true);
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
  import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
3
- import { paperMixin } from "../styles";
3
+ import { focusedDisabled, paperMixin } from "../styles";
4
4
  import { cls } from "../util";
5
5
 
6
6
  export type MenuProps = {
@@ -12,6 +12,8 @@ export type MenuProps = {
12
12
  onOpenChange?(open: boolean): void;
13
13
 
14
14
  portalContainer?: HTMLElement | null;
15
+ side?: "top" | "right" | "bottom" | "left";
16
+ align?: "start" | "center" | "end";
15
17
  }
16
18
 
17
19
  const Menu = React.forwardRef<
@@ -22,6 +24,8 @@ const Menu = React.forwardRef<
22
24
  trigger,
23
25
  open,
24
26
  defaultOpen,
27
+ side,
28
+ align,
25
29
  onOpenChange,
26
30
  portalContainer
27
31
  }, ref) => (
@@ -35,7 +39,10 @@ const Menu = React.forwardRef<
35
39
  {trigger}
36
40
  </DropdownMenu.Trigger>
37
41
  <DropdownMenu.Portal container={portalContainer}>
38
- <DropdownMenu.Content className={cls(paperMixin, "shadow py-2 z-30")}>
42
+ <DropdownMenu.Content
43
+ side={side}
44
+ align={align}
45
+ className={cls(paperMixin, focusedDisabled, "shadow py-2 z-30")}>
39
46
  {children}
40
47
  </DropdownMenu.Content>
41
48
  </DropdownMenu.Portal>
@@ -28,10 +28,7 @@ export type MultiSelectProps = {
28
28
  disabled?: boolean,
29
29
  error?: boolean,
30
30
  position?: "item-aligned" | "popper",
31
- endAdornment?: React.ReactNode,
32
31
  inputRef?: React.RefObject<HTMLButtonElement>,
33
- padding?: boolean,
34
- includeFocusOutline?: boolean,
35
32
  children?: React.ReactNode,
36
33
  };
37
34
 
@@ -52,7 +49,6 @@ export function MultiSelect({
52
49
  disabled,
53
50
  renderValue,
54
51
  renderValues,
55
- includeFocusOutline = true,
56
52
  containerClassName,
57
53
  className,
58
54
  children,
@@ -150,7 +146,9 @@ export function MultiSelect({
150
146
 
151
147
  </div>
152
148
 
153
- <Dialog.Root open={openInternal} onOpenChange={setOpenInternal}>
149
+ <Dialog.Root open={openInternal}
150
+ modal={true}
151
+ onOpenChange={setOpenInternal}>
154
152
  <Dialog.Portal>
155
153
  <MultiSelectContext.Provider
156
154
  value={{
@@ -0,0 +1,370 @@
1
+ // src/components/multi-select.tsx
2
+ import * as PopoverPrimitive from "@radix-ui/react-popover";
3
+
4
+ import * as React from "react";
5
+ import { Command as CommandPrimitive } from "cmdk";
6
+ import { cls } from "../util";
7
+ import { CloseIcon, ExpandMoreIcon } from "../icons";
8
+ import { Separator } from "./Separator";
9
+ import { Checkbox } from "./Checkbox";
10
+ import { Chip } from "./Chip";
11
+ import {
12
+ defaultBorderMixin,
13
+ fieldBackgroundDisabledMixin,
14
+ fieldBackgroundHoverMixin,
15
+ fieldBackgroundInvisibleMixin,
16
+ fieldBackgroundMixin,
17
+ focusedDisabled
18
+ } from "../styles";
19
+
20
+ /**
21
+ * Props for MultiSelect component
22
+ */
23
+ interface MultiSelectProps
24
+ extends React.ButtonHTMLAttributes<HTMLButtonElement> {
25
+ /**
26
+ * An array of option objects to be displayed in the multi-select component.
27
+ * Each option object has a label, value, and an optional icon.
28
+ */
29
+ options: {
30
+ /** The text to display for the option. */
31
+ label: string;
32
+ /** The unique value associated with the option. */
33
+ value: string;
34
+ }[];
35
+
36
+ /**
37
+ * Callback function triggered when the selected values change.
38
+ * Receives an array of the new selected values.
39
+ */
40
+ onValueChange: (value: string[]) => void;
41
+
42
+ /** The default selected values when the component mounts. */
43
+ defaultValue: string[];
44
+
45
+ /**
46
+ * Placeholder text to be displayed when no values are selected.
47
+ * Optional, defaults to "Select options".
48
+ */
49
+ placeholder?: string;
50
+
51
+ /**
52
+ * Animation duration in seconds for the visual effects (e.g., bouncing badges).
53
+ * Optional, defaults to 0 (no animation).
54
+ */
55
+ animation?: number;
56
+
57
+ /**
58
+ * Maximum number of items to display. Extra selected items will be summarized.
59
+ * Optional, defaults to 3.
60
+ */
61
+ maxCount?: number;
62
+
63
+ /**
64
+ * The modality of the popover. When set to true, interaction with outside elements
65
+ * will be disabled and only popover content will be visible to screen readers.
66
+ * Optional, defaults to false.
67
+ */
68
+ modalPopover?: boolean;
69
+
70
+ /**
71
+ * If true, renders the multi-select component as a child of another component.
72
+ * Optional, defaults to false.
73
+ */
74
+ asChild?: boolean;
75
+
76
+ /**
77
+ * Additional class names to apply custom styles to the multi-select component.
78
+ * Optional, can be used to add custom styles.
79
+ */
80
+ className?: string;
81
+
82
+ size?: "small" | "medium",
83
+
84
+ invisible?: boolean;
85
+ disabled?: boolean;
86
+
87
+ variant?: "default" | "secondary" | "destructive";
88
+ }
89
+
90
+ export const NewMultiSelect = React.forwardRef<
91
+ HTMLButtonElement,
92
+ MultiSelectProps
93
+ >(
94
+ (
95
+ {
96
+ size,
97
+ options,
98
+ onValueChange,
99
+ invisible,
100
+ disabled,
101
+ variant,
102
+ defaultValue = [],
103
+ placeholder = "Select options",
104
+ animation = 0,
105
+ maxCount = 3,
106
+ modalPopover = false,
107
+ asChild = false,
108
+ className,
109
+ ...props
110
+ },
111
+ ref
112
+ ) => {
113
+ const [selectedValues, setSelectedValues] =
114
+ React.useState<string[]>(defaultValue);
115
+ const [isPopoverOpen, setIsPopoverOpen] = React.useState(false);
116
+ // const [isAnimating, setIsAnimating] = React.useState(false);
117
+
118
+ React.useEffect(() => {
119
+ setSelectedValues(defaultValue);
120
+ }, [defaultValue]);
121
+
122
+ const handleInputKeyDown = (
123
+ event: React.KeyboardEvent<HTMLInputElement>
124
+ ) => {
125
+ if (event.key === "Enter") {
126
+ setIsPopoverOpen(true);
127
+ } else if (event.key === "Backspace" && !event.currentTarget.value) {
128
+ const newSelectedValues = [...selectedValues];
129
+ newSelectedValues.pop();
130
+ setSelectedValues(newSelectedValues);
131
+ onValueChange(newSelectedValues);
132
+ }
133
+ };
134
+
135
+ const toggleOption = (value: string) => {
136
+ const newSelectedValues = selectedValues.includes(value)
137
+ ? selectedValues.filter((v) => v !== value)
138
+ : [...selectedValues, value];
139
+ setSelectedValues(newSelectedValues);
140
+ onValueChange(newSelectedValues);
141
+ };
142
+
143
+ const handleClear = () => {
144
+ setSelectedValues([]);
145
+ onValueChange([]);
146
+ };
147
+
148
+ const handleTogglePopover = () => {
149
+ setIsPopoverOpen((prev) => !prev);
150
+ };
151
+
152
+ const clearExtraOptions = () => {
153
+ const newSelectedValues = selectedValues.slice(0, maxCount);
154
+ setSelectedValues(newSelectedValues);
155
+ onValueChange(newSelectedValues);
156
+ };
157
+
158
+ const toggleAll = () => {
159
+ if (selectedValues.length === options.length) {
160
+ handleClear();
161
+ } else {
162
+ const allValues = options.map((option) => option.value);
163
+ setSelectedValues(allValues);
164
+ onValueChange(allValues);
165
+ }
166
+ };
167
+
168
+ return (
169
+ <PopoverPrimitive.Root
170
+ open={isPopoverOpen}
171
+ onOpenChange={setIsPopoverOpen}
172
+ modal={modalPopover}
173
+ >
174
+ <PopoverPrimitive.Trigger asChild>
175
+ <button
176
+ ref={ref}
177
+ {...props}
178
+ onClick={handleTogglePopover}
179
+ className={cls(
180
+ size === "small" ? "min-h-[42px]" : "min-h-[64px]",
181
+ "select-none rounded-md text-sm",
182
+ invisible ? fieldBackgroundInvisibleMixin : fieldBackgroundMixin,
183
+ disabled ? fieldBackgroundDisabledMixin : fieldBackgroundHoverMixin,
184
+ "relative flex items-center",
185
+ className
186
+ )}
187
+ >
188
+ {selectedValues.length > 0 ? (
189
+ <div className="flex justify-between items-center w-full">
190
+ <div className="flex flex-wrap items-center">
191
+ {selectedValues.slice(0, maxCount).map((value) => {
192
+ const option = options.find((o) => o.value === value);
193
+ return (
194
+ <Chip
195
+ key={value}
196
+ className={cls(
197
+ "m-1 transition ease-in-out delay-150 hover:-translate-y-1 hover:scale-110 duration-300",
198
+ {
199
+ "border-foreground/10 text-foreground bg-card hover:bg-card/80": variant === "default",
200
+ "border-foreground/10 bg-secondary text-secondary-foreground hover:bg-secondary/80": variant === "secondary",
201
+ "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80": variant === "destructive",
202
+ }
203
+ )}
204
+ style={{ animationDuration: `${animation}s` }}
205
+ >
206
+ {option?.label}
207
+ <CloseIcon
208
+ size={"smallest"}
209
+ onClick={(event) => {
210
+ event.stopPropagation();
211
+ toggleOption(value);
212
+ }}
213
+ />
214
+ </Chip>
215
+ );
216
+ })}
217
+ {selectedValues.length > maxCount && (
218
+ <Chip
219
+ className={cls(
220
+ "bg-transparent text-foreground border-foreground/1 hover:bg-transparent",
221
+ "m-1 transition ease-in-out delay-150 hover:-translate-y-1 hover:scale-110 duration-300",
222
+ {
223
+ "border-foreground/10 text-foreground bg-card hover:bg-card/80": variant === "default",
224
+ "border-foreground/10 bg-secondary text-secondary-foreground hover:bg-secondary/80": variant === "secondary",
225
+ "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80": variant === "destructive",
226
+ }
227
+ )}
228
+ style={{ animationDuration: `${animation}s` }}
229
+ >
230
+ {`+ ${selectedValues.length - maxCount} more`}
231
+ <CloseIcon
232
+ size={"smallest"}
233
+ onClick={(event) => {
234
+ event.stopPropagation();
235
+ clearExtraOptions();
236
+ }}
237
+ />
238
+ </Chip>
239
+ )}
240
+ </div>
241
+ <div className="flex items-center justify-between">
242
+ <CloseIcon
243
+ size={"small"}
244
+ onClick={(event) => {
245
+ event.stopPropagation();
246
+ handleClear();
247
+ }}
248
+ />
249
+ <Separator
250
+ orientation="vertical"
251
+ />
252
+
253
+ <div className={cls("px-2 h-full flex items-center")}>
254
+ <ExpandMoreIcon size={"small"}
255
+ className={cls("transition", isPopoverOpen ? "rotate-180" : "")}/>
256
+ </div>
257
+ </div>
258
+ </div>
259
+ ) : (
260
+ <div className="flex items-center justify-between w-full mx-auto">
261
+ <span className="text-sm text-muted-foreground mx-3">
262
+ {placeholder}
263
+ </span>
264
+ <div className={cls("px-2 h-full flex items-center")}>
265
+ <ExpandMoreIcon size={"small"}
266
+ className={cls("transition", isPopoverOpen ? "rotate-180" : "")}/>
267
+ </div>
268
+ </div>
269
+ )}
270
+ </button>
271
+ </PopoverPrimitive.Trigger>
272
+ <PopoverPrimitive.Content
273
+ className={cls("z-50 relative overflow-hidden border bg-white dark:bg-gray-900 p-2 rounded-lg", defaultBorderMixin)}
274
+ align="start"
275
+ onEscapeKeyDown={() => setIsPopoverOpen(false)}
276
+ >
277
+ <CommandPrimitive>
278
+ <CommandPrimitive.Input
279
+ className={cls(focusedDisabled, "bg-transparent outline-none flex-1 h-full w-full m-4")}
280
+ placeholder="Search..."
281
+ onKeyDown={handleInputKeyDown}
282
+ />
283
+ <CommandPrimitive.List>
284
+ <CommandPrimitive.Empty>No results found.</CommandPrimitive.Empty>
285
+ <CommandPrimitive.Group>
286
+ <CommandPrimitive.Item
287
+ key="all"
288
+ onSelect={toggleAll}
289
+ className={
290
+ cls(
291
+ // (fieldValue ?? []).includes(value) ? "bg-slate-200 dark:bg-slate-950" : "",
292
+ "cursor-pointer",
293
+ "flex flex-row items-center gap-2",
294
+ "ring-offset-transparent",
295
+ "p-1 rounded aria-[selected=true]:outline-none aria-[selected=true]:ring-2 aria-[selected=true]:ring-primary aria-[selected=true]:ring-opacity-75 aria-[selected=true]:ring-offset-2",
296
+ "aria-[selected=true]:bg-slate-100 aria-[selected=true]:dark:bg-slate-900",
297
+ "cursor-pointer p-1 rounded aria-[selected=true]:bg-slate-100 aria-[selected=true]:dark:bg-slate-900",
298
+ className
299
+ )
300
+ }
301
+ >
302
+ <Checkbox checked={selectedValues.length === options.length} size={"small"}/>
303
+ <span>(Select All)</span>
304
+ </CommandPrimitive.Item>
305
+ {options.map((option) => {
306
+ const isSelected = selectedValues.includes(option.value);
307
+ return (
308
+ <CommandPrimitive.Item
309
+ key={option.value}
310
+ onSelect={() => toggleOption(option.value)}
311
+ className={cls(
312
+ // (fieldValue ?? []).includes(value) ? "bg-slate-200 dark:bg-slate-950" : "",
313
+ "cursor-pointer",
314
+ "flex flex-row items-center gap-2",
315
+ "ring-offset-transparent",
316
+ "p-1 rounded aria-[selected=true]:outline-none aria-[selected=true]:ring-2 aria-[selected=true]:ring-primary aria-[selected=true]:ring-opacity-75 aria-[selected=true]:ring-offset-2",
317
+ "aria-[selected=true]:bg-slate-100 aria-[selected=true]:dark:bg-slate-900",
318
+ "cursor-pointer p-1 rounded aria-[selected=true]:bg-slate-100 aria-[selected=true]:dark:bg-slate-900",
319
+ className
320
+ )}
321
+ >
322
+
323
+ <Checkbox checked={isSelected} size={"small"}/>
324
+ <span>{option.label}</span>
325
+ </CommandPrimitive.Item>
326
+ );
327
+ })}
328
+ </CommandPrimitive.Group>
329
+ <CommandPrimitive.Separator/>
330
+ <CommandPrimitive.Group>
331
+ <div className="flex items-center justify-between">
332
+ {selectedValues.length > 0 && (
333
+ <>
334
+ <CommandPrimitive.Item
335
+ onSelect={handleClear}
336
+ className="flex-1 justify-center cursor-pointer"
337
+ >
338
+ Clear
339
+ </CommandPrimitive.Item>
340
+ <Separator
341
+ orientation="vertical"
342
+ />
343
+ </>
344
+ )}
345
+ <CommandPrimitive.Item
346
+ onSelect={() => setIsPopoverOpen(false)}
347
+ className="flex-1 justify-center cursor-pointer max-w-full"
348
+ >
349
+ Close
350
+ </CommandPrimitive.Item>
351
+ </div>
352
+ </CommandPrimitive.Group>
353
+ </CommandPrimitive.List>
354
+ </CommandPrimitive>
355
+ </PopoverPrimitive.Content>
356
+ {/*{animation > 0 && selectedValues.length > 0 && (*/}
357
+ {/* <WandSparkles*/}
358
+ {/* className={cls(*/}
359
+ {/* "cursor-pointer my-2 text-foreground bg-background w-3 h-3",*/}
360
+ {/* isAnimating ? "" : "text-muted-foreground"*/}
361
+ {/* )}*/}
362
+ {/* onClick={() => setIsAnimating(!isAnimating)}*/}
363
+ {/* />*/}
364
+ {/*)}*/}
365
+ </PopoverPrimitive.Root>
366
+ );
367
+ }
368
+ );
369
+
370
+ NewMultiSelect.displayName = "MultiSelect";
@@ -1,6 +1,7 @@
1
1
  import React, { ChangeEvent, forwardRef, useCallback, useEffect, useState } from "react";
2
2
  import * as SelectPrimitive from "@radix-ui/react-select";
3
3
  import {
4
+ defaultBorderMixin,
4
5
  fieldBackgroundDisabledMixin,
5
6
  fieldBackgroundHoverMixin,
6
7
  fieldBackgroundInvisibleMixin,
@@ -168,7 +169,7 @@ export const Select = forwardRef<HTMLDivElement, SelectProps>(({
168
169
  </div>
169
170
  <SelectPrimitive.Portal>
170
171
  <SelectPrimitive.Content position={position}
171
- className="z-50 relative overflow-hidden border border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-800 p-2 rounded-lg shadow-lg">
172
+ className={cls("z-50 relative overflow-hidden border bg-white dark:bg-gray-900 p-2 rounded-lg", defaultBorderMixin)}>
172
173
  <SelectPrimitive.Viewport className={"p-1"}
173
174
  style={{ maxHeight: "var(--radix-select-content-available-height)" }}>
174
175
  {children}
@@ -206,8 +207,8 @@ export function SelectItem({
206
207
  "w-full",
207
208
  "relative flex items-center p-2 rounded-md text-sm text-slate-700 dark:text-slate-300",
208
209
  "focus:z-10",
209
- "data-[state=checked]:bg-slate-100 data-[state=checked]:dark:bg-slate-900 focus:bg-slate-100 dark:focus:bg-slate-950",
210
- "data-[state=checked]:focus:bg-slate-200 data-[state=checked]:dark:focus:bg-slate-950",
210
+ "data-[state=checked]:bg-slate-100 data-[state=checked]:dark:bg-slate-800 focus:bg-slate-100 dark:focus:bg-gray-950",
211
+ "data-[state=checked]:focus:bg-slate-200 data-[state=checked]:dark:focus:bg-gray-950",
211
212
  disabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer",
212
213
  "[&>*]:w-full",
213
214
  "overflow-visible",
@@ -26,6 +26,7 @@ export * from "./Markdown";
26
26
  export * from "./Menu";
27
27
  export * from "./Menubar";
28
28
  export * from "./MultiSelect";
29
+ export * from "./NewMultiSelect";
29
30
  export * from "./Paper";
30
31
  export * from "./RadioGroup";
31
32
  export * from "./SearchBar";