@moloodev/ui 0.0.0-stage → 0.2.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.
Files changed (93) hide show
  1. package/package.json +139 -3
  2. package/registry/components.ts +135 -0
  3. package/registry/fonts.css +81 -0
  4. package/registry/suit-OFL.txt +94 -0
  5. package/registry/wanted-sans-OFL.txt +93 -0
  6. package/src/components/ui/accordion.tsx +74 -0
  7. package/src/components/ui/alert-dialog.tsx +187 -0
  8. package/src/components/ui/alert.tsx +83 -0
  9. package/src/components/ui/aspect-ratio.tsx +22 -0
  10. package/src/components/ui/attachment.tsx +207 -0
  11. package/src/components/ui/avatar.tsx +109 -0
  12. package/src/components/ui/badge.tsx +54 -0
  13. package/src/components/ui/breadcrumb.tsx +126 -0
  14. package/src/components/ui/bubble.tsx +128 -0
  15. package/src/components/ui/button-group.tsx +103 -0
  16. package/src/components/ui/button.tsx +68 -0
  17. package/src/components/ui/calendar.tsx +222 -0
  18. package/src/components/ui/card.tsx +103 -0
  19. package/src/components/ui/carousel.tsx +242 -0
  20. package/src/components/ui/chart.tsx +398 -0
  21. package/src/components/ui/checkbox.tsx +28 -0
  22. package/src/components/ui/collapsible.tsx +40 -0
  23. package/src/components/ui/combobox.tsx +299 -0
  24. package/src/components/ui/command.tsx +218 -0
  25. package/src/components/ui/context-menu.tsx +271 -0
  26. package/src/components/ui/data-table.tsx +919 -0
  27. package/src/components/ui/date-picker.tsx +65 -0
  28. package/src/components/ui/dialog.tsx +224 -0
  29. package/src/components/ui/drawer.tsx +231 -0
  30. package/src/components/ui/dropdown-menu.tsx +268 -0
  31. package/src/components/ui/dropzone.tsx +186 -0
  32. package/src/components/ui/empty.tsx +101 -0
  33. package/src/components/ui/field.tsx +238 -0
  34. package/src/components/ui/form.tsx +83 -0
  35. package/src/components/ui/highlighted-source.tsx +200 -0
  36. package/src/components/ui/hover-card.tsx +51 -0
  37. package/src/components/ui/image-field.tsx +796 -0
  38. package/src/components/ui/input-group.tsx +149 -0
  39. package/src/components/ui/input-otp.tsx +86 -0
  40. package/src/components/ui/input-step.ts +19 -0
  41. package/src/components/ui/input.tsx +100 -0
  42. package/src/components/ui/item.tsx +201 -0
  43. package/src/components/ui/kbd.tsx +91 -0
  44. package/src/components/ui/kind-icon-glob.ts +24 -0
  45. package/src/components/ui/kind-icon-load.ts +52 -0
  46. package/src/components/ui/kind-icon-static.ts +46 -0
  47. package/src/components/ui/kind-icon.tsx +84 -0
  48. package/src/components/ui/kind-mark.tsx +65 -0
  49. package/src/components/ui/label.tsx +29 -0
  50. package/src/components/ui/marker.tsx +71 -0
  51. package/src/components/ui/menubar.tsx +277 -0
  52. package/src/components/ui/message-scroller.tsx +137 -0
  53. package/src/components/ui/message.tsx +92 -0
  54. package/src/components/ui/moloo-logo.tsx +45 -0
  55. package/src/components/ui/native-select.tsx +61 -0
  56. package/src/components/ui/navigation-menu.tsx +168 -0
  57. package/src/components/ui/outline-list.tsx +73 -0
  58. package/src/components/ui/pagination.tsx +130 -0
  59. package/src/components/ui/panel.tsx +108 -0
  60. package/src/components/ui/popover.tsx +90 -0
  61. package/src/components/ui/progress.tsx +117 -0
  62. package/src/components/ui/radio-group.tsx +38 -0
  63. package/src/components/ui/resizable.tsx +52 -0
  64. package/src/components/ui/scroll-area.tsx +54 -0
  65. package/src/components/ui/select.tsx +205 -0
  66. package/src/components/ui/separator.tsx +25 -0
  67. package/src/components/ui/sheet.tsx +140 -0
  68. package/src/components/ui/sidebar.tsx +746 -0
  69. package/src/components/ui/skeleton.tsx +13 -0
  70. package/src/components/ui/slider.tsx +52 -0
  71. package/src/components/ui/spinner.tsx +18 -0
  72. package/src/components/ui/switch.tsx +32 -0
  73. package/src/components/ui/table.tsx +116 -0
  74. package/src/components/ui/tabs.tsx +83 -0
  75. package/src/components/ui/tag-list-input.tsx +194 -0
  76. package/src/components/ui/textarea.tsx +18 -0
  77. package/src/components/ui/timeline.tsx +265 -0
  78. package/src/components/ui/toast.tsx +237 -0
  79. package/src/components/ui/toggle-group.tsx +90 -0
  80. package/src/components/ui/toggle.tsx +45 -0
  81. package/src/components/ui/tooltip.tsx +66 -0
  82. package/src/css.d.ts +1 -0
  83. package/src/hooks/use-clipboard.ts +82 -0
  84. package/src/hooks/use-mobile.ts +21 -0
  85. package/src/index.ts +106 -0
  86. package/src/kind-icon.ts +4 -0
  87. package/src/lib/format-relative-time.ts +41 -0
  88. package/src/lib/list-selection.ts +61 -0
  89. package/src/lib/utils.ts +6 -0
  90. package/src/moloo.css +360 -0
  91. package/src/styles.css +36 -0
  92. package/src/typeset.css +518 -0
  93. package/README.md +0 -3
@@ -0,0 +1,137 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import {
5
+ MessageScroller as MessageScrollerPrimitive,
6
+ useMessageScroller,
7
+ useMessageScrollerScrollable,
8
+ useMessageScrollerVisibility,
9
+ } from "@shadcn/react/message-scroller"
10
+
11
+ import { cn } from "../../lib/utils"
12
+ import { Button } from "./button"
13
+ import { IconArrowDown } from "@tabler/icons-react"
14
+
15
+ function MessageScrollerProvider(
16
+ props: React.ComponentProps<typeof MessageScrollerPrimitive.Provider>
17
+ ) {
18
+ return <MessageScrollerPrimitive.Provider {...props} />
19
+ }
20
+
21
+ function MessageScroller({
22
+ className,
23
+ ...props
24
+ }: React.ComponentProps<typeof MessageScrollerPrimitive.Root>) {
25
+ return (
26
+ <MessageScrollerPrimitive.Root
27
+ data-slot="message-scroller"
28
+ className={cn(
29
+ "group/message-scroller relative flex size-full min-h-0 flex-col overflow-hidden",
30
+ className
31
+ )}
32
+ {...props}
33
+ />
34
+ )
35
+ }
36
+
37
+ function MessageScrollerViewport({
38
+ className,
39
+ ...props
40
+ }: React.ComponentProps<typeof MessageScrollerPrimitive.Viewport>) {
41
+ return (
42
+ <MessageScrollerPrimitive.Viewport
43
+ data-slot="message-scroller-viewport"
44
+ className={cn(
45
+ "size-full min-h-0 min-w-0 scroll-fade-b scrollbar-thin scrollbar-gutter-stable overflow-y-auto overscroll-contain contain-content data-autoscrolling:scrollbar-thumb-transparent data-autoscrolling:scrollbar-track-transparent",
46
+ className
47
+ )}
48
+ {...props}
49
+ />
50
+ )
51
+ }
52
+
53
+ function MessageScrollerContent({
54
+ className,
55
+ ...props
56
+ }: React.ComponentProps<typeof MessageScrollerPrimitive.Content>) {
57
+ return (
58
+ <MessageScrollerPrimitive.Content
59
+ data-slot="message-scroller-content"
60
+ className={cn("flex h-max min-h-full flex-col gap-6", className)}
61
+ {...props}
62
+ />
63
+ )
64
+ }
65
+
66
+ function MessageScrollerItem({
67
+ className,
68
+ scrollAnchor = false,
69
+ ...props
70
+ }: React.ComponentProps<typeof MessageScrollerPrimitive.Item>) {
71
+ return (
72
+ <MessageScrollerPrimitive.Item
73
+ data-slot="message-scroller-item"
74
+ scrollAnchor={scrollAnchor}
75
+ className={cn(
76
+ "flex min-w-0 shrink-0 flex-col [contain-intrinsic-size:auto_10rem] [content-visibility:auto]",
77
+ className
78
+ )}
79
+ {...props}
80
+ />
81
+ )
82
+ }
83
+
84
+ function MessageScrollerButton({
85
+ direction = "end",
86
+ className,
87
+ children,
88
+ render,
89
+ variant = "outline",
90
+ size = "icon-sm",
91
+ ...props
92
+ }: React.ComponentProps<typeof MessageScrollerPrimitive.Button> &
93
+ Pick<React.ComponentProps<typeof Button>, "variant" | "size">) {
94
+ return (
95
+ <MessageScrollerPrimitive.Button
96
+ data-slot="message-scroller-button"
97
+ data-direction={direction}
98
+ data-variant={variant}
99
+ data-size={size}
100
+ direction={direction}
101
+ className={cn(
102
+ // Position + motion only. Chrome comes from Button outline
103
+ // (border-2 + shadow-moloo); bg-background keeps it visible on card surfaces.
104
+ "absolute inset-s-1/2 z-10 -translate-x-1/2 bg-background transition-[translate,scale,opacity] duration-200",
105
+ "data-[active=false]:pointer-events-none data-[active=false]:scale-95 data-[active=false]:opacity-0 data-[active=false]:duration-400 data-[active=false]:ease-[cubic-bezier(0.7,0,0.84,0)]",
106
+ "data-[active=true]:translate-y-0 data-[active=true]:scale-100 data-[active=true]:opacity-100 data-[active=true]:ease-[cubic-bezier(0.23,1,0.32,1)]",
107
+ "data-[direction=end]:bottom-4 data-[direction=end]:data-[active=false]:translate-y-full",
108
+ "data-[direction=start]:top-4 data-[direction=start]:data-[active=false]:-translate-y-full",
109
+ "rtl:translate-x-1/2 data-[direction=start]:[&_svg]:rotate-180",
110
+ className
111
+ )}
112
+ render={render ?? <Button variant={variant} size={size} />}
113
+ {...props}
114
+ >
115
+ {children ?? (
116
+ <>
117
+ <IconArrowDown />
118
+ <span className="sr-only">
119
+ {direction === "end" ? "Scroll to end" : "Scroll to start"}
120
+ </span>
121
+ </>
122
+ )}
123
+ </MessageScrollerPrimitive.Button>
124
+ )
125
+ }
126
+
127
+ export {
128
+ MessageScrollerProvider,
129
+ MessageScroller,
130
+ MessageScrollerViewport,
131
+ MessageScrollerContent,
132
+ MessageScrollerItem,
133
+ MessageScrollerButton,
134
+ useMessageScroller,
135
+ useMessageScrollerScrollable,
136
+ useMessageScrollerVisibility,
137
+ }
@@ -0,0 +1,92 @@
1
+ import * as React from "react"
2
+
3
+ import { cn } from "../../lib/utils"
4
+
5
+ function MessageGroup({ className, ...props }: React.ComponentProps<"div">) {
6
+ return (
7
+ <div
8
+ data-slot="message-group"
9
+ className={cn("flex min-w-0 flex-col gap-1.5", className)}
10
+ {...props}
11
+ />
12
+ )
13
+ }
14
+
15
+ function Message({
16
+ className,
17
+ align = "start",
18
+ ...props
19
+ }: React.ComponentProps<"div"> & { align?: "start" | "end" }) {
20
+ return (
21
+ <div
22
+ data-slot="message"
23
+ data-align={align}
24
+ className={cn(
25
+ "group/message relative flex w-full min-w-0 gap-1.5 text-sm data-[align=end]:flex-row-reverse",
26
+ className
27
+ )}
28
+ {...props}
29
+ />
30
+ )
31
+ }
32
+
33
+ function MessageAvatar({ className, ...props }: React.ComponentProps<"div">) {
34
+ return (
35
+ <div
36
+ data-slot="message-avatar"
37
+ className={cn(
38
+ "flex w-fit min-w-8 shrink-0 items-center justify-center self-end overflow-hidden rounded-full bg-muted group-has-data-[slot=message-footer]/message:-translate-y-6",
39
+ className
40
+ )}
41
+ {...props}
42
+ />
43
+ )
44
+ }
45
+
46
+ function MessageContent({ className, ...props }: React.ComponentProps<"div">) {
47
+ return (
48
+ <div
49
+ data-slot="message-content"
50
+ className={cn(
51
+ "flex w-full min-w-0 flex-col gap-1 wrap-break-word group-data-[align=end]/message:*:data-slot:self-end",
52
+ className
53
+ )}
54
+ {...props}
55
+ />
56
+ )
57
+ }
58
+
59
+ function MessageHeader({ className, ...props }: React.ComponentProps<"div">) {
60
+ return (
61
+ <div
62
+ data-slot="message-header"
63
+ className={cn(
64
+ "flex max-w-full min-w-0 items-center px-2.5 text-xs font-semibold text-muted-foreground group-has-data-[variant=ghost]/message:px-0",
65
+ className
66
+ )}
67
+ {...props}
68
+ />
69
+ )
70
+ }
71
+
72
+ function MessageFooter({ className, ...props }: React.ComponentProps<"div">) {
73
+ return (
74
+ <div
75
+ data-slot="message-footer"
76
+ className={cn(
77
+ "flex max-w-full min-w-0 items-center px-2.5 text-xs font-semibold text-muted-foreground group-has-data-[variant=ghost]/message:px-0 group-data-[align=end]/message:justify-end",
78
+ className
79
+ )}
80
+ {...props}
81
+ />
82
+ )
83
+ }
84
+
85
+ export {
86
+ MessageGroup,
87
+ Message,
88
+ MessageAvatar,
89
+ MessageContent,
90
+ MessageFooter,
91
+ MessageHeader,
92
+ }
@@ -0,0 +1,45 @@
1
+ import { cn } from "../../lib/utils"
2
+
3
+ function MolooLogo({
4
+ className,
5
+ decorative = false,
6
+ title,
7
+ "aria-label": ariaLabel,
8
+ ...props
9
+ }: React.ComponentProps<"svg"> & {
10
+ /** Hide the mark from assistive technology. */
11
+ decorative?: boolean
12
+ /** SVG title. Tooltip, and the accessible name when aria-label is omitted. */
13
+ title?: string
14
+ }) {
15
+ const label = ariaLabel ?? title ?? "moloo"
16
+ const titleText = decorative ? title : (title ?? label)
17
+ return (
18
+ <svg
19
+ xmlns="http://www.w3.org/2000/svg"
20
+ viewBox="0 0 64 64"
21
+ data-slot="moloo-logo"
22
+ className={cn("inline-block size-6 shrink-0", className)}
23
+ {...props}
24
+ role={decorative ? undefined : "img"}
25
+ aria-hidden={decorative ? true : undefined}
26
+ aria-label={decorative ? undefined : label}
27
+ >
28
+ {titleText ? <title>{titleText}</title> : null}
29
+ <rect className="fill-primary" x={34} y={4} width={26} height={26} rx={6} />
30
+ <rect className="fill-primary" x={34} y={34} width={26} height={26} rx={6} />
31
+ <rect className="fill-primary" x={4} y={34} width={26} height={26} rx={6} />
32
+ <rect
33
+ className="fill-accent"
34
+ x={2}
35
+ y={2}
36
+ width={26}
37
+ height={26}
38
+ rx={6}
39
+ transform="rotate(-12 15 15)"
40
+ />
41
+ </svg>
42
+ )
43
+ }
44
+
45
+ export { MolooLogo }
@@ -0,0 +1,61 @@
1
+ import * as React from "react"
2
+
3
+ import { cn } from "../../lib/utils"
4
+ import { IconChevronDown } from "@tabler/icons-react"
5
+
6
+ type NativeSelectProps = Omit<React.ComponentProps<"select">, "size"> & {
7
+ size?: "sm" | "default"
8
+ }
9
+
10
+ function NativeSelect({
11
+ className,
12
+ size = "default",
13
+ ...props
14
+ }: NativeSelectProps) {
15
+ return (
16
+ <div
17
+ className={cn(
18
+ "moloo-field moloo-focus-within group/native-select relative w-fit has-[select:disabled]:opacity-50 has-[select[aria-invalid=true]]:border-destructive has-[select[aria-invalid=true]]:shadow-[var(--moloo-invalid-shadow)]",
19
+ className
20
+ )}
21
+ data-slot="native-select-wrapper"
22
+ data-size={size}
23
+ >
24
+ <select
25
+ data-slot="native-select"
26
+ data-size={size}
27
+ className="h-9 w-full min-w-0 appearance-none rounded-md border-0 bg-transparent py-1 pr-8 pl-2.5 text-sm font-semibold whitespace-nowrap text-foreground transition-colors outline-none select-none selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed data-[size=sm]:h-8 dark:hover:bg-input/50"
28
+ {...props}
29
+ />
30
+ <IconChevronDown className="pointer-events-none absolute top-1/2 right-2.5 size-4 -translate-y-1/2 text-muted-foreground select-none" aria-hidden="true" data-slot="native-select-icon" />
31
+ </div>
32
+ )
33
+ }
34
+
35
+ function NativeSelectOption({
36
+ className,
37
+ ...props
38
+ }: React.ComponentProps<"option">) {
39
+ return (
40
+ <option
41
+ data-slot="native-select-option"
42
+ className={cn("bg-[Canvas] text-[CanvasText]", className)}
43
+ {...props}
44
+ />
45
+ )
46
+ }
47
+
48
+ function NativeSelectOptGroup({
49
+ className,
50
+ ...props
51
+ }: React.ComponentProps<"optgroup">) {
52
+ return (
53
+ <optgroup
54
+ data-slot="native-select-optgroup"
55
+ className={cn("bg-[Canvas] text-[CanvasText]", className)}
56
+ {...props}
57
+ />
58
+ )
59
+ }
60
+
61
+ export { NativeSelect, NativeSelectOptGroup, NativeSelectOption }
@@ -0,0 +1,168 @@
1
+ import { NavigationMenu as NavigationMenuPrimitive } from "@base-ui/react/navigation-menu"
2
+ import { cva } from "class-variance-authority"
3
+
4
+ import { cn } from "../../lib/utils"
5
+ import { IconChevronDown } from "@tabler/icons-react"
6
+
7
+ function NavigationMenu({
8
+ align = "start",
9
+ className,
10
+ children,
11
+ ...props
12
+ }: NavigationMenuPrimitive.Root.Props &
13
+ Pick<NavigationMenuPrimitive.Positioner.Props, "align">) {
14
+ return (
15
+ <NavigationMenuPrimitive.Root
16
+ data-slot="navigation-menu"
17
+ className={cn(
18
+ "group/navigation-menu relative flex max-w-max flex-1 items-center justify-center",
19
+ className
20
+ )}
21
+ {...props}
22
+ >
23
+ {children}
24
+ <NavigationMenuPositioner align={align} />
25
+ </NavigationMenuPrimitive.Root>
26
+ )
27
+ }
28
+
29
+ function NavigationMenuList({
30
+ className,
31
+ ...props
32
+ }: React.ComponentPropsWithRef<typeof NavigationMenuPrimitive.List>) {
33
+ return (
34
+ <NavigationMenuPrimitive.List
35
+ data-slot="navigation-menu-list"
36
+ className={cn(
37
+ "group flex flex-1 list-none items-center justify-center gap-0",
38
+ className
39
+ )}
40
+ {...props}
41
+ />
42
+ )
43
+ }
44
+
45
+ function NavigationMenuItem({
46
+ className,
47
+ ...props
48
+ }: React.ComponentPropsWithRef<typeof NavigationMenuPrimitive.Item>) {
49
+ return (
50
+ <NavigationMenuPrimitive.Item
51
+ data-slot="navigation-menu-item"
52
+ className={cn("relative", className)}
53
+ {...props}
54
+ />
55
+ )
56
+ }
57
+
58
+ const navigationMenuTriggerStyle = cva(
59
+ "moloo-focus group/navigation-menu-trigger inline-flex h-9 w-max items-center justify-center rounded-md px-2.5 py-1.5 font-display text-sm font-bold tracking-[-0.02em] transition-all outline-none hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-popup-open:bg-accent data-popup-open:text-accent-foreground data-popup-open:hover:bg-accent data-open:bg-accent data-open:text-accent-foreground data-open:hover:bg-accent data-open:focus:bg-accent"
60
+ )
61
+
62
+ function NavigationMenuTrigger({
63
+ className,
64
+ children,
65
+ ...props
66
+ }: NavigationMenuPrimitive.Trigger.Props) {
67
+ return (
68
+ <NavigationMenuPrimitive.Trigger
69
+ data-slot="navigation-menu-trigger"
70
+ className={cn(navigationMenuTriggerStyle(), "group", className)}
71
+ {...props}
72
+ >
73
+ {children}{" "}
74
+ <IconChevronDown className="relative top-px ml-1 size-3 transition duration-300 group-data-popup-open/navigation-menu-trigger:rotate-180 group-data-open/navigation-menu-trigger:rotate-180" aria-hidden="true" />
75
+ </NavigationMenuPrimitive.Trigger>
76
+ )
77
+ }
78
+
79
+ function NavigationMenuContent({
80
+ className,
81
+ ...props
82
+ }: NavigationMenuPrimitive.Content.Props) {
83
+ return (
84
+ <NavigationMenuPrimitive.Content
85
+ data-slot="navigation-menu-content"
86
+ className={cn(
87
+ "data-ending-style:data-activation-direction=left:translate-x-[50%] data-ending-style:data-activation-direction=right:translate-x-[-50%] data-starting-style:data-activation-direction=left:translate-x-[-50%] data-starting-style:data-activation-direction=right:translate-x-[50%] h-full w-auto p-1 transition-[opacity,transform,translate] duration-[0.35s] ease-[cubic-bezier(0.22,1,0.36,1)] group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:moloo-surface bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:shadow-moloo-lg group-data-[viewport=false]/navigation-menu:duration-300 data-ending-style:opacity-0 data-starting-style:opacity-0 data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 data-[motion^=from-]:animate-in data-[motion^=from-]:fade-in data-[motion^=to-]:animate-out data-[motion^=to-]:fade-out **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none group-data-[viewport=false]/navigation-menu:data-open:animate-in group-data-[viewport=false]/navigation-menu:data-open:fade-in-0 group-data-[viewport=false]/navigation-menu:data-open:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-closed:animate-out group-data-[viewport=false]/navigation-menu:data-closed:fade-out-0 group-data-[viewport=false]/navigation-menu:data-closed:zoom-out-95",
88
+ className
89
+ )}
90
+ {...props}
91
+ />
92
+ )
93
+ }
94
+
95
+ function NavigationMenuPositioner({
96
+ className,
97
+ side = "bottom",
98
+ sideOffset = 8,
99
+ align = "start",
100
+ alignOffset = 0,
101
+ ...props
102
+ }: NavigationMenuPrimitive.Positioner.Props) {
103
+ return (
104
+ <NavigationMenuPrimitive.Portal>
105
+ <NavigationMenuPrimitive.Positioner
106
+ side={side}
107
+ sideOffset={sideOffset}
108
+ align={align}
109
+ alignOffset={alignOffset}
110
+ className={cn(
111
+ "isolate z-50 h-(--positioner-height) w-(--positioner-width) max-w-(--available-width) transition-[top,left,right,bottom] duration-[0.35s] ease-[cubic-bezier(0.22,1,0.36,1)] data-instant:transition-none data-[side=bottom]:before:top-[-10px] data-[side=bottom]:before:right-0 data-[side=bottom]:before:left-0",
112
+ className
113
+ )}
114
+ {...props}
115
+ >
116
+ <NavigationMenuPrimitive.Popup className="data-[ending-style]:easing-[ease] xs:w-(--popup-width) relative h-(--popup-height) w-(--popup-width) origin-(--transform-origin) overflow-hidden rounded-md moloo-surface bg-popover text-popover-foreground shadow-moloo-lg transition-[opacity,transform,width,height,scale,translate] duration-[0.35s] ease-[cubic-bezier(0.22,1,0.36,1)] outline-none data-ending-style:scale-90 data-ending-style:opacity-0 data-ending-style:duration-150 data-starting-style:scale-90 data-starting-style:opacity-0">
117
+ <NavigationMenuPrimitive.Viewport className="relative size-full overflow-hidden rounded-md" />
118
+ </NavigationMenuPrimitive.Popup>
119
+ </NavigationMenuPrimitive.Positioner>
120
+ </NavigationMenuPrimitive.Portal>
121
+ )
122
+ }
123
+
124
+ function NavigationMenuLink({
125
+ className,
126
+ ...props
127
+ }: NavigationMenuPrimitive.Link.Props) {
128
+ return (
129
+ <NavigationMenuPrimitive.Link
130
+ data-slot="navigation-menu-link"
131
+ className={cn(
132
+ "moloo-focus flex items-center gap-2 rounded-md p-2 text-sm font-semibold transition-all outline-none hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground in-data-[slot=navigation-menu-content]:rounded-md data-active:bg-accent data-active:text-accent-foreground data-active:hover:bg-accent data-active:focus:bg-accent [&_svg:not([class*='size-'])]:size-4",
133
+ className
134
+ )}
135
+ {...props}
136
+ />
137
+ )
138
+ }
139
+
140
+ function NavigationMenuIndicator({
141
+ className,
142
+ ...props
143
+ }: React.ComponentPropsWithRef<typeof NavigationMenuPrimitive.Icon>) {
144
+ return (
145
+ <NavigationMenuPrimitive.Icon
146
+ data-slot="navigation-menu-indicator"
147
+ className={cn(
148
+ "top-full z-1 flex h-1.5 items-end justify-center overflow-hidden data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:animate-in data-[state=visible]:fade-in",
149
+ className
150
+ )}
151
+ {...props}
152
+ >
153
+ <div className="relative top-[60%] h-2 w-2 rotate-45 rounded-md bg-border shadow-moloo-lg" />
154
+ </NavigationMenuPrimitive.Icon>
155
+ )
156
+ }
157
+
158
+ export {
159
+ NavigationMenu,
160
+ NavigationMenuContent,
161
+ NavigationMenuIndicator,
162
+ NavigationMenuItem,
163
+ NavigationMenuLink,
164
+ NavigationMenuList,
165
+ NavigationMenuTrigger,
166
+ navigationMenuTriggerStyle,
167
+ NavigationMenuPositioner,
168
+ }
@@ -0,0 +1,73 @@
1
+ import type { ReactNode } from "react"
2
+
3
+ import { cn } from "../../lib/utils"
4
+
5
+ export type OutlineListItem = {
6
+ id: string
7
+ label: string
8
+ level: number
9
+ }
10
+
11
+ export type OutlineListProps = {
12
+ items: readonly OutlineListItem[]
13
+ onSelect: (id: string) => void
14
+ empty?: ReactNode
15
+ className?: string
16
+ }
17
+
18
+ function levelIndent(level: number) {
19
+ return `${0.5 + Math.max(level - 1, 0) * 0.625}rem`
20
+ }
21
+
22
+ function levelTone(level: number) {
23
+ if (level <= 1) return "text-foreground"
24
+ if (level === 2) return "text-foreground/90"
25
+ if (level >= 3) return "text-muted-foreground"
26
+ return null
27
+ }
28
+
29
+ function ariaLevel(level: number) {
30
+ if (!Number.isFinite(level)) return undefined
31
+ return Math.max(1, Math.trunc(level))
32
+ }
33
+
34
+ export function OutlineList({
35
+ items,
36
+ onSelect,
37
+ empty,
38
+ className,
39
+ }: OutlineListProps) {
40
+ return (
41
+ <ul
42
+ data-slot="outline-list"
43
+ className={cn(
44
+ "flex min-h-0 flex-1 list-none flex-col gap-0.5 overflow-auto p-1.5",
45
+ className
46
+ )}
47
+ >
48
+ {items.length === 0 ? (
49
+ empty ? (
50
+ <li className="px-2 py-1 text-xs text-muted-foreground">{empty}</li>
51
+ ) : null
52
+ ) : (
53
+ items.map((item) => (
54
+ <li key={item.id} aria-level={ariaLevel(item.level)}>
55
+ <button
56
+ type="button"
57
+ data-slot="outline-list-item"
58
+ onClick={() => onSelect(item.id)}
59
+ style={{ paddingInlineStart: levelIndent(item.level) }}
60
+ className={cn(
61
+ "moloo-focus flex w-full rounded-md py-1 pr-2 text-left font-display text-xs font-semibold tracking-[-0.02em] outline-none transition-colors",
62
+ "hover:bg-muted/60",
63
+ levelTone(item.level)
64
+ )}
65
+ >
66
+ {item.label}
67
+ </button>
68
+ </li>
69
+ ))
70
+ )}
71
+ </ul>
72
+ )
73
+ }