@suphark/ui 0.19.0 → 0.20.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/CHANGELOG.md +30 -0
- package/package.json +14 -13
- package/scripts/audit.mjs +0 -0
- package/src/components/app-shell/app-shell.tsx +12 -0
- package/src/components/app-shell/index.ts +1 -0
- package/src/components/app-shell/mobile-tab-bar.tsx +104 -0
- package/src/components/shared/action-menu.tsx +155 -0
- package/src/components/shared/filter-bar.tsx +108 -0
- package/src/components/shared/month-stepper.tsx +124 -0
- package/src/components/shared/segmented-control.tsx +73 -0
- package/src/components/shared/settings-layout.tsx +170 -0
- package/src/components/shared/setup-checklist.tsx +104 -0
- package/src/components/shared/simple-dialog.tsx +172 -0
- package/src/components/shared/simple-pagination.tsx +109 -0
- package/src/components/shared/simple-table.tsx +93 -0
- package/src/components/shared/simple-tabs.tsx +118 -0
- package/src/components/shared/simple-tooltip.tsx +87 -0
- package/src/components/shared/ui-text.tsx +28 -0
- package/src/components/ui/accordion.tsx +2 -2
- package/src/components/ui/alert-dialog.tsx +4 -4
- package/src/components/ui/alert.tsx +2 -2
- package/src/components/ui/attachment.tsx +9 -9
- package/src/components/ui/avatar.tsx +4 -4
- package/src/components/ui/badge.tsx +1 -1
- package/src/components/ui/breadcrumb.tsx +1 -1
- package/src/components/ui/bubble.tsx +123 -0
- package/src/components/ui/button-group.tsx +2 -2
- package/src/components/ui/button.tsx +6 -6
- package/src/components/ui/calendar.tsx +9 -9
- package/src/components/ui/card.tsx +3 -3
- package/src/components/ui/chart.tsx +2 -2
- package/src/components/ui/checkbox.tsx +1 -1
- package/src/components/ui/combobox.tsx +6 -6
- package/src/components/ui/command.tsx +4 -4
- package/src/components/ui/context-menu.tsx +7 -7
- package/src/components/ui/dialog.tsx +4 -4
- package/src/components/ui/direction.tsx +6 -0
- package/src/components/ui/drawer.tsx +10 -10
- package/src/components/ui/dropdown-menu.tsx +8 -8
- package/src/components/ui/empty.tsx +4 -4
- package/src/components/ui/field.tsx +8 -8
- package/src/components/ui/hover-card.tsx +1 -2
- package/src/components/ui/input-group.tsx +3 -3
- package/src/components/ui/input-otp.tsx +1 -1
- package/src/components/ui/input.tsx +1 -1
- package/src/components/ui/item.tsx +4 -4
- package/src/components/ui/kbd.tsx +1 -1
- package/src/components/ui/label.tsx +1 -1
- package/src/components/ui/marker.tsx +67 -0
- package/src/components/ui/menubar.tsx +9 -9
- package/src/components/ui/message-scroller.tsx +129 -0
- package/src/components/ui/message.tsx +84 -0
- package/src/components/ui/native-select.tsx +1 -1
- package/src/components/ui/navigation-menu.tsx +6 -6
- package/src/components/ui/popover.tsx +1 -1
- package/src/components/ui/progress.tsx +2 -3
- package/src/components/ui/questionnaire.tsx +314 -0
- package/src/components/ui/radio-group.tsx +1 -2
- package/src/components/ui/resizable.tsx +1 -1
- package/src/components/ui/select.tsx +4 -4
- package/src/components/ui/separator.tsx +0 -1
- package/src/components/ui/sheet.tsx +3 -3
- package/src/components/ui/sidebar.tsx +14 -14
- package/src/components/ui/switch.tsx +2 -3
- package/src/components/ui/textarea.tsx +1 -1
- package/src/components/ui/toast.tsx +221 -0
- package/src/components/ui/toggle-group.tsx +2 -2
- package/src/components/ui/toggle.tsx +1 -1
- package/src/components/ui/tooltip.tsx +0 -1
- package/src/index.ts +41 -0
- package/src/lib/month.ts +60 -0
- package/src/styles.css +10 -0
|
@@ -86,7 +86,7 @@ function SidebarProvider({
|
|
|
86
86
|
// Helper to toggle the sidebar.
|
|
87
87
|
const toggleSidebar = React.useCallback(() => {
|
|
88
88
|
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open);
|
|
89
|
-
}, [isMobile, setOpen]);
|
|
89
|
+
}, [isMobile, setOpen, setOpenMobile]);
|
|
90
90
|
|
|
91
91
|
// Adds a keyboard shortcut to toggle the sidebar.
|
|
92
92
|
React.useEffect(() => {
|
|
@@ -115,7 +115,7 @@ function SidebarProvider({
|
|
|
115
115
|
setOpenMobile,
|
|
116
116
|
toggleSidebar,
|
|
117
117
|
}),
|
|
118
|
-
[state, open, setOpen, isMobile, openMobile, toggleSidebar],
|
|
118
|
+
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar],
|
|
119
119
|
);
|
|
120
120
|
|
|
121
121
|
return (
|
|
@@ -222,7 +222,7 @@ function Sidebar({
|
|
|
222
222
|
data-slot="sidebar-container"
|
|
223
223
|
data-side={side}
|
|
224
224
|
className={cn(
|
|
225
|
-
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear data-[side=
|
|
225
|
+
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear data-[side=left]:left-0 data-[side=left]:group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)] data-[side=right]:right-0 data-[side=right]:group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)] md:flex",
|
|
226
226
|
// Adjust the padding for floating and inset variants.
|
|
227
227
|
variant === "floating" || variant === "inset"
|
|
228
228
|
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
|
|
@@ -277,10 +277,10 @@ function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
|
|
|
277
277
|
onClick={toggleSidebar}
|
|
278
278
|
title="Toggle Sidebar"
|
|
279
279
|
className={cn(
|
|
280
|
-
"absolute inset-y-0 z-20 hidden w-4 transition-all ease-linear after:absolute after:inset-y-0 after:start-1/2 after:w-[2px] hover:after:bg-sidebar-border
|
|
280
|
+
"absolute inset-y-0 z-20 hidden w-4 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:start-1/2 after:w-[2px] hover:after:bg-sidebar-border sm:flex ltr:-translate-x-1/2 rtl:-translate-x-1/2",
|
|
281
281
|
"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
|
|
282
282
|
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
|
|
283
|
-
"group-data-[collapsible=offcanvas]:translate-x-0
|
|
283
|
+
"group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full hover:group-data-[collapsible=offcanvas]:bg-sidebar",
|
|
284
284
|
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
|
|
285
285
|
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
|
|
286
286
|
className,
|
|
@@ -295,7 +295,7 @@ function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
|
|
|
295
295
|
<main
|
|
296
296
|
data-slot="sidebar-inset"
|
|
297
297
|
className={cn(
|
|
298
|
-
"relative flex w-full flex-1 flex-col bg-background md:peer-data-[variant=inset]:peer-data-[
|
|
298
|
+
"relative flex w-full flex-1 flex-col bg-background md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
|
|
299
299
|
className,
|
|
300
300
|
)}
|
|
301
301
|
{...props}
|
|
@@ -382,7 +382,7 @@ function SidebarGroupLabel({
|
|
|
382
382
|
props: mergeProps<"div">(
|
|
383
383
|
{
|
|
384
384
|
className: cn(
|
|
385
|
-
"flex h-8 shrink-0 items-center rounded-md px-2 font-medium text-sidebar-foreground/70
|
|
385
|
+
"flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 ring-sidebar-ring outline-hidden transition-[margin,opacity] duration-200 ease-linear group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0 focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
|
386
386
|
className,
|
|
387
387
|
),
|
|
388
388
|
},
|
|
@@ -406,7 +406,7 @@ function SidebarGroupAction({
|
|
|
406
406
|
props: mergeProps<"button">(
|
|
407
407
|
{
|
|
408
408
|
className: cn(
|
|
409
|
-
"absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground
|
|
409
|
+
"absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground ring-sidebar-ring outline-hidden transition-transform group-data-[collapsible=icon]:hidden after:absolute after:-inset-2 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0",
|
|
410
410
|
className,
|
|
411
411
|
),
|
|
412
412
|
},
|
|
@@ -454,7 +454,7 @@ function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
|
|
|
454
454
|
}
|
|
455
455
|
|
|
456
456
|
const sidebarMenuButtonVariants = cva(
|
|
457
|
-
"peer/menu-button group/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm
|
|
457
|
+
"peer/menu-button group/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm ring-sidebar-ring outline-hidden transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-open:hover:bg-sidebar-accent data-open:hover:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:font-medium data-active:text-sidebar-accent-foreground [&_svg]:size-4 [&_svg]:shrink-0 [&>span:last-child]:truncate",
|
|
458
458
|
{
|
|
459
459
|
variants: {
|
|
460
460
|
variant: {
|
|
@@ -543,9 +543,9 @@ function SidebarMenuAction({
|
|
|
543
543
|
props: mergeProps<"button">(
|
|
544
544
|
{
|
|
545
545
|
className: cn(
|
|
546
|
-
"absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground
|
|
546
|
+
"absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground ring-sidebar-ring outline-hidden transition-transform group-data-[collapsible=icon]:hidden peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 after:absolute after:-inset-2 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0",
|
|
547
547
|
showOnHover &&
|
|
548
|
-
"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100
|
|
548
|
+
"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 peer-data-active/menu-button:text-sidebar-accent-foreground aria-expanded:opacity-100 md:opacity-0",
|
|
549
549
|
className,
|
|
550
550
|
),
|
|
551
551
|
},
|
|
@@ -565,7 +565,7 @@ function SidebarMenuBadge({ className, ...props }: React.ComponentProps<"div">)
|
|
|
565
565
|
data-slot="sidebar-menu-badge"
|
|
566
566
|
data-sidebar="menu-badge"
|
|
567
567
|
className={cn(
|
|
568
|
-
"pointer-events-none absolute right-1 flex h-5 min-w-5
|
|
568
|
+
"pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium text-sidebar-foreground tabular-nums select-none group-data-[collapsible=icon]:hidden peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 peer-data-active/menu-button:text-sidebar-accent-foreground",
|
|
569
569
|
className,
|
|
570
570
|
)}
|
|
571
571
|
{...props}
|
|
@@ -612,7 +612,7 @@ function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
|
|
|
612
612
|
data-slot="sidebar-menu-sub"
|
|
613
613
|
data-sidebar="menu-sub"
|
|
614
614
|
className={cn(
|
|
615
|
-
"mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-sidebar-border
|
|
615
|
+
"mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5 group-data-[collapsible=icon]:hidden",
|
|
616
616
|
className,
|
|
617
617
|
)}
|
|
618
618
|
{...props}
|
|
@@ -647,7 +647,7 @@ function SidebarMenuSubButton({
|
|
|
647
647
|
props: mergeProps<"a">(
|
|
648
648
|
{
|
|
649
649
|
className: cn(
|
|
650
|
-
"flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-hidden
|
|
650
|
+
"flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground ring-sidebar-ring outline-hidden group-data-[collapsible=icon]:hidden hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[size=md]:text-sm data-[size=sm]:text-xs data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
|
|
651
651
|
className,
|
|
652
652
|
),
|
|
653
653
|
},
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { Switch as SwitchPrimitive } from "@base-ui/react/switch";
|
|
4
|
-
|
|
5
4
|
import { cn } from "@suphark/ui/lib/utils";
|
|
6
5
|
|
|
7
6
|
function Switch({
|
|
@@ -16,14 +15,14 @@ function Switch({
|
|
|
16
15
|
data-slot="switch"
|
|
17
16
|
data-size={size}
|
|
18
17
|
className={cn(
|
|
19
|
-
"peer group/switch relative inline-flex shrink-0 items-center rounded-full border border-transparent outline-none
|
|
18
|
+
"peer group/switch relative inline-flex shrink-0 items-center rounded-full border border-transparent transition-all outline-none group-has-[:focus-visible]/field-label:border-transparent group-has-[:focus-visible]/field-label:ring-0 after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-[size=default]:h-[18.4px] data-[size=default]:w-[32px] data-[size=sm]:h-[14px] data-[size=sm]:w-[24px] dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:bg-primary data-unchecked:bg-input dark:data-unchecked:bg-input/80 data-disabled:cursor-not-allowed data-disabled:opacity-50",
|
|
20
19
|
className,
|
|
21
20
|
)}
|
|
22
21
|
{...props}
|
|
23
22
|
>
|
|
24
23
|
<SwitchPrimitive.Thumb
|
|
25
24
|
data-slot="switch-thumb"
|
|
26
|
-
className="pointer-events-none block rounded-full bg-background ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=
|
|
25
|
+
className="pointer-events-none block rounded-full bg-background ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=sm]/switch:data-checked:translate-x-[calc(100%-2px)] dark:data-checked:bg-primary-foreground group-data-[size=default]/switch:data-unchecked:translate-x-0 group-data-[size=sm]/switch:data-unchecked:translate-x-0 dark:data-unchecked:bg-foreground"
|
|
27
26
|
/>
|
|
28
27
|
</SwitchPrimitive.Root>
|
|
29
28
|
);
|
|
@@ -6,7 +6,7 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
|
|
6
6
|
<textarea
|
|
7
7
|
data-slot="textarea"
|
|
8
8
|
className={cn(
|
|
9
|
-
"field-sizing-content
|
|
9
|
+
"flex field-sizing-content min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
|
10
10
|
className,
|
|
11
11
|
)}
|
|
12
12
|
{...props}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Toast as ToastPrimitive } from "@base-ui/react/toast";
|
|
4
|
+
import { Button } from "@suphark/ui/components/ui/button";
|
|
5
|
+
import { cn } from "@suphark/ui/lib/utils";
|
|
6
|
+
import {
|
|
7
|
+
CircleCheckIcon,
|
|
8
|
+
InfoIcon,
|
|
9
|
+
Loader2Icon,
|
|
10
|
+
OctagonXIcon,
|
|
11
|
+
TriangleAlertIcon,
|
|
12
|
+
XIcon,
|
|
13
|
+
} from "lucide-react";
|
|
14
|
+
import type * as React from "react";
|
|
15
|
+
|
|
16
|
+
const toast = ToastPrimitive.createToastManager();
|
|
17
|
+
|
|
18
|
+
function ToastProvider({ ...props }: ToastPrimitive.Provider.Props) {
|
|
19
|
+
return <ToastPrimitive.Provider {...props} />;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function ToastPortal({ ...props }: ToastPrimitive.Portal.Props) {
|
|
23
|
+
return <ToastPrimitive.Portal data-slot="toast-portal" {...props} />;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function ToastViewport({ className, ...props }: ToastPrimitive.Viewport.Props) {
|
|
27
|
+
return (
|
|
28
|
+
<ToastPrimitive.Viewport
|
|
29
|
+
data-slot="toast-viewport"
|
|
30
|
+
className={cn(
|
|
31
|
+
"pointer-events-none fixed inset-x-4 bottom-4 z-50 mx-auto w-auto max-w-sm outline-none sm:right-4 sm:left-auto sm:mx-0 sm:w-full",
|
|
32
|
+
className,
|
|
33
|
+
)}
|
|
34
|
+
{...props}
|
|
35
|
+
/>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function Toast({ className, ...props }: ToastPrimitive.Root.Props) {
|
|
40
|
+
return (
|
|
41
|
+
<ToastPrimitive.Root
|
|
42
|
+
data-slot="toast"
|
|
43
|
+
className={cn(
|
|
44
|
+
"group/toast pointer-events-auto absolute right-0 bottom-0 z-[calc(1000-var(--toast-index))] w-full origin-bottom rounded-2xl border bg-popover text-popover-foreground shadow-lg will-change-transform outline-none select-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
|
|
45
|
+
"[--gap:0.75rem] [--height:var(--toast-frontmost-height,var(--toast-height))] [--offset-y:calc(var(--toast-offset-y)*-1+calc(var(--toast-index)*var(--gap)*-1)+var(--toast-swipe-movement-y))] [--peek:0.75rem] [--scale:calc(max(0,1-(var(--toast-index)*0.1)))] [--shrink:calc(1-var(--scale))]",
|
|
46
|
+
"h-(--height) [transform:translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)-(var(--toast-index)*var(--peek))-(var(--shrink)*var(--height))))_scale(var(--scale))] [transition:transform_500ms_cubic-bezier(0.22,1,0.36,1),opacity_500ms,height_150ms]",
|
|
47
|
+
"after:absolute after:top-full after:left-0 after:h-[calc(var(--gap)+1px)] after:w-full after:content-['']",
|
|
48
|
+
"data-expanded:h-(--toast-height) data-expanded:[transform:translateX(var(--toast-swipe-movement-x))_translateY(var(--offset-y))]",
|
|
49
|
+
"data-limited:opacity-0 data-starting-style:[transform:translateY(150%)]",
|
|
50
|
+
"[&[data-ending-style]:not([data-limited]):not([data-swipe-direction])]:[transform:translateY(150%)]",
|
|
51
|
+
"data-ending-style:data-[swipe-direction=down]:[transform:translateY(calc(var(--toast-swipe-movement-y)+150%))]",
|
|
52
|
+
"data-ending-style:data-[swipe-direction=left]:[transform:translateX(calc(var(--toast-swipe-movement-x)-150%))_translateY(var(--offset-y))]",
|
|
53
|
+
"data-ending-style:data-[swipe-direction=right]:[transform:translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--offset-y))]",
|
|
54
|
+
"data-ending-style:data-[swipe-direction=up]:[transform:translateY(calc(var(--toast-swipe-movement-y)-150%))]",
|
|
55
|
+
"data-expanded:data-ending-style:data-[swipe-direction=down]:[transform:translateY(calc(var(--toast-swipe-movement-y)+150%))]",
|
|
56
|
+
"data-expanded:data-ending-style:data-[swipe-direction=left]:[transform:translateX(calc(var(--toast-swipe-movement-x)-150%))_translateY(var(--offset-y))]",
|
|
57
|
+
"data-expanded:data-ending-style:data-[swipe-direction=right]:[transform:translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--offset-y))]",
|
|
58
|
+
"data-expanded:data-ending-style:data-[swipe-direction=up]:[transform:translateY(calc(var(--toast-swipe-movement-y)-150%))]",
|
|
59
|
+
className,
|
|
60
|
+
)}
|
|
61
|
+
{...props}
|
|
62
|
+
/>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function ToastContent({ className, ...props }: ToastPrimitive.Content.Props) {
|
|
67
|
+
return (
|
|
68
|
+
<ToastPrimitive.Content
|
|
69
|
+
data-slot="toast-content"
|
|
70
|
+
className={cn(
|
|
71
|
+
"flex h-full items-center gap-3 overflow-hidden p-4 transition-opacity duration-250 ease-[cubic-bezier(0.22,1,0.36,1)] data-behind:opacity-0 data-expanded:opacity-100",
|
|
72
|
+
className,
|
|
73
|
+
)}
|
|
74
|
+
{...props}
|
|
75
|
+
/>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function ToastTitle({ className, ...props }: ToastPrimitive.Title.Props) {
|
|
80
|
+
return (
|
|
81
|
+
<ToastPrimitive.Title
|
|
82
|
+
data-slot="toast-title"
|
|
83
|
+
className={cn("text-sm font-medium", className)}
|
|
84
|
+
{...props}
|
|
85
|
+
/>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function ToastDescription({ className, ...props }: ToastPrimitive.Description.Props) {
|
|
90
|
+
return (
|
|
91
|
+
<ToastPrimitive.Description
|
|
92
|
+
data-slot="toast-description"
|
|
93
|
+
className={cn("text-sm text-muted-foreground", className)}
|
|
94
|
+
{...props}
|
|
95
|
+
/>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function ToastAction({
|
|
100
|
+
className,
|
|
101
|
+
render = <Button variant="outline" size="sm" />,
|
|
102
|
+
...props
|
|
103
|
+
}: ToastPrimitive.Action.Props) {
|
|
104
|
+
return (
|
|
105
|
+
<ToastPrimitive.Action
|
|
106
|
+
data-slot="toast-action"
|
|
107
|
+
render={render}
|
|
108
|
+
className={cn("shrink-0", className)}
|
|
109
|
+
{...props}
|
|
110
|
+
/>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function ToastClose({
|
|
115
|
+
className,
|
|
116
|
+
children,
|
|
117
|
+
render = <Button variant="ghost" size="icon-sm" />,
|
|
118
|
+
...props
|
|
119
|
+
}: ToastPrimitive.Close.Props) {
|
|
120
|
+
return (
|
|
121
|
+
<ToastPrimitive.Close
|
|
122
|
+
data-slot="toast-close"
|
|
123
|
+
aria-label="Close toast"
|
|
124
|
+
render={render}
|
|
125
|
+
className={cn(
|
|
126
|
+
"relative shrink-0 text-muted-foreground after:absolute after:-inset-2 after:content-[''] hover:text-foreground",
|
|
127
|
+
className,
|
|
128
|
+
)}
|
|
129
|
+
{...props}
|
|
130
|
+
>
|
|
131
|
+
{children ?? <XIcon aria-hidden="true" />}
|
|
132
|
+
</ToastPrimitive.Close>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function ToastIcon({ type }: { type: string | undefined }) {
|
|
137
|
+
let icon: React.ReactNode = null;
|
|
138
|
+
|
|
139
|
+
if (type === "success") {
|
|
140
|
+
icon = <CircleCheckIcon aria-hidden="true" />;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (type === "info") {
|
|
144
|
+
icon = <InfoIcon aria-hidden="true" />;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (type === "warning") {
|
|
148
|
+
icon = <TriangleAlertIcon aria-hidden="true" />;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (type === "error") {
|
|
152
|
+
icon = <OctagonXIcon className="text-destructive" aria-hidden="true" />;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (type === "loading") {
|
|
156
|
+
icon = <Loader2Icon className="animate-spin" aria-hidden="true" />;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!icon) {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<span
|
|
165
|
+
data-slot="toast-icon"
|
|
166
|
+
className="shrink-0 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4"
|
|
167
|
+
>
|
|
168
|
+
{icon}
|
|
169
|
+
</span>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function ToastList() {
|
|
174
|
+
const { toasts } = ToastPrimitive.useToastManager();
|
|
175
|
+
|
|
176
|
+
return toasts.map((toastItem) => (
|
|
177
|
+
<Toast key={toastItem.id} toast={toastItem}>
|
|
178
|
+
<ToastContent>
|
|
179
|
+
<ToastIcon type={toastItem.type} />
|
|
180
|
+
<div className="flex min-w-0 flex-1 flex-col gap-1">
|
|
181
|
+
<ToastTitle />
|
|
182
|
+
<ToastDescription />
|
|
183
|
+
</div>
|
|
184
|
+
<ToastAction />
|
|
185
|
+
<ToastClose />
|
|
186
|
+
</ToastContent>
|
|
187
|
+
</Toast>
|
|
188
|
+
));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function Toaster({ children, toastManager = toast, ...props }: ToastPrimitive.Provider.Props) {
|
|
192
|
+
return (
|
|
193
|
+
<ToastProvider toastManager={toastManager} {...props}>
|
|
194
|
+
{children}
|
|
195
|
+
<ToastPortal>
|
|
196
|
+
<ToastViewport>
|
|
197
|
+
<ToastList />
|
|
198
|
+
</ToastViewport>
|
|
199
|
+
</ToastPortal>
|
|
200
|
+
</ToastProvider>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const createToastManager = ToastPrimitive.createToastManager;
|
|
205
|
+
const useToastManager = ToastPrimitive.useToastManager;
|
|
206
|
+
|
|
207
|
+
export {
|
|
208
|
+
createToastManager,
|
|
209
|
+
Toast,
|
|
210
|
+
ToastAction,
|
|
211
|
+
ToastClose,
|
|
212
|
+
ToastContent,
|
|
213
|
+
ToastDescription,
|
|
214
|
+
Toaster,
|
|
215
|
+
ToastPortal,
|
|
216
|
+
ToastProvider,
|
|
217
|
+
ToastTitle,
|
|
218
|
+
ToastViewport,
|
|
219
|
+
toast,
|
|
220
|
+
useToastManager,
|
|
221
|
+
};
|
|
@@ -41,7 +41,7 @@ function ToggleGroup({
|
|
|
41
41
|
data-orientation={orientation}
|
|
42
42
|
style={{ "--gap": spacing } as React.CSSProperties}
|
|
43
43
|
className={cn(
|
|
44
|
-
"group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] rounded-lg data-
|
|
44
|
+
"group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] rounded-lg data-[size=sm]:rounded-[min(var(--radius-md),10px)] data-vertical:flex-col data-vertical:items-stretch",
|
|
45
45
|
className,
|
|
46
46
|
)}
|
|
47
47
|
{...props}
|
|
@@ -69,7 +69,7 @@ function ToggleGroupItem({
|
|
|
69
69
|
data-size={context.size || size}
|
|
70
70
|
data-spacing={context.spacing}
|
|
71
71
|
className={cn(
|
|
72
|
-
"shrink-0
|
|
72
|
+
"shrink-0 group-data-[spacing=0]/toggle-group:rounded-none group-data-[spacing=0]/toggle-group:px-2 focus:z-10 focus-visible:z-10 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-end]:pr-1.5 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-start]:pl-1.5 group-data-horizontal/toggle-group:data-[spacing=0]:first:rounded-l-lg group-data-vertical/toggle-group:data-[spacing=0]:first:rounded-t-lg group-data-horizontal/toggle-group:data-[spacing=0]:last:rounded-r-lg group-data-vertical/toggle-group:data-[spacing=0]:last:rounded-b-lg group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:border-l-0 group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t",
|
|
73
73
|
toggleVariants({
|
|
74
74
|
variant: context.variant || variant,
|
|
75
75
|
size: context.size || size,
|
|
@@ -5,7 +5,7 @@ import { cn } from "@suphark/ui/lib/utils";
|
|
|
5
5
|
import { cva, type VariantProps } from "class-variance-authority";
|
|
6
6
|
|
|
7
7
|
const toggleVariants = cva(
|
|
8
|
-
"group/toggle inline-flex items-center justify-center gap-1
|
|
8
|
+
"group/toggle inline-flex items-center justify-center gap-1 rounded-lg text-sm font-medium whitespace-nowrap transition-all outline-none hover:bg-muted hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 aria-pressed:bg-muted data-[state=on]:bg-muted dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
9
9
|
{
|
|
10
10
|
variants: {
|
|
11
11
|
variant: {
|
package/src/index.ts
CHANGED
|
@@ -14,6 +14,11 @@ export type { DateRange } from "react-day-picker";
|
|
|
14
14
|
// sonner: `toast()` คู่กับ <Toaster /> — แอปไม่ต้องติดตั้ง sonner เอง
|
|
15
15
|
export { toast } from "sonner";
|
|
16
16
|
export { ActionForm } from "./components/shared/action-form";
|
|
17
|
+
export {
|
|
18
|
+
ActionMenu,
|
|
19
|
+
type ActionMenuCommand,
|
|
20
|
+
type ActionMenuItem,
|
|
21
|
+
} from "./components/shared/action-menu";
|
|
17
22
|
export * from "./components/shared/button";
|
|
18
23
|
export {
|
|
19
24
|
type CalendarDayContext,
|
|
@@ -63,6 +68,7 @@ export { EmptyRow } from "./components/shared/empty-row";
|
|
|
63
68
|
export { EmptyState } from "./components/shared/empty-state";
|
|
64
69
|
export { EnvSnippet, randomSecret } from "./components/shared/env-snippet";
|
|
65
70
|
export { type FileKind, FileTypeIcon, getFileKind } from "./components/shared/file-type-icon";
|
|
71
|
+
export { FilterBar, type FilterBarFilter } from "./components/shared/filter-bar";
|
|
66
72
|
export { FormFieldBoolean } from "./components/shared/form/form-field-boolean";
|
|
67
73
|
export { FormFieldColorSelect } from "./components/shared/form/form-field-color-select";
|
|
68
74
|
export { FormFieldCombobox } from "./components/shared/form/form-field-combobox";
|
|
@@ -109,6 +115,11 @@ export {
|
|
|
109
115
|
type LanguageSwitchProps,
|
|
110
116
|
} from "./components/shared/language/language-switch";
|
|
111
117
|
export { ErrorState, type RouteError } from "./components/shared/layout/error-state";
|
|
118
|
+
export {
|
|
119
|
+
MonthStepper,
|
|
120
|
+
type MonthStepperProps,
|
|
121
|
+
useMonthCursor,
|
|
122
|
+
} from "./components/shared/month-stepper";
|
|
112
123
|
export {
|
|
113
124
|
MultiSelectCombobox,
|
|
114
125
|
type MultiSelectComboboxProps,
|
|
@@ -141,8 +152,21 @@ export {
|
|
|
141
152
|
export { type SaveState, SaveStatus, type SaveStatusText } from "./components/shared/save-status";
|
|
142
153
|
export { SecretReveal } from "./components/shared/secret-reveal";
|
|
143
154
|
export { SectionCard } from "./components/shared/section-card";
|
|
155
|
+
export { SegmentedControl, type SegmentedOption } from "./components/shared/segmented-control";
|
|
144
156
|
export { SelectionButton } from "./components/shared/selection-button";
|
|
157
|
+
export { SettingsLayout, type SettingsSection } from "./components/shared/settings-layout";
|
|
145
158
|
export { DangerZone, SettingsRow, SwitchRow } from "./components/shared/settings-row";
|
|
159
|
+
export { SetupChecklist, type SetupChecklistStep } from "./components/shared/setup-checklist";
|
|
160
|
+
export { SimpleDialog, SimpleSheet } from "./components/shared/simple-dialog";
|
|
161
|
+
export { paginationRange, SimplePagination } from "./components/shared/simple-pagination";
|
|
162
|
+
export { SimpleTable, type SimpleTableColumn } from "./components/shared/simple-table";
|
|
163
|
+
export {
|
|
164
|
+
SimpleAccordion,
|
|
165
|
+
type SimpleAccordionItem,
|
|
166
|
+
type SimpleTabItem,
|
|
167
|
+
SimpleTabs,
|
|
168
|
+
} from "./components/shared/simple-tabs";
|
|
169
|
+
export { SimplePopover, SimpleTooltip } from "./components/shared/simple-tooltip";
|
|
146
170
|
export { SkipLink } from "./components/shared/skip-link";
|
|
147
171
|
export { Stat, type StatDelta, StatGrid, type StatTone } from "./components/shared/stat";
|
|
148
172
|
export { StatusBadge, statusTone, type Tone } from "./components/shared/status-badge";
|
|
@@ -178,7 +202,9 @@ export * from "./components/ui/aspect-ratio";
|
|
|
178
202
|
export * from "./components/ui/attachment";
|
|
179
203
|
export * from "./components/ui/avatar";
|
|
180
204
|
export * from "./components/ui/badge";
|
|
205
|
+
// toast.tsx (Base UI) ไม่อยู่ใน barrel: ชื่อ toast/Toaster ชนกับ sonner ที่แพ็กเกจใช้ — import ตรงจาก @suphark/ui/components/ui/toast ถ้าต้องการ
|
|
181
206
|
export * from "./components/ui/breadcrumb";
|
|
207
|
+
export * from "./components/ui/bubble";
|
|
182
208
|
export * from "./components/ui/button";
|
|
183
209
|
export * from "./components/ui/button-group";
|
|
184
210
|
export * from "./components/ui/calendar";
|
|
@@ -191,6 +217,7 @@ export * from "./components/ui/command";
|
|
|
191
217
|
export * from "./components/ui/context-menu";
|
|
192
218
|
export * from "./components/ui/date-picker";
|
|
193
219
|
export * from "./components/ui/dialog";
|
|
220
|
+
export * from "./components/ui/direction";
|
|
194
221
|
export * from "./components/ui/drawer";
|
|
195
222
|
export * from "./components/ui/dropdown-menu";
|
|
196
223
|
export * from "./components/ui/empty";
|
|
@@ -203,12 +230,16 @@ export * from "./components/ui/input-otp";
|
|
|
203
230
|
export * from "./components/ui/item";
|
|
204
231
|
export * from "./components/ui/kbd";
|
|
205
232
|
export * from "./components/ui/label";
|
|
233
|
+
export * from "./components/ui/marker";
|
|
206
234
|
export * from "./components/ui/menubar";
|
|
235
|
+
export * from "./components/ui/message";
|
|
236
|
+
export * from "./components/ui/message-scroller";
|
|
207
237
|
export * from "./components/ui/native-select";
|
|
208
238
|
export * from "./components/ui/navigation-menu";
|
|
209
239
|
export * from "./components/ui/pagination";
|
|
210
240
|
export * from "./components/ui/popover";
|
|
211
241
|
export * from "./components/ui/progress";
|
|
242
|
+
export * from "./components/ui/questionnaire";
|
|
212
243
|
export * from "./components/ui/radio-group";
|
|
213
244
|
export * from "./components/ui/relative-date";
|
|
214
245
|
export * from "./components/ui/resizable";
|
|
@@ -239,6 +270,16 @@ export { useTrackedState } from "./hooks/use-tracked-state";
|
|
|
239
270
|
export { copyText, selectContents } from "./lib/copy-text";
|
|
240
271
|
export { formatBytes, formatRelativeThaiDate, formatThaiFullDate } from "./lib/formatters";
|
|
241
272
|
export { getDateTimeFormat, getNumberFormat } from "./lib/intl-cache";
|
|
273
|
+
export {
|
|
274
|
+
compareMonth,
|
|
275
|
+
currentMonthKey,
|
|
276
|
+
formatMonthKey,
|
|
277
|
+
isMonthKey,
|
|
278
|
+
type MonthKey,
|
|
279
|
+
parseMonthKey,
|
|
280
|
+
shiftMonth,
|
|
281
|
+
toMonthKey,
|
|
282
|
+
} from "./lib/month";
|
|
242
283
|
export {
|
|
243
284
|
type AtomicPermissionRequirement,
|
|
244
285
|
type CheckPermissionOptions,
|
package/src/lib/month.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { getDateTimeFormat } from "./intl-cache";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* เดือนแบบ `"YYYY-MM"` (รูปแบบเดียวกับ `CalendarMonthGrid month`) — ไม่มีวัน/เวลา จึงไม่มีปัญหาโซนเวลา
|
|
5
|
+
* ยกจาก DueBell `CycleMonth` ให้แอปบัญชี/รายงานใช้ร่วม
|
|
6
|
+
*/
|
|
7
|
+
export type MonthKey = string;
|
|
8
|
+
|
|
9
|
+
const MONTH_KEY = /^(\d{4})-(\d{2})$/;
|
|
10
|
+
|
|
11
|
+
/** แยก `"YYYY-MM"` → ปี + เดือน (0–11); รูปแบบผิด = throw (ค่าจาก URL ให้เช็กด้วย `isMonthKey` ก่อน) */
|
|
12
|
+
export function parseMonthKey(key: MonthKey): { year: number; monthIndex: number } {
|
|
13
|
+
const m = MONTH_KEY.exec(key);
|
|
14
|
+
const monthIndex = m ? Number(m[2]) - 1 : Number.NaN;
|
|
15
|
+
if (!m || monthIndex < 0 || monthIndex > 11) {
|
|
16
|
+
throw new Error(`month ต้องเป็น "YYYY-MM" ได้ "${key}"`);
|
|
17
|
+
}
|
|
18
|
+
return { year: Number(m[1]), monthIndex };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function isMonthKey(value: unknown): value is MonthKey {
|
|
22
|
+
if (typeof value !== "string") return false;
|
|
23
|
+
const m = MONTH_KEY.exec(value);
|
|
24
|
+
return !!m && Number(m[2]) >= 1 && Number(m[2]) <= 12;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Date → `"YYYY-MM"` ตามเวลาท้องถิ่นของเครื่อง */
|
|
28
|
+
export function toMonthKey(date: Date): MonthKey {
|
|
29
|
+
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function currentMonthKey(now: Date = new Date()): MonthKey {
|
|
33
|
+
return toMonthKey(now);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** เลื่อนเดือน (ลบ = ย้อนหลัง) ข้ามปีให้เอง: shiftMonth("2026-01", -1) → "2025-12" */
|
|
37
|
+
export function shiftMonth(key: MonthKey, by: number): MonthKey {
|
|
38
|
+
const { year, monthIndex } = parseMonthKey(key);
|
|
39
|
+
return toMonthKey(new Date(year, monthIndex + by, 1));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** เทียบเดือน: < 0 = a ก่อน b (เทียบเป็นสตริงได้เพราะเติม 0 แล้ว) */
|
|
43
|
+
export function compareMonth(a: MonthKey, b: MonthKey): number {
|
|
44
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* ชื่อเดือนสำหรับแสดง — `th-TH` ได้ปี พ.ศ. ตาม Intl (`"ตุลาคม 2569"`), `en-US` ได้ ค.ศ.
|
|
49
|
+
* ต้องการ ค.ศ. ในภาษาไทย ใช้ locale `"th-TH-u-ca-gregory"`
|
|
50
|
+
*/
|
|
51
|
+
export function formatMonthKey(
|
|
52
|
+
key: MonthKey,
|
|
53
|
+
locale = "th-TH",
|
|
54
|
+
style: "long" | "short" = "long",
|
|
55
|
+
): string {
|
|
56
|
+
const { year, monthIndex } = parseMonthKey(key);
|
|
57
|
+
return getDateTimeFormat(locale, { month: style, year: "numeric" }).format(
|
|
58
|
+
new Date(year, monthIndex, 1),
|
|
59
|
+
);
|
|
60
|
+
}
|
package/src/styles.css
CHANGED
|
@@ -282,6 +282,16 @@
|
|
|
282
282
|
}
|
|
283
283
|
}
|
|
284
284
|
|
|
285
|
+
/* ขอบล่างของจอ iPhone (home indicator) — แถบที่ fixed ติดล่าง เช่น MobileTabBar */
|
|
286
|
+
@utility pb-safe {
|
|
287
|
+
padding-bottom: env(safe-area-inset-bottom, 0px);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/* เว้นที่ใต้เนื้อหาให้พ้น MobileTabBar (สูง ~4rem + safe-area) — AppShell mobileTabs ใส่ให้เอง */
|
|
291
|
+
@utility pb-tab-bar {
|
|
292
|
+
padding-bottom: calc(5rem + env(safe-area-inset-bottom, 0px));
|
|
293
|
+
}
|
|
294
|
+
|
|
285
295
|
@utility scrollbar-thin {
|
|
286
296
|
scrollbar-width: thin;
|
|
287
297
|
scrollbar-color: var(--scrollbar-thumb) transparent;
|