@firecms/ui 3.1.0-canary.24c8270 → 3.1.0-canary.501d471
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/dist/components/BooleanSwitchWithLabel.d.ts +2 -1
- package/dist/components/Chip.d.ts +1 -1
- package/dist/components/ResizablePanels.d.ts +16 -0
- package/dist/components/SearchableSelect.d.ts +48 -0
- package/dist/components/Tabs.d.ts +8 -1
- package/dist/components/Tooltip.d.ts +18 -2
- package/dist/components/index.d.ts +2 -0
- package/dist/icons/FirestoreIcon.d.ts +6 -0
- package/dist/icons/components/DatabaseIcon.d.ts +6 -0
- package/dist/icons/index.d.ts +2 -0
- package/dist/index.es.js +1377 -478
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +1379 -480
- package/dist/index.umd.js.map +1 -1
- package/package.json +2 -2
- package/src/components/BooleanSwitchWithLabel.tsx +4 -0
- package/src/components/Button.tsx +2 -1
- package/src/components/Chip.tsx +4 -3
- package/src/components/DateTimeField.tsx +7 -2
- package/src/components/MultiSelect.tsx +23 -4
- package/src/components/ResizablePanels.tsx +181 -0
- package/src/components/SearchableSelect.tsx +335 -0
- package/src/components/Skeleton.tsx +4 -2
- package/src/components/Tabs.tsx +44 -16
- package/src/components/TextareaAutosize.tsx +77 -212
- package/src/components/Tooltip.tsx +7 -6
- package/src/components/index.tsx +2 -0
- package/src/icons/FirestoreIcon.tsx +47 -0
- package/src/icons/components/DatabaseIcon.tsx +10 -0
- package/src/icons/index.ts +2 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { useEffect, useRef, useState } from "react";
|
|
5
|
+
import { Command as CommandPrimitive } from "cmdk";
|
|
6
|
+
import { cls } from "../util";
|
|
7
|
+
import { CheckIcon, KeyboardArrowDownIcon } from "../icons";
|
|
8
|
+
import { Separator } from "./Separator";
|
|
9
|
+
import {
|
|
10
|
+
defaultBorderMixin,
|
|
11
|
+
fieldBackgroundDisabledMixin,
|
|
12
|
+
fieldBackgroundHoverMixin,
|
|
13
|
+
fieldBackgroundInvisibleMixin,
|
|
14
|
+
fieldBackgroundMixin,
|
|
15
|
+
focusedDisabled
|
|
16
|
+
} from "../styles";
|
|
17
|
+
import { useInjectStyles } from "../hooks";
|
|
18
|
+
import { usePortalContainer } from "../hooks/PortalContainerContext";
|
|
19
|
+
|
|
20
|
+
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
export interface SearchableSelectProps {
|
|
23
|
+
/** Currently selected value. Can be one of the items or a custom string. */
|
|
24
|
+
value?: string;
|
|
25
|
+
/** Callback when the value changes (from selection or custom input). */
|
|
26
|
+
onValueChange?: (value: string) => void;
|
|
27
|
+
/** Placeholder shown when no value is selected. */
|
|
28
|
+
placeholder?: string;
|
|
29
|
+
/** Label above the field. */
|
|
30
|
+
label?: React.ReactNode | string;
|
|
31
|
+
/** Size variant. */
|
|
32
|
+
size?: "smallest" | "small" | "medium" | "large";
|
|
33
|
+
/** Whether the field is disabled. */
|
|
34
|
+
disabled?: boolean;
|
|
35
|
+
/** Whether to show an error state. */
|
|
36
|
+
error?: boolean;
|
|
37
|
+
/** Whether to use the invisible (borderless) style. */
|
|
38
|
+
invisible?: boolean;
|
|
39
|
+
/** CSS class for the trigger button. */
|
|
40
|
+
className?: string;
|
|
41
|
+
/** CSS class for the trigger input area. */
|
|
42
|
+
inputClassName?: string;
|
|
43
|
+
/** Render the selected value in a custom way in the trigger. */
|
|
44
|
+
renderValue?: (value: string) => React.ReactNode;
|
|
45
|
+
/** Whether the popover should trap focus. */
|
|
46
|
+
modalPopover?: boolean;
|
|
47
|
+
/** If true, allow accepting the typed text as the value even if it doesn't match an item. */
|
|
48
|
+
allowCustomValues?: boolean;
|
|
49
|
+
/** Portal container element. */
|
|
50
|
+
portalContainer?: HTMLElement | null;
|
|
51
|
+
/** If true, auto-open the popover on mount so the user can start typing immediately. */
|
|
52
|
+
autoFocus?: boolean;
|
|
53
|
+
/** The option items — use SearchableSelectItem. */
|
|
54
|
+
children: React.ReactNode;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface SearchableSelectItemProps {
|
|
58
|
+
value: string;
|
|
59
|
+
children?: React.ReactNode;
|
|
60
|
+
className?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ─── Component ──────────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
export const SearchableSelect = React.forwardRef<
|
|
66
|
+
HTMLButtonElement,
|
|
67
|
+
SearchableSelectProps
|
|
68
|
+
>(
|
|
69
|
+
(
|
|
70
|
+
{
|
|
71
|
+
value,
|
|
72
|
+
onValueChange,
|
|
73
|
+
placeholder = "Select...",
|
|
74
|
+
label,
|
|
75
|
+
size = "large",
|
|
76
|
+
disabled,
|
|
77
|
+
error,
|
|
78
|
+
invisible,
|
|
79
|
+
className,
|
|
80
|
+
inputClassName,
|
|
81
|
+
renderValue,
|
|
82
|
+
modalPopover = false,
|
|
83
|
+
allowCustomValues = true,
|
|
84
|
+
portalContainer,
|
|
85
|
+
autoFocus,
|
|
86
|
+
children,
|
|
87
|
+
},
|
|
88
|
+
ref
|
|
89
|
+
) => {
|
|
90
|
+
const [isMounted, setIsMounted] = useState(false);
|
|
91
|
+
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
|
92
|
+
const [search, setSearch] = useState("");
|
|
93
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
94
|
+
|
|
95
|
+
const contextContainer = usePortalContainer();
|
|
96
|
+
const finalContainer = (portalContainer ?? contextContainer ?? undefined) as HTMLElement | undefined;
|
|
97
|
+
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
setIsMounted(true);
|
|
100
|
+
}, []);
|
|
101
|
+
|
|
102
|
+
// Auto-open popover on mount when autoFocus is true
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
if (autoFocus && isMounted) {
|
|
105
|
+
onPopoverOpenChange(true);
|
|
106
|
+
}
|
|
107
|
+
}, [autoFocus, isMounted]);
|
|
108
|
+
|
|
109
|
+
// Collect all item values + labels from children
|
|
110
|
+
const itemsMap = React.useMemo(() => {
|
|
111
|
+
const map = new Map<string, React.ReactNode>();
|
|
112
|
+
React.Children.forEach(children, (child) => {
|
|
113
|
+
if (React.isValidElement<SearchableSelectItemProps>(child) && child.props.value != null) {
|
|
114
|
+
map.set(String(child.props.value), child.props.children ?? child.props.value);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
return map;
|
|
118
|
+
}, [children]);
|
|
119
|
+
|
|
120
|
+
const onPopoverOpenChange = (open: boolean) => {
|
|
121
|
+
setIsPopoverOpen(open);
|
|
122
|
+
if (open) {
|
|
123
|
+
// Pre-fill search with current value for easy editing
|
|
124
|
+
setSearch(value ?? "");
|
|
125
|
+
// Focus the input after popover opens
|
|
126
|
+
setTimeout(() => inputRef.current?.focus(), 0);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const handleSelect = (selectedValue: string) => {
|
|
131
|
+
onValueChange?.(selectedValue);
|
|
132
|
+
setIsPopoverOpen(false);
|
|
133
|
+
setSearch("");
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
137
|
+
if (e.key === "Enter" && allowCustomValues) {
|
|
138
|
+
const trimmed = search.trim();
|
|
139
|
+
if (trimmed) {
|
|
140
|
+
// If cmdk found no match, accept custom value
|
|
141
|
+
// If there are matches, cmdk will handle selecting the highlighted one
|
|
142
|
+
// We check if the current search is NOT one of the items
|
|
143
|
+
const isExistingItem = itemsMap.has(trimmed);
|
|
144
|
+
if (!isExistingItem) {
|
|
145
|
+
e.preventDefault();
|
|
146
|
+
handleSelect(trimmed);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
} else if (e.key === "Escape") {
|
|
150
|
+
setIsPopoverOpen(false);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// Resolve display label for the trigger
|
|
155
|
+
const displayLabel = React.useMemo(() => {
|
|
156
|
+
if (!value) return null;
|
|
157
|
+
if (renderValue) return renderValue(value);
|
|
158
|
+
const itemLabel = itemsMap.get(value);
|
|
159
|
+
if (itemLabel) return itemLabel;
|
|
160
|
+
return <span className="text-sm">{value}</span>;
|
|
161
|
+
}, [value, renderValue, itemsMap]);
|
|
162
|
+
|
|
163
|
+
useInjectStyles("SearchableSelect", `
|
|
164
|
+
[cmdk-group] {
|
|
165
|
+
max-height: 45vh;
|
|
166
|
+
overflow-y: auto;
|
|
167
|
+
}`);
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<div>
|
|
171
|
+
{label && (
|
|
172
|
+
typeof label === "string"
|
|
173
|
+
? <div className={cls("text-sm font-medium ml-3.5 mb-1",
|
|
174
|
+
error ? "text-red-500 dark:text-red-600" : "text-surface-accent-500 dark:text-surface-accent-300",
|
|
175
|
+
)}>{label}</div>
|
|
176
|
+
: label
|
|
177
|
+
)}
|
|
178
|
+
|
|
179
|
+
<PopoverPrimitive.Root
|
|
180
|
+
open={isMounted && isPopoverOpen}
|
|
181
|
+
onOpenChange={onPopoverOpenChange}
|
|
182
|
+
modal={modalPopover}
|
|
183
|
+
>
|
|
184
|
+
<PopoverPrimitive.Trigger asChild>
|
|
185
|
+
<button
|
|
186
|
+
ref={ref}
|
|
187
|
+
disabled={disabled}
|
|
188
|
+
onClick={() => !disabled && onPopoverOpenChange(!isPopoverOpen)}
|
|
189
|
+
className={cls(
|
|
190
|
+
{
|
|
191
|
+
"min-h-[28px]": size === "smallest",
|
|
192
|
+
"min-h-[32px]": size === "small",
|
|
193
|
+
"min-h-[44px]": size === "medium",
|
|
194
|
+
"min-h-[64px]": size === "large",
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
"py-0.5": size === "smallest",
|
|
198
|
+
"py-1": size === "small",
|
|
199
|
+
"py-2": size === "medium" || size === "large",
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
"px-2": size === "small" || size === "smallest",
|
|
203
|
+
"px-4": size === "medium" || size === "large",
|
|
204
|
+
},
|
|
205
|
+
"select-none rounded-md text-sm w-full text-start",
|
|
206
|
+
"focus:ring-0 focus-visible:ring-0 outline-none focus:outline-none focus-visible:outline-none",
|
|
207
|
+
invisible ? fieldBackgroundInvisibleMixin : fieldBackgroundMixin,
|
|
208
|
+
disabled ? fieldBackgroundDisabledMixin : fieldBackgroundHoverMixin,
|
|
209
|
+
"relative flex items-center",
|
|
210
|
+
className,
|
|
211
|
+
inputClassName
|
|
212
|
+
)}
|
|
213
|
+
>
|
|
214
|
+
<div className="flex items-center justify-between w-full gap-1">
|
|
215
|
+
<div className="flex-grow min-w-0 truncate">
|
|
216
|
+
{displayLabel ?? (
|
|
217
|
+
<span className="text-sm text-surface-accent-500 dark:text-surface-accent-400">
|
|
218
|
+
{placeholder}
|
|
219
|
+
</span>
|
|
220
|
+
)}
|
|
221
|
+
</div>
|
|
222
|
+
<div className={cls("flex-shrink-0 flex items-center")}>
|
|
223
|
+
<KeyboardArrowDownIcon
|
|
224
|
+
size={size === "large" ? "medium" : "small"}
|
|
225
|
+
className={cls("transition", isPopoverOpen ? "rotate-180" : "")}
|
|
226
|
+
/>
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
</button>
|
|
230
|
+
</PopoverPrimitive.Trigger>
|
|
231
|
+
<PopoverPrimitive.Portal container={finalContainer}>
|
|
232
|
+
<PopoverPrimitive.Content
|
|
233
|
+
className={cls(
|
|
234
|
+
"z-50 overflow-hidden border bg-white dark:bg-surface-900 rounded-lg",
|
|
235
|
+
defaultBorderMixin
|
|
236
|
+
)}
|
|
237
|
+
align="start"
|
|
238
|
+
sideOffset={4}
|
|
239
|
+
style={{ width: "var(--radix-popover-trigger-width)", minWidth: 180 }}
|
|
240
|
+
onEscapeKeyDown={() => onPopoverOpenChange(false)}
|
|
241
|
+
>
|
|
242
|
+
<CommandPrimitive shouldFilter={true}>
|
|
243
|
+
<div className="flex flex-row items-center">
|
|
244
|
+
<CommandPrimitive.Input
|
|
245
|
+
ref={inputRef}
|
|
246
|
+
value={search}
|
|
247
|
+
onValueChange={setSearch}
|
|
248
|
+
className={cls(
|
|
249
|
+
focusedDisabled,
|
|
250
|
+
"bg-transparent outline-none flex-1 h-full w-full text-surface-accent-900 dark:text-white",
|
|
251
|
+
{
|
|
252
|
+
"m-2 text-xs": size === "smallest" || size === "small",
|
|
253
|
+
"m-3 text-sm": size === "medium" || size === "large",
|
|
254
|
+
},
|
|
255
|
+
)}
|
|
256
|
+
placeholder="Search..."
|
|
257
|
+
onKeyDown={handleInputKeyDown}
|
|
258
|
+
/>
|
|
259
|
+
</div>
|
|
260
|
+
<Separator orientation="horizontal" className="my-0" />
|
|
261
|
+
<CommandPrimitive.List>
|
|
262
|
+
{allowCustomValues && search.trim() && !itemsMap.has(search.trim()) ? (
|
|
263
|
+
<CommandPrimitive.Empty
|
|
264
|
+
className="px-3 py-2 text-xs cursor-pointer text-primary hover:bg-surface-accent-100 dark:hover:bg-surface-accent-800"
|
|
265
|
+
onClick={() => handleSelect(search.trim())}
|
|
266
|
+
>
|
|
267
|
+
Use “{search.trim()}”
|
|
268
|
+
</CommandPrimitive.Empty>
|
|
269
|
+
) : (
|
|
270
|
+
<CommandPrimitive.Empty className="px-3 py-2 text-xs text-text-secondary dark:text-text-secondary-dark">
|
|
271
|
+
No results found.
|
|
272
|
+
</CommandPrimitive.Empty>
|
|
273
|
+
)}
|
|
274
|
+
<CommandPrimitive.Group>
|
|
275
|
+
{React.Children.map(children, (child) => {
|
|
276
|
+
if (!React.isValidElement<SearchableSelectItemProps>(child)) return child;
|
|
277
|
+
const itemValue = child.props.value;
|
|
278
|
+
const isSelected = String(value) === String(itemValue);
|
|
279
|
+
return (
|
|
280
|
+
<CommandPrimitive.Item
|
|
281
|
+
key={String(itemValue)}
|
|
282
|
+
value={String(itemValue)}
|
|
283
|
+
onMouseDown={(e) => {
|
|
284
|
+
e.preventDefault();
|
|
285
|
+
e.stopPropagation();
|
|
286
|
+
}}
|
|
287
|
+
onSelect={() => handleSelect(String(itemValue))}
|
|
288
|
+
className={cls(
|
|
289
|
+
"flex flex-row items-center gap-1.5",
|
|
290
|
+
isSelected ? "bg-surface-accent-200 dark:bg-surface-accent-950" : "",
|
|
291
|
+
"cursor-pointer",
|
|
292
|
+
"m-0.5",
|
|
293
|
+
"ring-offset-transparent",
|
|
294
|
+
"p-1.5 rounded",
|
|
295
|
+
"aria-[selected=true]:outline-none",
|
|
296
|
+
"aria-[selected=true]:bg-surface-accent-100 aria-[selected=true]:dark:bg-surface-accent-900",
|
|
297
|
+
"text-surface-accent-700 dark:text-surface-accent-300",
|
|
298
|
+
child.props.className
|
|
299
|
+
)}
|
|
300
|
+
>
|
|
301
|
+
<div className={cls(
|
|
302
|
+
"w-4 h-4 flex items-center justify-center flex-shrink-0",
|
|
303
|
+
isSelected ? "text-primary" : "text-transparent",
|
|
304
|
+
)}>
|
|
305
|
+
{isSelected && <CheckIcon size={14} />}
|
|
306
|
+
</div>
|
|
307
|
+
{child.props.children ?? child.props.value}
|
|
308
|
+
</CommandPrimitive.Item>
|
|
309
|
+
);
|
|
310
|
+
})}
|
|
311
|
+
</CommandPrimitive.Group>
|
|
312
|
+
</CommandPrimitive.List>
|
|
313
|
+
</CommandPrimitive>
|
|
314
|
+
</PopoverPrimitive.Content>
|
|
315
|
+
</PopoverPrimitive.Portal>
|
|
316
|
+
</PopoverPrimitive.Root>
|
|
317
|
+
</div>
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
SearchableSelect.displayName = "SearchableSelect";
|
|
323
|
+
|
|
324
|
+
// ─── Item ───────────────────────────────────────────────────────────────────
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* A single option inside a SearchableSelect.
|
|
328
|
+
* The `value` prop is the string value that gets selected.
|
|
329
|
+
* The `children` is what's displayed in the dropdown.
|
|
330
|
+
* This component is not rendered directly — SearchableSelect reads its props.
|
|
331
|
+
*/
|
|
332
|
+
export function SearchableSelectItem(_props: SearchableSelectItemProps) {
|
|
333
|
+
// Rendered by SearchableSelect, not by React
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
@@ -14,8 +14,8 @@ export function Skeleton({
|
|
|
14
14
|
}: SkeletonProps) {
|
|
15
15
|
return <span
|
|
16
16
|
style={{
|
|
17
|
-
width: width ? `${width}px` :
|
|
18
|
-
height: height ? `${height}px` :
|
|
17
|
+
width: width !== undefined ? `${width}px` : undefined,
|
|
18
|
+
height: height !== undefined ? `${height}px` : undefined
|
|
19
19
|
}}
|
|
20
20
|
className={
|
|
21
21
|
cls(
|
|
@@ -23,6 +23,8 @@ export function Skeleton({
|
|
|
23
23
|
"bg-surface-accent-200 dark:bg-surface-accent-800 rounded-md",
|
|
24
24
|
"animate-pulse",
|
|
25
25
|
"max-w-full max-h-full",
|
|
26
|
+
width === undefined ? "w-full" : "",
|
|
27
|
+
height === undefined ? "h-3" : "",
|
|
26
28
|
className)
|
|
27
29
|
}/>;
|
|
28
30
|
}
|
package/src/components/Tabs.tsx
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
|
-
import React, { useRef, useState, useEffect } from "react";
|
|
1
|
+
import React, { createContext, useContext, useRef, useState, useEffect } from "react";
|
|
2
2
|
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
|
3
3
|
import { cls } from "../util";
|
|
4
4
|
import { defaultBorderMixin } from "../styles";
|
|
5
5
|
import { IconButton } from "./IconButton";
|
|
6
6
|
import { ChevronLeftIcon, ChevronRightIcon } from "../icons";
|
|
7
7
|
|
|
8
|
+
type TabsMode = "primary" | "secondary";
|
|
9
|
+
const TabsModeContext = createContext<TabsMode>("primary");
|
|
10
|
+
|
|
8
11
|
export type TabsProps = {
|
|
9
12
|
value: string,
|
|
10
13
|
children: React.ReactNode,
|
|
11
14
|
innerClassName?: string,
|
|
12
15
|
className?: string,
|
|
13
|
-
onValueChange: (value: string) => void
|
|
16
|
+
onValueChange: (value: string) => void,
|
|
17
|
+
/**
|
|
18
|
+
* "primary" renders the default pill-style tabs.
|
|
19
|
+
* "secondary" renders underline-style tabs suitable for inner/nested panels.
|
|
20
|
+
*/
|
|
21
|
+
mode?: TabsMode
|
|
14
22
|
};
|
|
15
23
|
|
|
16
24
|
export function Tabs({
|
|
@@ -18,7 +26,8 @@ export function Tabs({
|
|
|
18
26
|
onValueChange,
|
|
19
27
|
className,
|
|
20
28
|
innerClassName,
|
|
21
|
-
children
|
|
29
|
+
children,
|
|
30
|
+
mode = "primary"
|
|
22
31
|
}: TabsProps) {
|
|
23
32
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
|
24
33
|
const [showLeftScroll, setShowLeftScroll] = useState(false);
|
|
@@ -67,7 +76,8 @@ export function Tabs({
|
|
|
67
76
|
}
|
|
68
77
|
};
|
|
69
78
|
|
|
70
|
-
return <
|
|
79
|
+
return <TabsModeContext.Provider value={mode}>
|
|
80
|
+
<TabsPrimitive.Root value={value} onValueChange={onValueChange} className={cls("flex flex-row items-center min-w-0", className)}>
|
|
71
81
|
{isScrollable && (
|
|
72
82
|
<button
|
|
73
83
|
type="button"
|
|
@@ -78,7 +88,9 @@ export function Tabs({
|
|
|
78
88
|
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-surface-400",
|
|
79
89
|
"disabled:pointer-events-none disabled:opacity-0",
|
|
80
90
|
"text-surface-600 dark:text-surface-400 hover:bg-surface-200 dark:hover:bg-surface-800",
|
|
81
|
-
"mr-1 bg-surface-50 dark:bg-surface-900 border",
|
|
91
|
+
mode === "primary" && "mr-1 bg-surface-50 dark:bg-surface-900 border",
|
|
92
|
+
mode === "primary" && defaultBorderMixin,
|
|
93
|
+
mode === "secondary" && "mr-1"
|
|
82
94
|
)}
|
|
83
95
|
>
|
|
84
96
|
<ChevronLeftIcon size="small" />
|
|
@@ -91,10 +103,10 @@ export function Tabs({
|
|
|
91
103
|
style={{ scrollbarWidth: "none", msOverflowStyle: "none" }}
|
|
92
104
|
>
|
|
93
105
|
<TabsPrimitive.List className={cls(
|
|
94
|
-
"border",
|
|
95
|
-
defaultBorderMixin,
|
|
96
|
-
"gap-2",
|
|
97
|
-
"inline-flex h-
|
|
106
|
+
mode === "primary" && "border",
|
|
107
|
+
mode === "primary" && defaultBorderMixin,
|
|
108
|
+
mode === "primary" && "gap-2 inline-flex h-10 items-center justify-center rounded-md bg-surface-50 p-1 text-surface-600 dark:bg-surface-900 dark:text-surface-400",
|
|
109
|
+
mode === "secondary" && "gap-1 inline-flex h-9 items-center text-surface-500 dark:text-surface-400",
|
|
98
110
|
innerClassName)
|
|
99
111
|
}>
|
|
100
112
|
{children}
|
|
@@ -110,13 +122,16 @@ export function Tabs({
|
|
|
110
122
|
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-surface-400",
|
|
111
123
|
"disabled:pointer-events-none disabled:opacity-0",
|
|
112
124
|
"text-surface-600 dark:text-surface-400 hover:bg-surface-200 dark:hover:bg-surface-800",
|
|
113
|
-
"ml-1 bg-surface-50 dark:bg-surface-900 border",
|
|
125
|
+
mode === "primary" && "ml-1 bg-surface-50 dark:bg-surface-900 border",
|
|
126
|
+
mode === "primary" && defaultBorderMixin,
|
|
127
|
+
mode === "secondary" && "ml-1"
|
|
114
128
|
)}
|
|
115
129
|
>
|
|
116
130
|
<ChevronRightIcon size="small" />
|
|
117
131
|
</button>
|
|
118
132
|
)}
|
|
119
133
|
</TabsPrimitive.Root>
|
|
134
|
+
</TabsModeContext.Provider>
|
|
120
135
|
}
|
|
121
136
|
|
|
122
137
|
export type TabProps = {
|
|
@@ -134,15 +149,28 @@ export function Tab({
|
|
|
134
149
|
children,
|
|
135
150
|
disabled
|
|
136
151
|
}: TabProps) {
|
|
152
|
+
const mode = useContext(TabsModeContext);
|
|
153
|
+
|
|
154
|
+
const primaryClasses = cls(
|
|
155
|
+
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-white transition-all",
|
|
156
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-surface-400 focus-visible:ring-offset-2",
|
|
157
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
158
|
+
"data-[state=active]:bg-white data-[state=active]:text-surface-900 dark:data-[state=active]:bg-surface-950 dark:data-[state=active]:text-surface-50",
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
const secondaryClasses = cls(
|
|
162
|
+
"inline-flex items-center justify-center whitespace-nowrap px-3 py-1.5 text-sm font-medium transition-all",
|
|
163
|
+
"border-b-2 border-transparent -mb-px",
|
|
164
|
+
"focus-visible:outline-none",
|
|
165
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
166
|
+
"hover:text-surface-700 dark:hover:text-surface-300",
|
|
167
|
+
"data-[state=active]:border-b-primary data-[state=active]:text-primary dark:data-[state=active]:border-b-primary dark:data-[state=active]:text-primary-dark",
|
|
168
|
+
);
|
|
169
|
+
|
|
137
170
|
return <TabsPrimitive.Trigger value={value}
|
|
138
171
|
disabled={disabled}
|
|
139
172
|
className={cls(
|
|
140
|
-
|
|
141
|
-
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-surface-400 focus-visible:ring-offset-2",
|
|
142
|
-
"disabled:pointer-events-none disabled:opacity-50",
|
|
143
|
-
"data-[state=active]:bg-white data-[state=active]:text-surface-900 dark:data-[state=active]:bg-surface-950 dark:data-[state=active]:text-surface-50",
|
|
144
|
-
// "data-[state=active]:border",
|
|
145
|
-
// defaultBorderMixin,
|
|
173
|
+
mode === "secondary" ? secondaryClasses : primaryClasses,
|
|
146
174
|
className,
|
|
147
175
|
innerClassName)}>
|
|
148
176
|
{children}
|