@hilum/ui 3.4.0 → 3.5.1
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 +4 -0
- package/dist/{chunk-MAHPAP65.js → chunk-RI6QJR2L.js} +6 -3
- package/dist/chunk-RI6QJR2L.js.map +1 -0
- package/dist/index.d.ts +789 -115
- package/dist/index.js +10404 -1099
- package/dist/index.js.map +1 -1
- package/dist/tokens.css +17 -0
- package/dist/tokens.d.ts +3 -0
- package/dist/tokens.js +1 -1
- package/llms.txt +18 -4
- package/package.json +19 -1
- package/src/components/accordion.tsx +746 -58
- package/src/components/alert-dialog.tsx +11 -5
- package/src/components/ask-user-questions.tsx +1366 -0
- package/src/components/badge.tsx +105 -24
- package/src/components/button.tsx +207 -38
- package/src/components/card.tsx +3 -2
- package/src/components/chat-message.tsx +116 -0
- package/src/components/checkbox-group.tsx +403 -0
- package/src/components/checkbox.tsx +6 -3
- package/src/components/color-input.tsx +3 -1
- package/src/components/color-picker.tsx +1992 -80
- package/src/components/dialog.tsx +141 -81
- package/src/components/dropdown-menu.tsx +21 -13
- package/src/components/dropdown.tsx +245 -0
- package/src/components/file-thumbnail.tsx +156 -0
- package/src/components/hover-card.tsx +2 -0
- package/src/components/input-copy.tsx +307 -0
- package/src/components/input-group.tsx +303 -108
- package/src/components/input-message.tsx +883 -0
- package/src/components/input.tsx +6 -3
- package/src/components/menu-item.tsx +156 -0
- package/src/components/mobile-drawer.tsx +50 -0
- package/src/components/nav-item.tsx +43 -0
- package/src/components/nav-menu.tsx +47 -0
- package/src/components/popover.tsx +3 -1
- package/src/components/radio-group.tsx +381 -35
- package/src/components/scroll-area.tsx +9 -2
- package/src/components/select.tsx +708 -121
- package/src/components/sheet.tsx +15 -6
- package/src/components/slider.tsx +1583 -20
- package/src/components/status-badge.tsx +2 -0
- package/src/components/switch.tsx +245 -28
- package/src/components/table.tsx +191 -77
- package/src/components/tabs-subtle.tsx +411 -0
- package/src/components/tabs.tsx +459 -49
- package/src/components/textarea.tsx +7 -3
- package/src/components/thinking-indicator.tsx +106 -0
- package/src/components/thinking-steps.tsx +382 -0
- package/src/components/tooltip.tsx +158 -33
- package/src/hooks/use-merge-split.tsx +359 -0
- package/src/hooks/use-proximity-hover.ts +208 -0
- package/src/index.ts +14 -0
- package/src/lib/elevated.tsx +52 -0
- package/src/lib/font-weight.ts +18 -0
- package/src/lib/icon-context.tsx +98 -0
- package/src/lib/icon-map.tsx +669 -0
- package/src/lib/interaction.ts +102 -0
- package/src/lib/scroll-fade.tsx +241 -0
- package/src/lib/shape-context.tsx +121 -0
- package/src/lib/springs.ts +30 -0
- package/src/lib/surface-classes.ts +43 -0
- package/src/lib/surface-context.tsx +17 -0
- package/src/tokens/tokens.ts +3 -0
- package/dist/chunk-MAHPAP65.js.map +0 -1
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
useId,
|
|
5
|
+
useRef,
|
|
6
|
+
useState,
|
|
7
|
+
useCallback,
|
|
8
|
+
useEffect,
|
|
9
|
+
createContext,
|
|
10
|
+
useContext,
|
|
11
|
+
forwardRef,
|
|
12
|
+
type ReactNode,
|
|
13
|
+
type HTMLAttributes,
|
|
14
|
+
} from "react";
|
|
15
|
+
import { motion, AnimatePresence } from "framer-motion";
|
|
16
|
+
import type { IconComponent } from "../lib/icon-context";
|
|
17
|
+
import { cn } from "../lib/utils";
|
|
18
|
+
import { spring } from "../lib/springs";
|
|
19
|
+
import { fontWeights } from "../lib/font-weight";
|
|
20
|
+
import { useShape } from "../lib/shape-context";
|
|
21
|
+
import { useProximityHover } from "../hooks/use-proximity-hover";
|
|
22
|
+
|
|
23
|
+
interface TabsSubtleContextValue {
|
|
24
|
+
registerTab: (index: number, element: HTMLElement | null) => void;
|
|
25
|
+
hoveredIndex: number | null;
|
|
26
|
+
selectedIndex: number;
|
|
27
|
+
onSelect: (index: number) => void;
|
|
28
|
+
idPrefix: string;
|
|
29
|
+
activeLabel: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const TabsSubtleContext = createContext<TabsSubtleContextValue | null>(null);
|
|
33
|
+
|
|
34
|
+
function useTabsSubtle() {
|
|
35
|
+
const ctx = useContext(TabsSubtleContext);
|
|
36
|
+
if (!ctx) throw new Error("useTabsSubtle must be used within a TabsSubtle");
|
|
37
|
+
return ctx;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface TabsSubtleProps extends Omit<HTMLAttributes<HTMLDivElement>, "onSelect"> {
|
|
41
|
+
children: ReactNode;
|
|
42
|
+
selectedIndex: number;
|
|
43
|
+
onSelect: (index: number) => void;
|
|
44
|
+
idPrefix?: string;
|
|
45
|
+
/** When true, only the selected tab shows its text label. Requires icons on tabs. */
|
|
46
|
+
activeLabel?: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const TabsSubtle = forwardRef<HTMLDivElement, TabsSubtleProps>(
|
|
50
|
+
(
|
|
51
|
+
{
|
|
52
|
+
children,
|
|
53
|
+
selectedIndex,
|
|
54
|
+
onSelect,
|
|
55
|
+
idPrefix: idPrefixProp,
|
|
56
|
+
activeLabel = false,
|
|
57
|
+
className,
|
|
58
|
+
...props
|
|
59
|
+
},
|
|
60
|
+
ref,
|
|
61
|
+
) => {
|
|
62
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
63
|
+
const isMouseInside = useRef(false);
|
|
64
|
+
const generatedId = useId();
|
|
65
|
+
const shape = useShape();
|
|
66
|
+
const idPrefix = idPrefixProp || generatedId;
|
|
67
|
+
|
|
68
|
+
const {
|
|
69
|
+
activeIndex: hoveredIndex,
|
|
70
|
+
setActiveIndex: setHoveredIndex,
|
|
71
|
+
itemRects: tabRects,
|
|
72
|
+
handlers,
|
|
73
|
+
registerItem,
|
|
74
|
+
measureItems: measureTabs,
|
|
75
|
+
} = useProximityHover(containerRef, { axis: "x" });
|
|
76
|
+
|
|
77
|
+
// Track tab elements locally so we can observe their individual resizes
|
|
78
|
+
const tabElementsRef = useRef(new Map<number, HTMLElement>());
|
|
79
|
+
const registerTab = useCallback(
|
|
80
|
+
(index: number, element: HTMLElement | null) => {
|
|
81
|
+
registerItem(index, element);
|
|
82
|
+
if (element) {
|
|
83
|
+
tabElementsRef.current.set(index, element);
|
|
84
|
+
} else {
|
|
85
|
+
tabElementsRef.current.delete(index);
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
[registerItem],
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
measureTabs();
|
|
93
|
+
}, [measureTabs, children]);
|
|
94
|
+
|
|
95
|
+
// Observe individual tab buttons for resize (label expand/collapse in activeLabel mode)
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
const elements = tabElementsRef.current;
|
|
98
|
+
if (elements.size === 0) return;
|
|
99
|
+
const ro = new ResizeObserver(() => measureTabs());
|
|
100
|
+
elements.forEach((el) => ro.observe(el));
|
|
101
|
+
return () => ro.disconnect();
|
|
102
|
+
}, [measureTabs, children]);
|
|
103
|
+
|
|
104
|
+
// Wrap handlers to track isMouseInside
|
|
105
|
+
const handleMouseMove = useCallback(
|
|
106
|
+
(e: React.MouseEvent) => {
|
|
107
|
+
isMouseInside.current = true;
|
|
108
|
+
handlers.onMouseMove(e);
|
|
109
|
+
},
|
|
110
|
+
[handlers],
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const handleMouseLeave = useCallback(() => {
|
|
114
|
+
isMouseInside.current = false;
|
|
115
|
+
handlers.onMouseLeave();
|
|
116
|
+
}, [handlers]);
|
|
117
|
+
|
|
118
|
+
const [focusedIndex, setFocusedIndex] = useState<number | null>(null);
|
|
119
|
+
|
|
120
|
+
const selectedRect = tabRects[selectedIndex];
|
|
121
|
+
const hoverRect = hoveredIndex !== null ? tabRects[hoveredIndex] : null;
|
|
122
|
+
const focusRect = focusedIndex !== null ? tabRects[focusedIndex] : null;
|
|
123
|
+
const isHoveringSelected = hoveredIndex === selectedIndex;
|
|
124
|
+
const isHovering = hoveredIndex !== null && !isHoveringSelected;
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<TabsSubtleContext.Provider
|
|
128
|
+
value={{ registerTab, hoveredIndex, selectedIndex, onSelect, idPrefix, activeLabel }}
|
|
129
|
+
>
|
|
130
|
+
<div
|
|
131
|
+
ref={(node) => {
|
|
132
|
+
(containerRef as React.MutableRefObject<HTMLDivElement | null>).current = node;
|
|
133
|
+
if (typeof ref === "function") ref(node);
|
|
134
|
+
else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node;
|
|
135
|
+
}}
|
|
136
|
+
onMouseMove={handleMouseMove}
|
|
137
|
+
onMouseLeave={handleMouseLeave}
|
|
138
|
+
onFocus={(e) => {
|
|
139
|
+
const indexAttr = (e.target as HTMLElement)
|
|
140
|
+
.closest("[data-proximity-index]")
|
|
141
|
+
?.getAttribute("data-proximity-index");
|
|
142
|
+
if (indexAttr != null) {
|
|
143
|
+
const idx = Number(indexAttr);
|
|
144
|
+
setHoveredIndex(idx);
|
|
145
|
+
setFocusedIndex((e.target as HTMLElement).matches(":focus-visible") ? idx : null);
|
|
146
|
+
}
|
|
147
|
+
}}
|
|
148
|
+
onBlur={(e) => {
|
|
149
|
+
if (containerRef.current?.contains(e.relatedTarget as Node)) return;
|
|
150
|
+
setFocusedIndex(null);
|
|
151
|
+
if (isMouseInside.current) return;
|
|
152
|
+
setHoveredIndex(null);
|
|
153
|
+
}}
|
|
154
|
+
onKeyDown={(e) => {
|
|
155
|
+
const items = Array.from(
|
|
156
|
+
containerRef.current?.querySelectorAll('[role="tab"]') ?? [],
|
|
157
|
+
) as HTMLElement[];
|
|
158
|
+
const currentIdx = items.indexOf(e.target as HTMLElement);
|
|
159
|
+
if (currentIdx === -1) return;
|
|
160
|
+
|
|
161
|
+
if (["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(e.key)) {
|
|
162
|
+
e.preventDefault();
|
|
163
|
+
const next = ["ArrowRight", "ArrowDown"].includes(e.key)
|
|
164
|
+
? (currentIdx + 1) % items.length
|
|
165
|
+
: (currentIdx - 1 + items.length) % items.length;
|
|
166
|
+
items[next].focus();
|
|
167
|
+
} else if (e.key === "Home") {
|
|
168
|
+
e.preventDefault();
|
|
169
|
+
items[0]?.focus();
|
|
170
|
+
} else if (e.key === "End") {
|
|
171
|
+
e.preventDefault();
|
|
172
|
+
items[items.length - 1]?.focus();
|
|
173
|
+
}
|
|
174
|
+
}}
|
|
175
|
+
className={cn(
|
|
176
|
+
"relative flex items-center gap-0.5 select-none overflow-x-auto max-w-full scrollbar-hide -my-1 py-1",
|
|
177
|
+
className,
|
|
178
|
+
)}
|
|
179
|
+
role="tablist"
|
|
180
|
+
{...props}
|
|
181
|
+
>
|
|
182
|
+
{/* Selected pill */}
|
|
183
|
+
{selectedRect && (
|
|
184
|
+
<motion.div
|
|
185
|
+
className={cn("absolute bg-active pointer-events-none", shape.bg)}
|
|
186
|
+
initial={false}
|
|
187
|
+
animate={{
|
|
188
|
+
left: selectedRect.left,
|
|
189
|
+
width: selectedRect.width,
|
|
190
|
+
top: selectedRect.top,
|
|
191
|
+
height: selectedRect.height,
|
|
192
|
+
opacity: isHovering ? 0.8 : 1,
|
|
193
|
+
}}
|
|
194
|
+
transition={{
|
|
195
|
+
...spring.moderate,
|
|
196
|
+
opacity: { duration: 0.08 },
|
|
197
|
+
}}
|
|
198
|
+
/>
|
|
199
|
+
)}
|
|
200
|
+
|
|
201
|
+
{/* Hover pill */}
|
|
202
|
+
<AnimatePresence>
|
|
203
|
+
{hoverRect && !isHoveringSelected && selectedRect && (
|
|
204
|
+
<motion.div
|
|
205
|
+
className={cn("absolute bg-active pointer-events-none", shape.bg)}
|
|
206
|
+
initial={{
|
|
207
|
+
left: selectedRect.left,
|
|
208
|
+
width: selectedRect.width,
|
|
209
|
+
top: selectedRect.top,
|
|
210
|
+
height: selectedRect.height,
|
|
211
|
+
opacity: 0,
|
|
212
|
+
}}
|
|
213
|
+
animate={{
|
|
214
|
+
left: hoverRect.left,
|
|
215
|
+
width: hoverRect.width,
|
|
216
|
+
top: hoverRect.top,
|
|
217
|
+
height: hoverRect.height,
|
|
218
|
+
opacity: 0.4,
|
|
219
|
+
}}
|
|
220
|
+
exit={
|
|
221
|
+
!isMouseInside.current && selectedRect
|
|
222
|
+
? {
|
|
223
|
+
left: selectedRect.left,
|
|
224
|
+
width: selectedRect.width,
|
|
225
|
+
top: selectedRect.top,
|
|
226
|
+
height: selectedRect.height,
|
|
227
|
+
opacity: 0,
|
|
228
|
+
transition: { ...spring.moderate, opacity: { duration: 0.06 } },
|
|
229
|
+
}
|
|
230
|
+
: { opacity: 0, transition: spring.fast.exit }
|
|
231
|
+
}
|
|
232
|
+
transition={{
|
|
233
|
+
...spring.fast,
|
|
234
|
+
opacity: { duration: 0.08 },
|
|
235
|
+
}}
|
|
236
|
+
/>
|
|
237
|
+
)}
|
|
238
|
+
</AnimatePresence>
|
|
239
|
+
|
|
240
|
+
{/* Focus ring */}
|
|
241
|
+
<AnimatePresence>
|
|
242
|
+
{focusRect && (
|
|
243
|
+
<motion.div
|
|
244
|
+
className={cn(
|
|
245
|
+
"absolute pointer-events-none z-20 border border-[#6B97FF]",
|
|
246
|
+
shape.focusRing,
|
|
247
|
+
)}
|
|
248
|
+
initial={false}
|
|
249
|
+
animate={{
|
|
250
|
+
left: focusRect.left - 2,
|
|
251
|
+
top: focusRect.top - 2,
|
|
252
|
+
width: focusRect.width + 4,
|
|
253
|
+
height: focusRect.height + 4,
|
|
254
|
+
}}
|
|
255
|
+
exit={{ opacity: 0, transition: spring.fast.exit }}
|
|
256
|
+
transition={{
|
|
257
|
+
...spring.fast,
|
|
258
|
+
opacity: { duration: 0.08 },
|
|
259
|
+
}}
|
|
260
|
+
/>
|
|
261
|
+
)}
|
|
262
|
+
</AnimatePresence>
|
|
263
|
+
|
|
264
|
+
{children}
|
|
265
|
+
</div>
|
|
266
|
+
</TabsSubtleContext.Provider>
|
|
267
|
+
);
|
|
268
|
+
},
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
TabsSubtle.displayName = "TabsSubtle";
|
|
272
|
+
|
|
273
|
+
interface TabsSubtleItemProps extends HTMLAttributes<HTMLButtonElement> {
|
|
274
|
+
icon?: IconComponent;
|
|
275
|
+
label: string;
|
|
276
|
+
index: number;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const TabsSubtleItem = forwardRef<HTMLButtonElement, TabsSubtleItemProps>(
|
|
280
|
+
({ icon: Icon, label, index, className, ...props }, ref) => {
|
|
281
|
+
const internalRef = useRef<HTMLButtonElement>(null);
|
|
282
|
+
const shape = useShape();
|
|
283
|
+
const { registerTab, hoveredIndex, selectedIndex, onSelect, idPrefix, activeLabel } =
|
|
284
|
+
useTabsSubtle();
|
|
285
|
+
|
|
286
|
+
useEffect(() => {
|
|
287
|
+
registerTab(index, internalRef.current);
|
|
288
|
+
return () => registerTab(index, null);
|
|
289
|
+
}, [index, registerTab]);
|
|
290
|
+
|
|
291
|
+
const isSelected = selectedIndex === index;
|
|
292
|
+
const isActive = hoveredIndex === index || isSelected;
|
|
293
|
+
const collapseLabel = activeLabel && !!Icon;
|
|
294
|
+
const showLabel = !collapseLabel || isSelected;
|
|
295
|
+
|
|
296
|
+
const labelContent = (
|
|
297
|
+
<span className="inline-grid text-[13px] whitespace-nowrap">
|
|
298
|
+
<span
|
|
299
|
+
className="col-start-1 row-start-1 invisible"
|
|
300
|
+
style={{ fontVariationSettings: fontWeights.semibold }}
|
|
301
|
+
aria-hidden="true"
|
|
302
|
+
>
|
|
303
|
+
{label}
|
|
304
|
+
</span>
|
|
305
|
+
<span
|
|
306
|
+
className={cn(
|
|
307
|
+
"col-start-1 row-start-1 transition-[color,font-variation-settings] duration-80",
|
|
308
|
+
isActive ? "text-foreground" : "text-muted-foreground",
|
|
309
|
+
)}
|
|
310
|
+
style={{
|
|
311
|
+
fontVariationSettings: isSelected ? fontWeights.semibold : fontWeights.normal,
|
|
312
|
+
}}
|
|
313
|
+
>
|
|
314
|
+
{label}
|
|
315
|
+
</span>
|
|
316
|
+
</span>
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
return (
|
|
320
|
+
<button
|
|
321
|
+
ref={(node) => {
|
|
322
|
+
(internalRef as React.MutableRefObject<HTMLButtonElement | null>).current = node;
|
|
323
|
+
if (typeof ref === "function") ref(node);
|
|
324
|
+
else if (ref) (ref as React.MutableRefObject<HTMLButtonElement | null>).current = node;
|
|
325
|
+
}}
|
|
326
|
+
data-proximity-index={index}
|
|
327
|
+
id={`${idPrefix}-tab-${index}`}
|
|
328
|
+
role="tab"
|
|
329
|
+
aria-selected={isSelected}
|
|
330
|
+
aria-controls={`${idPrefix}-panel-${index}`}
|
|
331
|
+
aria-label={collapseLabel && !showLabel ? label : undefined}
|
|
332
|
+
tabIndex={isSelected ? 0 : -1}
|
|
333
|
+
onClick={() => onSelect(index)}
|
|
334
|
+
className={cn(
|
|
335
|
+
"relative z-10 flex items-center px-3 py-2 cursor-pointer bg-transparent border-none outline-none",
|
|
336
|
+
collapseLabel ? "h-8" : "gap-2",
|
|
337
|
+
shape.bg,
|
|
338
|
+
className,
|
|
339
|
+
)}
|
|
340
|
+
{...props}
|
|
341
|
+
>
|
|
342
|
+
{Icon && (
|
|
343
|
+
<Icon
|
|
344
|
+
size={16}
|
|
345
|
+
strokeWidth={isActive ? 2 : 1.5}
|
|
346
|
+
className={cn(
|
|
347
|
+
"shrink-0 transition-[color,stroke-width] duration-80",
|
|
348
|
+
isActive ? "text-foreground" : "text-muted-foreground",
|
|
349
|
+
)}
|
|
350
|
+
/>
|
|
351
|
+
)}
|
|
352
|
+
{collapseLabel ? (
|
|
353
|
+
<AnimatePresence initial={false}>
|
|
354
|
+
{showLabel && (
|
|
355
|
+
<motion.span
|
|
356
|
+
key="label"
|
|
357
|
+
className="overflow-hidden"
|
|
358
|
+
initial={{ width: 0, opacity: 0, marginLeft: 0 }}
|
|
359
|
+
animate={{ width: "auto", opacity: 1, marginLeft: 8 }}
|
|
360
|
+
exit={{ width: 0, opacity: 0, marginLeft: 0 }}
|
|
361
|
+
transition={{
|
|
362
|
+
...spring.fast,
|
|
363
|
+
opacity: { duration: 0.06 },
|
|
364
|
+
}}
|
|
365
|
+
>
|
|
366
|
+
{labelContent}
|
|
367
|
+
</motion.span>
|
|
368
|
+
)}
|
|
369
|
+
</AnimatePresence>
|
|
370
|
+
) : (
|
|
371
|
+
labelContent
|
|
372
|
+
)}
|
|
373
|
+
</button>
|
|
374
|
+
);
|
|
375
|
+
},
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
TabsSubtleItem.displayName = "TabsSubtleItem";
|
|
379
|
+
|
|
380
|
+
interface TabsSubtlePanelProps extends HTMLAttributes<HTMLDivElement> {
|
|
381
|
+
index: number;
|
|
382
|
+
selectedIndex: number;
|
|
383
|
+
idPrefix: string;
|
|
384
|
+
children: ReactNode;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const TabsSubtlePanel = forwardRef<HTMLDivElement, TabsSubtlePanelProps>(
|
|
388
|
+
({ index, selectedIndex, idPrefix, children, className, ...props }, ref) => {
|
|
389
|
+
const isSelected = selectedIndex === index;
|
|
390
|
+
|
|
391
|
+
return (
|
|
392
|
+
<div
|
|
393
|
+
ref={ref}
|
|
394
|
+
id={`${idPrefix}-panel-${index}`}
|
|
395
|
+
role="tabpanel"
|
|
396
|
+
aria-labelledby={`${idPrefix}-tab-${index}`}
|
|
397
|
+
hidden={!isSelected}
|
|
398
|
+
tabIndex={-1}
|
|
399
|
+
className={cn("outline-none", className)}
|
|
400
|
+
{...props}
|
|
401
|
+
>
|
|
402
|
+
{isSelected && children}
|
|
403
|
+
</div>
|
|
404
|
+
);
|
|
405
|
+
},
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
TabsSubtlePanel.displayName = "TabsSubtlePanel";
|
|
409
|
+
|
|
410
|
+
export { TabsSubtle, TabsSubtleItem, TabsSubtlePanel };
|
|
411
|
+
export default TabsSubtle;
|