@firecms/ui 3.0.1 → 3.1.0-canary.02232f4
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/README.md +9 -7
- package/dist/components/BooleanSwitchWithLabel.d.ts +2 -1
- package/dist/components/Card.d.ts +1 -1
- package/dist/components/Chip.d.ts +1 -1
- package/dist/components/ColorPicker.d.ts +30 -0
- package/dist/components/DateTimeField.d.ts +7 -0
- package/dist/components/Dialog.d.ts +2 -1
- package/dist/components/FileUpload.d.ts +1 -1
- package/dist/components/Menu.d.ts +2 -1
- package/dist/components/Menubar.d.ts +2 -1
- package/dist/components/MultiSelect.d.ts +2 -1
- package/dist/components/ResizablePanels.d.ts +16 -0
- package/dist/components/SearchBar.d.ts +11 -1
- package/dist/components/SearchableSelect.d.ts +48 -0
- package/dist/components/Select.d.ts +2 -1
- package/dist/components/Sheet.d.ts +1 -0
- package/dist/components/Tabs.d.ts +8 -1
- package/dist/components/ToggleButtonGroup.d.ts +30 -0
- package/dist/components/Tooltip.d.ts +18 -2
- package/dist/components/index.d.ts +4 -0
- package/dist/hooks/PortalContainerContext.d.ts +31 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/useOutsideAlerter.d.ts +1 -1
- 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.css +57 -6
- package/dist/index.es.js +2846 -1165
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +2846 -1165
- package/dist/index.umd.js.map +1 -1
- package/dist/styles.d.ts +11 -11
- package/package.json +7 -7
- package/src/components/BooleanSwitch.tsx +3 -3
- package/src/components/BooleanSwitchWithLabel.tsx +4 -0
- package/src/components/Button.tsx +6 -5
- package/src/components/Card.tsx +7 -7
- package/src/components/Checkbox.tsx +1 -1
- package/src/components/Chip.tsx +4 -3
- package/src/components/ColorPicker.tsx +134 -0
- package/src/components/DateTimeField.tsx +129 -35
- package/src/components/DebouncedTextField.tsx +3 -3
- package/src/components/Dialog.tsx +25 -16
- package/src/components/DialogActions.tsx +1 -1
- package/src/components/ExpandablePanel.tsx +1 -1
- package/src/components/FileUpload.tsx +25 -24
- package/src/components/IconButton.tsx +3 -2
- package/src/components/Menu.tsx +44 -30
- package/src/components/Menubar.tsx +14 -3
- package/src/components/MultiSelect.tsx +113 -77
- package/src/components/Popover.tsx +11 -3
- package/src/components/ResizablePanels.tsx +181 -0
- package/src/components/SearchBar.tsx +37 -19
- package/src/components/SearchableSelect.tsx +335 -0
- package/src/components/Select.tsx +86 -73
- package/src/components/Separator.tsx +2 -2
- package/src/components/Sheet.tsx +12 -3
- package/src/components/Skeleton.tsx +4 -2
- package/src/components/Slider.tsx +4 -4
- package/src/components/Table.tsx +1 -1
- package/src/components/Tabs.tsx +150 -37
- package/src/components/TextField.tsx +19 -8
- package/src/components/TextareaAutosize.tsx +77 -212
- package/src/components/ToggleButtonGroup.tsx +67 -0
- package/src/components/Tooltip.tsx +16 -8
- package/src/components/index.tsx +4 -0
- package/src/hooks/PortalContainerContext.tsx +48 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useInjectStyles.tsx +12 -3
- package/src/hooks/useOutsideAlerter.tsx +1 -1
- package/src/icons/FirestoreIcon.tsx +47 -0
- package/src/icons/components/DatabaseIcon.tsx +10 -0
- package/src/icons/index.ts +2 -0
- package/src/index.css +57 -6
- package/src/styles.ts +11 -11
- package/src/util/cls.ts +1 -1
- package/tailwind.config.js +2 -3
|
@@ -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
|
}
|
|
@@ -37,8 +37,8 @@ function SliderThumb(props: {
|
|
|
37
37
|
"border-surface-accent-300 bg-surface-accent-300 dark:border-surface-700 dark:bg-surface-700": props.props.disabled
|
|
38
38
|
},
|
|
39
39
|
props.classes,
|
|
40
|
-
"focus-visible:ring-4 focus-visible:ring-primary focus-visible:ring-opacity-50",
|
|
41
|
-
"hover:ring-4 hover:ring-primary hover:ring-opacity-25",
|
|
40
|
+
"focus-visible:ring-4 focus-visible:ring-primary focus-visible:ring-opacity-50 focus-visible:ring-primary/50",
|
|
41
|
+
"hover:ring-4 hover:ring-primary hover:ring-opacity-25 hover:ring-primary/25",
|
|
42
42
|
"block rounded-full transition-colors focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50")}
|
|
43
43
|
|
|
44
44
|
/>
|
|
@@ -48,7 +48,7 @@ function SliderThumb(props: {
|
|
|
48
48
|
className={cls(
|
|
49
49
|
"TooltipContent",
|
|
50
50
|
"max-w-lg leading-relaxed",
|
|
51
|
-
"z-50 rounded px-3 py-2 text-xs leading-none bg-surface-accent-700 dark:bg-surface-accent-800 bg-opacity-90 font-medium text-surface-accent-50 shadow-2xl select-none duration-400 ease-in transform opacity-100",
|
|
51
|
+
"z-50 rounded px-3 py-2 text-xs leading-none bg-surface-accent-700 dark:bg-surface-accent-800 bg-opacity-90 bg-surface-accent-700/90 dark:bg-surface-accent-800/90 font-medium text-surface-accent-50 shadow-2xl select-none duration-400 ease-in transform opacity-100",
|
|
52
52
|
)}>
|
|
53
53
|
{props.props.value?.[props.index]}
|
|
54
54
|
</TooltipPrimitive.Content>
|
|
@@ -84,7 +84,7 @@ const Slider = React.forwardRef<
|
|
|
84
84
|
>
|
|
85
85
|
<SliderPrimitive.Track
|
|
86
86
|
style={{ height: size === "small" ? 4 : 8 }}
|
|
87
|
-
className={"relative w-full grow overflow-hidden rounded-full bg-surface-accent-300 bg-opacity-40 dark:bg-surface-700 dark:bg-opacity-40"}>
|
|
87
|
+
className={"relative w-full grow overflow-hidden rounded-full bg-surface-accent-300 bg-opacity-40 bg-surface-accent-300/40 dark:bg-surface-700 dark:bg-opacity-40 dark:bg-surface-700/40"}>
|
|
88
88
|
|
|
89
89
|
<SliderPrimitive.Range
|
|
90
90
|
className={cls("absolute h-full", {
|
package/src/components/Table.tsx
CHANGED
|
@@ -36,7 +36,7 @@ export const TableBody = React.memo(({
|
|
|
36
36
|
...rest
|
|
37
37
|
}: TableBodyProps) => (
|
|
38
38
|
<tbody
|
|
39
|
-
className={cls("bg-white dark:bg-surface-950 text-sm divide-y divide-surface-100 dark:divide-surface-700 dark:divide-opacity-70", className)}
|
|
39
|
+
className={cls("bg-white dark:bg-surface-950 text-sm divide-y divide-surface-100 dark:divide-surface-700 dark:divide-opacity-70 dark:divide-surface-700/70", className)}
|
|
40
40
|
{...rest}
|
|
41
41
|
>
|
|
42
42
|
{children}
|
package/src/components/Tabs.tsx
CHANGED
|
@@ -1,32 +1,137 @@
|
|
|
1
|
-
import React 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
|
+
import { defaultBorderMixin } from "../styles";
|
|
5
|
+
import { IconButton } from "./IconButton";
|
|
6
|
+
import { ChevronLeftIcon, ChevronRightIcon } from "../icons";
|
|
7
|
+
|
|
8
|
+
type TabsMode = "primary" | "secondary";
|
|
9
|
+
const TabsModeContext = createContext<TabsMode>("primary");
|
|
4
10
|
|
|
5
11
|
export type TabsProps = {
|
|
6
12
|
value: string,
|
|
7
13
|
children: React.ReactNode,
|
|
8
14
|
innerClassName?: string,
|
|
9
15
|
className?: string,
|
|
10
|
-
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
|
|
11
22
|
};
|
|
12
23
|
|
|
13
24
|
export function Tabs({
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
25
|
+
value,
|
|
26
|
+
onValueChange,
|
|
27
|
+
className,
|
|
28
|
+
innerClassName,
|
|
29
|
+
children,
|
|
30
|
+
mode = "primary"
|
|
31
|
+
}: TabsProps) {
|
|
32
|
+
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
|
33
|
+
const [showLeftScroll, setShowLeftScroll] = useState(false);
|
|
34
|
+
const [showRightScroll, setShowRightScroll] = useState(false);
|
|
35
|
+
const [isScrollable, setIsScrollable] = useState(false);
|
|
36
|
+
|
|
37
|
+
const checkScroll = () => {
|
|
38
|
+
if (scrollContainerRef.current) {
|
|
39
|
+
const { scrollLeft, scrollWidth, clientWidth } = scrollContainerRef.current;
|
|
40
|
+
setShowLeftScroll(scrollLeft > 0);
|
|
41
|
+
setShowRightScroll(Math.ceil(scrollLeft + clientWidth) < scrollWidth);
|
|
42
|
+
setIsScrollable(scrollWidth > clientWidth);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
checkScroll();
|
|
48
|
+
window.addEventListener("resize", checkScroll);
|
|
49
|
+
|
|
50
|
+
let observer: ResizeObserver;
|
|
51
|
+
if (scrollContainerRef.current) {
|
|
52
|
+
observer = new ResizeObserver(checkScroll);
|
|
53
|
+
observer.observe(scrollContainerRef.current);
|
|
54
|
+
if (scrollContainerRef.current.firstElementChild) {
|
|
55
|
+
observer.observe(scrollContainerRef.current.firstElementChild);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return () => {
|
|
60
|
+
window.removeEventListener("resize", checkScroll);
|
|
61
|
+
observer?.disconnect();
|
|
62
|
+
};
|
|
63
|
+
}, [children]);
|
|
64
|
+
|
|
65
|
+
const scroll = (direction: "left" | "right") => {
|
|
66
|
+
if (scrollContainerRef.current) {
|
|
67
|
+
const container = scrollContainerRef.current;
|
|
68
|
+
const scrollAmount = Math.max(container.clientWidth / 2, 200);
|
|
69
|
+
const targetScroll = container.scrollLeft + (direction === "left" ? -scrollAmount : scrollAmount);
|
|
20
70
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
|
|
71
|
+
container.scrollTo({
|
|
72
|
+
left: targetScroll,
|
|
73
|
+
behavior: "smooth"
|
|
74
|
+
});
|
|
75
|
+
// checkScroll will be called by onScroll event
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return <TabsModeContext.Provider value={mode}>
|
|
80
|
+
<TabsPrimitive.Root value={value} onValueChange={onValueChange} className={cls("flex flex-row items-center min-w-0", className)}>
|
|
81
|
+
{isScrollable && (
|
|
82
|
+
<button
|
|
83
|
+
type="button"
|
|
84
|
+
disabled={!showLeftScroll}
|
|
85
|
+
onClick={() => scroll("left")}
|
|
86
|
+
className={cls(
|
|
87
|
+
"flex-shrink-0 z-10 flex items-center justify-center rounded-md px-0.5 py-1.5 transition-all h-10 w-6",
|
|
88
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-surface-400",
|
|
89
|
+
"disabled:pointer-events-none disabled:opacity-0",
|
|
90
|
+
"text-surface-600 dark:text-surface-400 hover:bg-surface-200 dark:hover:bg-surface-800",
|
|
91
|
+
mode === "primary" && "mr-1 bg-surface-50 dark:bg-surface-900 border",
|
|
92
|
+
mode === "primary" && defaultBorderMixin,
|
|
93
|
+
mode === "secondary" && "mr-1"
|
|
94
|
+
)}
|
|
95
|
+
>
|
|
96
|
+
<ChevronLeftIcon size="small" />
|
|
97
|
+
</button>
|
|
98
|
+
)}
|
|
99
|
+
<div
|
|
100
|
+
ref={scrollContainerRef}
|
|
101
|
+
className="flex-1 overflow-x-auto no-scrollbar min-w-0"
|
|
102
|
+
onScroll={checkScroll}
|
|
103
|
+
style={{ scrollbarWidth: "none", msOverflowStyle: "none" }}
|
|
104
|
+
>
|
|
105
|
+
<TabsPrimitive.List className={cls(
|
|
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",
|
|
110
|
+
innerClassName)
|
|
111
|
+
}>
|
|
112
|
+
{children}
|
|
113
|
+
</TabsPrimitive.List>
|
|
114
|
+
</div>
|
|
115
|
+
{isScrollable && (
|
|
116
|
+
<button
|
|
117
|
+
type="button"
|
|
118
|
+
disabled={!showRightScroll}
|
|
119
|
+
onClick={() => scroll("right")}
|
|
120
|
+
className={cls(
|
|
121
|
+
"flex-shrink-0 z-10 flex items-center justify-center rounded-md px-0.5 py-1.5 transition-all h-10 w-6",
|
|
122
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-surface-400",
|
|
123
|
+
"disabled:pointer-events-none disabled:opacity-0",
|
|
124
|
+
"text-surface-600 dark:text-surface-400 hover:bg-surface-200 dark:hover:bg-surface-800",
|
|
125
|
+
mode === "primary" && "ml-1 bg-surface-50 dark:bg-surface-900 border",
|
|
126
|
+
mode === "primary" && defaultBorderMixin,
|
|
127
|
+
mode === "secondary" && "ml-1"
|
|
128
|
+
)}
|
|
129
|
+
>
|
|
130
|
+
<ChevronRightIcon size="small" />
|
|
131
|
+
</button>
|
|
132
|
+
)}
|
|
29
133
|
</TabsPrimitive.Root>
|
|
134
|
+
</TabsModeContext.Provider>
|
|
30
135
|
}
|
|
31
136
|
|
|
32
137
|
export type TabProps = {
|
|
@@ -38,28 +143,36 @@ export type TabProps = {
|
|
|
38
143
|
};
|
|
39
144
|
|
|
40
145
|
export function Tab({
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
146
|
+
value,
|
|
147
|
+
className,
|
|
148
|
+
innerClassName,
|
|
149
|
+
children,
|
|
150
|
+
disabled
|
|
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
|
+
|
|
47
170
|
return <TabsPrimitive.Trigger value={value}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
disabled
|
|
53
|
-
? "text-surface-accent-400 dark:text-surface-accent-500"
|
|
54
|
-
: cls("text-surface-accent-700 dark:text-surface-accent-300",
|
|
55
|
-
"data-[state=active]:text-surface-accent-900 data-[state=active]:dark:text-white",
|
|
56
|
-
"hover:text-surface-accent-800 dark:hover:text-surface-accent-200"),
|
|
57
|
-
className)}>
|
|
58
|
-
<div className={cls("line-clamp-1",
|
|
59
|
-
"uppercase inline-block p-2 px-4 rounded",
|
|
60
|
-
"hover:bg-surface-accent-200 hover:bg-opacity-75 dark:hover:bg-surface-accent-800",
|
|
171
|
+
disabled={disabled}
|
|
172
|
+
className={cls(
|
|
173
|
+
mode === "secondary" ? secondaryClasses : primaryClasses,
|
|
174
|
+
className,
|
|
61
175
|
innerClassName)}>
|
|
62
|
-
|
|
63
|
-
</div>
|
|
176
|
+
{children}
|
|
64
177
|
</TabsPrimitive.Trigger>;
|
|
65
178
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import React, { ForwardedRef, forwardRef, useEffect, useRef } from "react";
|
|
3
3
|
|
|
4
|
-
import { TextareaAutosize } from "./TextareaAutosize";
|
|
5
4
|
import {
|
|
6
5
|
fieldBackgroundDisabledMixin,
|
|
7
6
|
fieldBackgroundHoverMixin,
|
|
@@ -85,10 +84,16 @@ export const TextField = forwardRef<HTMLDivElement, TextFieldProps<string | numb
|
|
|
85
84
|
|
|
86
85
|
const inputRef = inputRefProp ?? useRef(null);
|
|
87
86
|
|
|
88
|
-
|
|
89
|
-
const [focused, setFocused] = React.useState(document.activeElement === inputRef.current);
|
|
87
|
+
const [focused, setFocused] = React.useState(false);
|
|
90
88
|
const hasValue = value !== undefined && value !== null && value !== "";
|
|
91
89
|
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
// @ts-ignore
|
|
92
|
+
if (inputRef.current && document.activeElement === inputRef.current) {
|
|
93
|
+
setFocused(true);
|
|
94
|
+
}
|
|
95
|
+
}, []);
|
|
96
|
+
|
|
92
97
|
useEffect(() => {
|
|
93
98
|
if (type !== "number") return;
|
|
94
99
|
const handleWheel = (event: any) => {
|
|
@@ -105,19 +110,21 @@ export const TextField = forwardRef<HTMLDivElement, TextFieldProps<string | numb
|
|
|
105
110
|
}, [inputRef, type]);
|
|
106
111
|
|
|
107
112
|
const input = multiline ? (
|
|
108
|
-
<
|
|
113
|
+
<textarea
|
|
109
114
|
{...(inputProps as any)}
|
|
110
115
|
ref={inputRef}
|
|
111
116
|
placeholder={focused || hasValue || !label ? placeholder : undefined}
|
|
112
117
|
autoFocus={autoFocus}
|
|
113
|
-
|
|
114
|
-
maxRows={maxRows}
|
|
118
|
+
rows={typeof minRows === "string" ? parseInt(minRows) : (minRows ?? 3)}
|
|
115
119
|
value={value ?? ""}
|
|
116
120
|
onChange={onChange}
|
|
121
|
+
onFocus={() => setFocused(true)}
|
|
122
|
+
onBlur={() => setFocused(false)}
|
|
117
123
|
style={inputStyle}
|
|
118
124
|
className={cls(
|
|
119
125
|
invisible ? focusedInvisibleMixin : "",
|
|
120
|
-
"rounded-md resize-none w-full outline-none
|
|
126
|
+
"rounded-md resize-none w-full outline-none text-base bg-transparent min-h-[64px] px-3",
|
|
127
|
+
label ? "pt-8 pb-2" : "py-2",
|
|
121
128
|
disabled && "outline-none opacity-50 text-surface-accent-600 dark:text-surface-accent-500",
|
|
122
129
|
inputClassName
|
|
123
130
|
)}
|
|
@@ -144,7 +151,11 @@ export const TextField = forwardRef<HTMLDivElement, TextFieldProps<string | numb
|
|
|
144
151
|
? size === "large"
|
|
145
152
|
? "pt-8 pb-2"
|
|
146
153
|
: "pt-4 pb-2"
|
|
147
|
-
: "
|
|
154
|
+
: size === "smallest"
|
|
155
|
+
? "py-0.5"
|
|
156
|
+
: size === "small"
|
|
157
|
+
? "py-1"
|
|
158
|
+
: "py-2",
|
|
148
159
|
endAdornment ? "pr-12" : "pr-3",
|
|
149
160
|
disabled &&
|
|
150
161
|
"outline-none opacity-65 dark:opacity-60 text-surface-accent-800 dark:text-white",
|