@suphark/ui 0.19.0 → 0.20.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.
Files changed (74) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/package.json +14 -13
  3. package/scripts/audit.mjs +0 -0
  4. package/src/components/app-shell/app-shell.tsx +12 -0
  5. package/src/components/app-shell/index.ts +1 -0
  6. package/src/components/app-shell/mobile-tab-bar.tsx +104 -0
  7. package/src/components/shared/action-menu.tsx +157 -0
  8. package/src/components/shared/button/confirm-action-button.tsx +2 -0
  9. package/src/components/shared/filter-bar.tsx +108 -0
  10. package/src/components/shared/month-stepper.tsx +124 -0
  11. package/src/components/shared/row-actions.tsx +2 -0
  12. package/src/components/shared/segmented-control.tsx +73 -0
  13. package/src/components/shared/settings-layout.tsx +170 -0
  14. package/src/components/shared/setup-checklist.tsx +104 -0
  15. package/src/components/shared/simple-dialog.tsx +172 -0
  16. package/src/components/shared/simple-pagination.tsx +109 -0
  17. package/src/components/shared/simple-table.tsx +93 -0
  18. package/src/components/shared/simple-tabs.tsx +118 -0
  19. package/src/components/shared/simple-tooltip.tsx +87 -0
  20. package/src/components/shared/ui-text.tsx +28 -0
  21. package/src/components/ui/accordion.tsx +2 -2
  22. package/src/components/ui/alert-dialog.tsx +4 -4
  23. package/src/components/ui/alert.tsx +2 -2
  24. package/src/components/ui/attachment.tsx +9 -9
  25. package/src/components/ui/avatar.tsx +4 -4
  26. package/src/components/ui/badge.tsx +1 -1
  27. package/src/components/ui/breadcrumb.tsx +1 -1
  28. package/src/components/ui/bubble.tsx +123 -0
  29. package/src/components/ui/button-group.tsx +2 -2
  30. package/src/components/ui/button.tsx +6 -6
  31. package/src/components/ui/calendar.tsx +9 -9
  32. package/src/components/ui/card.tsx +3 -3
  33. package/src/components/ui/chart.tsx +2 -2
  34. package/src/components/ui/checkbox.tsx +1 -1
  35. package/src/components/ui/combobox.tsx +6 -6
  36. package/src/components/ui/command.tsx +4 -4
  37. package/src/components/ui/context-menu.tsx +7 -7
  38. package/src/components/ui/dialog.tsx +4 -4
  39. package/src/components/ui/direction.tsx +6 -0
  40. package/src/components/ui/drawer.tsx +10 -10
  41. package/src/components/ui/dropdown-menu.tsx +8 -8
  42. package/src/components/ui/empty.tsx +4 -4
  43. package/src/components/ui/field.tsx +8 -8
  44. package/src/components/ui/hover-card.tsx +1 -2
  45. package/src/components/ui/input-group.tsx +3 -3
  46. package/src/components/ui/input-otp.tsx +1 -1
  47. package/src/components/ui/input.tsx +1 -1
  48. package/src/components/ui/item.tsx +4 -4
  49. package/src/components/ui/kbd.tsx +1 -1
  50. package/src/components/ui/label.tsx +1 -1
  51. package/src/components/ui/marker.tsx +67 -0
  52. package/src/components/ui/menubar.tsx +9 -9
  53. package/src/components/ui/message-scroller.tsx +129 -0
  54. package/src/components/ui/message.tsx +84 -0
  55. package/src/components/ui/native-select.tsx +1 -1
  56. package/src/components/ui/navigation-menu.tsx +6 -6
  57. package/src/components/ui/popover.tsx +1 -1
  58. package/src/components/ui/progress.tsx +2 -3
  59. package/src/components/ui/questionnaire.tsx +314 -0
  60. package/src/components/ui/radio-group.tsx +1 -2
  61. package/src/components/ui/resizable.tsx +1 -1
  62. package/src/components/ui/select.tsx +4 -4
  63. package/src/components/ui/separator.tsx +0 -1
  64. package/src/components/ui/sheet.tsx +3 -3
  65. package/src/components/ui/sidebar.tsx +14 -14
  66. package/src/components/ui/switch.tsx +2 -3
  67. package/src/components/ui/textarea.tsx +1 -1
  68. package/src/components/ui/toast.tsx +221 -0
  69. package/src/components/ui/toggle-group.tsx +2 -2
  70. package/src/components/ui/toggle.tsx +1 -1
  71. package/src/components/ui/tooltip.tsx +0 -1
  72. package/src/index.ts +41 -0
  73. package/src/lib/month.ts +60 -0
  74. 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=right]:right-0 data-[side=left]:left-0 data-[side=right]:group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)] data-[side=left]:group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)] md:flex",
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 group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex ltr:-translate-x-1/2 rtl:-translate-x-1/2",
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 hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:after:left-full",
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-[state=collapsed]:ml-2 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",
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 text-xs outline-hidden ring-sidebar-ring transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0 [&>svg]:size-4 [&>svg]:shrink-0",
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 outline-hidden ring-sidebar-ring transition-transform after:absolute after:-inset-2 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 group-data-[collapsible=icon]:hidden md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0",
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 outline-hidden ring-sidebar-ring transition-[width,height,padding] 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 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-active:bg-sidebar-accent data-active:font-medium data-active:text-sidebar-accent-foreground data-open:hover:bg-sidebar-accent data-open:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&_svg]:size-4 [&_svg]:shrink-0",
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 outline-hidden ring-sidebar-ring transition-transform after:absolute after:-inset-2 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground group-data-[collapsible=icon]:hidden 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 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0",
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 aria-expanded:opacity-100 peer-data-active/menu-button:text-sidebar-accent-foreground md:opacity-0",
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 select-none items-center justify-center rounded-md px-1 font-medium text-sidebar-foreground text-xs tabular-nums peer-hover/menu-button:text-sidebar-accent-foreground group-data-[collapsible=icon]:hidden 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",
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 border-l px-2.5 py-0.5 group-data-[collapsible=icon]:hidden",
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 ring-sidebar-ring 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-active:bg-sidebar-accent data-[size=md]:text-sm data-[size=sm]:text-xs data-active:text-sidebar-accent-foreground group-data-[collapsible=icon]:hidden [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
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 transition-all after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 group-has-[:focus-visible]/field-label:border-transparent group-has-[:focus-visible]/field-label:ring-0 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-[size=default]:h-[18.4px] data-[size=sm]:h-[14px] data-[size=default]:w-[32px] data-[size=sm]:w-[24px] data-disabled:cursor-not-allowed data-checked:bg-primary data-unchecked:bg-input data-disabled:opacity-50 dark:data-unchecked:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
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=default]/switch:data-unchecked:translate-x-0 group-data-[size=sm]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=sm]/switch:data-unchecked:translate-x-0 dark:data-checked:bg-primary-foreground dark:data-unchecked:bg-foreground"
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 flex min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-base outline-none transition-colors 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:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 dark:disabled:bg-input/80",
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-vertical:flex-col data-vertical:items-stretch data-[size=sm]:rounded-[min(var(--radius-md),10px)]",
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 focus:z-10 focus-visible:z-10 group-data-[spacing=0]/toggle-group:rounded-none 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]:border-l-0 group-data-[spacing=0]/toggle-group:px-2 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]:last:rounded-r-lg group-data-vertical/toggle-group:data-[spacing=0]:last:rounded-b-lg group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-vertical/toggle-group:data-[spacing=0]:first:rounded-t-lg group-data-horizontal/toggle-group:data-[spacing=0]:first:rounded-l-lg",
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 whitespace-nowrap rounded-lg font-medium text-sm outline-none transition-all 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-pressed:bg-muted aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=on]:bg-muted dark:aria-invalid:ring-destructive/40 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
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: {
@@ -1,7 +1,6 @@
1
1
  "use client";
2
2
 
3
3
  import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip";
4
-
5
4
  import { cn } from "@suphark/ui/lib/utils";
6
5
 
7
6
  function TooltipProvider({ delay = 0, ...props }: TooltipPrimitive.Provider.Props) {
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,
@@ -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;