@moloodev/ui 0.0.0-stage → 0.1.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/package.json +139 -3
- package/registry/components.ts +135 -0
- package/registry/fonts.css +81 -0
- package/registry/suit-OFL.txt +94 -0
- package/registry/wanted-sans-OFL.txt +93 -0
- package/src/components/ui/accordion.tsx +74 -0
- package/src/components/ui/alert-dialog.tsx +187 -0
- package/src/components/ui/alert.tsx +83 -0
- package/src/components/ui/aspect-ratio.tsx +22 -0
- package/src/components/ui/attachment.tsx +207 -0
- package/src/components/ui/avatar.tsx +109 -0
- package/src/components/ui/badge.tsx +54 -0
- package/src/components/ui/breadcrumb.tsx +126 -0
- package/src/components/ui/bubble.tsx +128 -0
- package/src/components/ui/button-group.tsx +103 -0
- package/src/components/ui/button.tsx +68 -0
- package/src/components/ui/calendar.tsx +222 -0
- package/src/components/ui/card.tsx +103 -0
- package/src/components/ui/carousel.tsx +242 -0
- package/src/components/ui/chart.tsx +398 -0
- package/src/components/ui/checkbox.tsx +28 -0
- package/src/components/ui/collapsible.tsx +40 -0
- package/src/components/ui/combobox.tsx +299 -0
- package/src/components/ui/command.tsx +218 -0
- package/src/components/ui/context-menu.tsx +271 -0
- package/src/components/ui/data-table.tsx +919 -0
- package/src/components/ui/date-picker.tsx +65 -0
- package/src/components/ui/dialog.tsx +224 -0
- package/src/components/ui/drawer.tsx +231 -0
- package/src/components/ui/dropdown-menu.tsx +268 -0
- package/src/components/ui/dropzone.tsx +186 -0
- package/src/components/ui/empty.tsx +101 -0
- package/src/components/ui/field.tsx +238 -0
- package/src/components/ui/form.tsx +83 -0
- package/src/components/ui/highlighted-source.tsx +200 -0
- package/src/components/ui/hover-card.tsx +51 -0
- package/src/components/ui/image-field.tsx +796 -0
- package/src/components/ui/input-group.tsx +149 -0
- package/src/components/ui/input-otp.tsx +86 -0
- package/src/components/ui/input-step.ts +19 -0
- package/src/components/ui/input.tsx +100 -0
- package/src/components/ui/item.tsx +201 -0
- package/src/components/ui/kbd.tsx +91 -0
- package/src/components/ui/kind-icon-glob.ts +24 -0
- package/src/components/ui/kind-icon-load.ts +52 -0
- package/src/components/ui/kind-icon-static.ts +46 -0
- package/src/components/ui/kind-icon.tsx +84 -0
- package/src/components/ui/kind-mark.tsx +65 -0
- package/src/components/ui/label.tsx +29 -0
- package/src/components/ui/marker.tsx +71 -0
- package/src/components/ui/menubar.tsx +277 -0
- package/src/components/ui/message-scroller.tsx +137 -0
- package/src/components/ui/message.tsx +92 -0
- package/src/components/ui/moloo-logo.tsx +45 -0
- package/src/components/ui/native-select.tsx +61 -0
- package/src/components/ui/navigation-menu.tsx +168 -0
- package/src/components/ui/outline-list.tsx +73 -0
- package/src/components/ui/pagination.tsx +130 -0
- package/src/components/ui/panel.tsx +108 -0
- package/src/components/ui/popover.tsx +90 -0
- package/src/components/ui/progress.tsx +117 -0
- package/src/components/ui/radio-group.tsx +38 -0
- package/src/components/ui/resizable.tsx +52 -0
- package/src/components/ui/scroll-area.tsx +54 -0
- package/src/components/ui/select.tsx +205 -0
- package/src/components/ui/separator.tsx +25 -0
- package/src/components/ui/sheet.tsx +140 -0
- package/src/components/ui/sidebar.tsx +746 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/slider.tsx +52 -0
- package/src/components/ui/spinner.tsx +18 -0
- package/src/components/ui/switch.tsx +32 -0
- package/src/components/ui/table.tsx +116 -0
- package/src/components/ui/tabs.tsx +83 -0
- package/src/components/ui/tag-list-input.tsx +194 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/timeline.tsx +265 -0
- package/src/components/ui/toast.tsx +237 -0
- package/src/components/ui/toggle-group.tsx +90 -0
- package/src/components/ui/toggle.tsx +45 -0
- package/src/components/ui/tooltip.tsx +66 -0
- package/src/css.d.ts +1 -0
- package/src/hooks/use-clipboard.ts +82 -0
- package/src/hooks/use-mobile.ts +21 -0
- package/src/index.ts +106 -0
- package/src/kind-icon.ts +4 -0
- package/src/lib/format-relative-time.ts +41 -0
- package/src/lib/list-selection.ts +61 -0
- package/src/lib/utils.ts +6 -0
- package/src/moloo.css +360 -0
- package/src/styles.css +36 -0
- package/src/typeset.css +518 -0
- package/README.md +0 -3
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { cn } from "../../lib/utils"
|
|
2
|
+
|
|
3
|
+
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
|
4
|
+
return (
|
|
5
|
+
<div
|
|
6
|
+
data-slot="skeleton"
|
|
7
|
+
className={cn("animate-pulse rounded-md bg-muted", className)}
|
|
8
|
+
{...props}
|
|
9
|
+
/>
|
|
10
|
+
)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export { Skeleton }
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Slider as SliderPrimitive } from "@base-ui/react/slider"
|
|
2
|
+
|
|
3
|
+
import { cn } from "../../lib/utils"
|
|
4
|
+
|
|
5
|
+
function Slider({
|
|
6
|
+
className,
|
|
7
|
+
defaultValue,
|
|
8
|
+
value,
|
|
9
|
+
min = 0,
|
|
10
|
+
max = 100,
|
|
11
|
+
...props
|
|
12
|
+
}: SliderPrimitive.Root.Props) {
|
|
13
|
+
const _values = Array.isArray(value)
|
|
14
|
+
? value
|
|
15
|
+
: Array.isArray(defaultValue)
|
|
16
|
+
? defaultValue
|
|
17
|
+
: [min, max]
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<SliderPrimitive.Root
|
|
21
|
+
className={cn("data-horizontal:w-full data-vertical:h-full", className)}
|
|
22
|
+
data-slot="slider"
|
|
23
|
+
defaultValue={defaultValue}
|
|
24
|
+
value={value}
|
|
25
|
+
min={min}
|
|
26
|
+
max={max}
|
|
27
|
+
thumbAlignment="edge"
|
|
28
|
+
{...props}
|
|
29
|
+
>
|
|
30
|
+
<SliderPrimitive.Control className="relative flex w-full touch-none items-center select-none data-disabled:opacity-50 data-vertical:h-full data-vertical:min-h-40 data-vertical:w-auto data-vertical:flex-col">
|
|
31
|
+
<SliderPrimitive.Track
|
|
32
|
+
data-slot="slider-track"
|
|
33
|
+
className="relative grow overflow-hidden rounded-md bg-muted select-none data-horizontal:h-1 data-horizontal:w-full data-vertical:h-full data-vertical:w-1"
|
|
34
|
+
>
|
|
35
|
+
<SliderPrimitive.Indicator
|
|
36
|
+
data-slot="slider-range"
|
|
37
|
+
className="bg-primary select-none data-horizontal:h-full data-vertical:w-full"
|
|
38
|
+
/>
|
|
39
|
+
</SliderPrimitive.Track>
|
|
40
|
+
{Array.from({ length: _values.length }, (_, index) => (
|
|
41
|
+
<SliderPrimitive.Thumb
|
|
42
|
+
data-slot="slider-thumb"
|
|
43
|
+
key={index}
|
|
44
|
+
className="moloo-focus-within relative block size-3.5 shrink-0 rounded-md border-2 border-border bg-primary shadow-moloo-sm transition-[color,box-shadow] select-none after:absolute after:-inset-2 disabled:pointer-events-none disabled:opacity-50"
|
|
45
|
+
/>
|
|
46
|
+
))}
|
|
47
|
+
</SliderPrimitive.Control>
|
|
48
|
+
</SliderPrimitive.Root>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export { Slider }
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { cn } from "../../lib/utils"
|
|
2
|
+
|
|
3
|
+
function Spinner({ className, ...props }: React.ComponentProps<"div">) {
|
|
4
|
+
return (
|
|
5
|
+
<div
|
|
6
|
+
data-slot="spinner"
|
|
7
|
+
role="status"
|
|
8
|
+
aria-label="Loading"
|
|
9
|
+
className={cn(
|
|
10
|
+
"size-[1.35rem] shrink-0 animate-spin rounded-full border-[3px] border-secondary border-t-primary border-r-primary",
|
|
11
|
+
className
|
|
12
|
+
)}
|
|
13
|
+
{...props}
|
|
14
|
+
/>
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export { Spinner }
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Switch as SwitchPrimitive } from "@base-ui/react/switch"
|
|
4
|
+
|
|
5
|
+
import { cn } from "../../lib/utils"
|
|
6
|
+
|
|
7
|
+
function Switch({
|
|
8
|
+
className,
|
|
9
|
+
size = "default",
|
|
10
|
+
...props
|
|
11
|
+
}: SwitchPrimitive.Root.Props & {
|
|
12
|
+
size?: "sm" | "default"
|
|
13
|
+
}) {
|
|
14
|
+
return (
|
|
15
|
+
<SwitchPrimitive.Root
|
|
16
|
+
data-slot="switch"
|
|
17
|
+
data-size={size}
|
|
18
|
+
className={cn(
|
|
19
|
+
"moloo-focus peer group/switch relative inline-flex shrink-0 items-center rounded-md border-2 border-input bg-[var(--field-bg)] p-0.5 transition-colors outline-none after:absolute after:-inset-x-3 after:-inset-y-2 aria-invalid:border-destructive aria-invalid:aria-checked:border-primary data-[size=default]:h-6 data-[size=default]:w-10 data-[size=sm]:h-5 data-[size=sm]:w-8 data-checked:border-primary data-checked:bg-primary dark:bg-input/30 dark:data-checked:bg-primary data-disabled:cursor-not-allowed data-disabled:opacity-50",
|
|
20
|
+
className
|
|
21
|
+
)}
|
|
22
|
+
{...props}
|
|
23
|
+
>
|
|
24
|
+
<SwitchPrimitive.Thumb
|
|
25
|
+
data-slot="switch-thumb"
|
|
26
|
+
className="pointer-events-none block shrink-0 rounded-sm bg-foreground transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 data-checked:translate-x-full data-checked:bg-primary-foreground"
|
|
27
|
+
/>
|
|
28
|
+
</SwitchPrimitive.Root>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export { Switch }
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
|
|
5
|
+
import { cn } from "../../lib/utils"
|
|
6
|
+
|
|
7
|
+
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
|
8
|
+
return (
|
|
9
|
+
<div
|
|
10
|
+
data-slot="table-container"
|
|
11
|
+
className="relative w-full overflow-x-auto"
|
|
12
|
+
>
|
|
13
|
+
<table
|
|
14
|
+
data-slot="table"
|
|
15
|
+
className={cn("w-full caption-bottom text-sm", className)}
|
|
16
|
+
{...props}
|
|
17
|
+
/>
|
|
18
|
+
</div>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
|
23
|
+
return (
|
|
24
|
+
<thead
|
|
25
|
+
data-slot="table-header"
|
|
26
|
+
className={cn("[&_tr]:border-b", className)}
|
|
27
|
+
{...props}
|
|
28
|
+
/>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
|
33
|
+
return (
|
|
34
|
+
<tbody
|
|
35
|
+
data-slot="table-body"
|
|
36
|
+
className={cn("[&_tr:last-child]:border-0", className)}
|
|
37
|
+
{...props}
|
|
38
|
+
/>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
|
43
|
+
return (
|
|
44
|
+
<tfoot
|
|
45
|
+
data-slot="table-footer"
|
|
46
|
+
className={cn(
|
|
47
|
+
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
|
48
|
+
className
|
|
49
|
+
)}
|
|
50
|
+
{...props}
|
|
51
|
+
/>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
|
56
|
+
return (
|
|
57
|
+
<tr
|
|
58
|
+
data-slot="table-row"
|
|
59
|
+
className={cn(
|
|
60
|
+
"border-b transition-colors hover:bg-muted/50 has-aria-expanded:bg-muted/50 data-[state=selected]:bg-muted",
|
|
61
|
+
className
|
|
62
|
+
)}
|
|
63
|
+
{...props}
|
|
64
|
+
/>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
|
69
|
+
return (
|
|
70
|
+
<th
|
|
71
|
+
data-slot="table-head"
|
|
72
|
+
className={cn(
|
|
73
|
+
"label h-10 px-2 text-left align-middle whitespace-nowrap text-muted-foreground [&:has([role=checkbox])]:pr-0",
|
|
74
|
+
className
|
|
75
|
+
)}
|
|
76
|
+
{...props}
|
|
77
|
+
/>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
|
82
|
+
return (
|
|
83
|
+
<td
|
|
84
|
+
data-slot="table-cell"
|
|
85
|
+
className={cn(
|
|
86
|
+
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0",
|
|
87
|
+
className
|
|
88
|
+
)}
|
|
89
|
+
{...props}
|
|
90
|
+
/>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function TableCaption({
|
|
95
|
+
className,
|
|
96
|
+
...props
|
|
97
|
+
}: React.ComponentProps<"caption">) {
|
|
98
|
+
return (
|
|
99
|
+
<caption
|
|
100
|
+
data-slot="table-caption"
|
|
101
|
+
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
|
102
|
+
{...props}
|
|
103
|
+
/>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export {
|
|
108
|
+
Table,
|
|
109
|
+
TableHeader,
|
|
110
|
+
TableBody,
|
|
111
|
+
TableFooter,
|
|
112
|
+
TableHead,
|
|
113
|
+
TableRow,
|
|
114
|
+
TableCell,
|
|
115
|
+
TableCaption,
|
|
116
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Tabs as TabsPrimitive } from "@base-ui/react/tabs"
|
|
4
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
5
|
+
|
|
6
|
+
import { cn } from "../../lib/utils"
|
|
7
|
+
|
|
8
|
+
function Tabs({
|
|
9
|
+
className,
|
|
10
|
+
orientation = "horizontal",
|
|
11
|
+
...props
|
|
12
|
+
}: TabsPrimitive.Root.Props) {
|
|
13
|
+
return (
|
|
14
|
+
<TabsPrimitive.Root
|
|
15
|
+
data-slot="tabs"
|
|
16
|
+
data-orientation={orientation}
|
|
17
|
+
orientation={orientation}
|
|
18
|
+
className={cn(
|
|
19
|
+
"group/tabs flex gap-2 data-horizontal:flex-col",
|
|
20
|
+
className
|
|
21
|
+
)}
|
|
22
|
+
{...props}
|
|
23
|
+
/>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const tabsListVariants = cva(
|
|
28
|
+
"group/tabs-list inline-flex w-fit items-center justify-center gap-1.5 text-muted-foreground group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col",
|
|
29
|
+
{
|
|
30
|
+
variants: {
|
|
31
|
+
variant: {
|
|
32
|
+
default: "bg-transparent p-0",
|
|
33
|
+
line: "gap-1 rounded-md bg-transparent p-0",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
defaultVariants: {
|
|
37
|
+
variant: "default",
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
function TabsList({
|
|
43
|
+
className,
|
|
44
|
+
variant = "default",
|
|
45
|
+
...props
|
|
46
|
+
}: TabsPrimitive.List.Props & VariantProps<typeof tabsListVariants>) {
|
|
47
|
+
return (
|
|
48
|
+
<TabsPrimitive.List
|
|
49
|
+
data-slot="tabs-list"
|
|
50
|
+
data-variant={variant}
|
|
51
|
+
className={cn(tabsListVariants({ variant }), className)}
|
|
52
|
+
{...props}
|
|
53
|
+
/>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function TabsTrigger({ className, ...props }: TabsPrimitive.Tab.Props) {
|
|
58
|
+
return (
|
|
59
|
+
<TabsPrimitive.Tab
|
|
60
|
+
data-slot="tabs-trigger"
|
|
61
|
+
className={cn(
|
|
62
|
+
"moloo-focus relative inline-flex items-center justify-center gap-1.5 rounded-md border-2 border-transparent bg-card px-3 py-1.5 font-display text-[0.8rem] font-bold tracking-[-0.02em] whitespace-nowrap text-muted-foreground shadow-moloo-sm transition-all outline-none group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start hover:text-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
63
|
+
"data-active:bg-accent data-active:text-accent-foreground data-active:shadow-moloo data-active:rotate-[-1.2deg]",
|
|
64
|
+
"group-data-[variant=line]/tabs-list:rounded-none group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:px-2 group-data-[variant=line]/tabs-list:shadow-none group-data-[variant=line]/tabs-list:data-active:rotate-0 group-data-[variant=line]/tabs-list:data-active:bg-transparent group-data-[variant=line]/tabs-list:data-active:text-foreground group-data-[variant=line]/tabs-list:data-active:shadow-none",
|
|
65
|
+
"after:absolute after:bg-accent after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-4px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100",
|
|
66
|
+
className
|
|
67
|
+
)}
|
|
68
|
+
{...props}
|
|
69
|
+
/>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function TabsContent({ className, ...props }: TabsPrimitive.Panel.Props) {
|
|
74
|
+
return (
|
|
75
|
+
<TabsPrimitive.Panel
|
|
76
|
+
data-slot="tabs-content"
|
|
77
|
+
className={cn("flex-1 text-sm outline-none", className)}
|
|
78
|
+
{...props}
|
|
79
|
+
/>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { IconPlus, IconX } from "@tabler/icons-react"
|
|
5
|
+
|
|
6
|
+
import { Badge } from "./badge"
|
|
7
|
+
import { Button } from "./button"
|
|
8
|
+
import { Input } from "./input"
|
|
9
|
+
import { cn } from "../../lib/utils"
|
|
10
|
+
|
|
11
|
+
export type TagListInputParseResult =
|
|
12
|
+
| { ok: true; value: string }
|
|
13
|
+
| { ok: false; error: string }
|
|
14
|
+
|
|
15
|
+
export type TagListInputProps = {
|
|
16
|
+
values: readonly string[]
|
|
17
|
+
onChange: (values: string[]) => void
|
|
18
|
+
parse?: (raw: string) => TagListInputParseResult
|
|
19
|
+
placeholder?: string
|
|
20
|
+
addLabel?: string
|
|
21
|
+
removeLabel?: (value: string) => string
|
|
22
|
+
empty?: React.ReactNode
|
|
23
|
+
disabled?: boolean
|
|
24
|
+
id?: string
|
|
25
|
+
className?: string
|
|
26
|
+
"aria-label"?: string
|
|
27
|
+
"aria-invalid"?: React.AriaAttributes["aria-invalid"]
|
|
28
|
+
"aria-describedby"?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function defaultParse(raw: string): TagListInputParseResult {
|
|
32
|
+
const value = raw.trim()
|
|
33
|
+
if (value.length === 0) {
|
|
34
|
+
return { ok: false, error: "Enter a value." }
|
|
35
|
+
}
|
|
36
|
+
return { ok: true, value }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function defaultRemoveLabel(value: string) {
|
|
40
|
+
return `Remove ${value}`
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function commitTag(
|
|
44
|
+
values: readonly string[],
|
|
45
|
+
raw: string,
|
|
46
|
+
parse: (raw: string) => TagListInputParseResult,
|
|
47
|
+
):
|
|
48
|
+
| { type: "invalid"; error: string }
|
|
49
|
+
| { type: "unchanged" }
|
|
50
|
+
| { type: "added"; values: string[] } {
|
|
51
|
+
const parsed = parse(raw)
|
|
52
|
+
if (!parsed.ok) return { type: "invalid", error: parsed.error }
|
|
53
|
+
if (values.includes(parsed.value)) return { type: "unchanged" }
|
|
54
|
+
return { type: "added", values: [...values, parsed.value] }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function markedInvalid(
|
|
58
|
+
ariaInvalid: React.AriaAttributes["aria-invalid"],
|
|
59
|
+
error: string | null,
|
|
60
|
+
) {
|
|
61
|
+
if (error !== null) return true
|
|
62
|
+
return (
|
|
63
|
+
ariaInvalid === true ||
|
|
64
|
+
ariaInvalid === "true" ||
|
|
65
|
+
ariaInvalid === "grammar" ||
|
|
66
|
+
ariaInvalid === "spelling"
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function TagListInput({
|
|
71
|
+
values,
|
|
72
|
+
onChange,
|
|
73
|
+
parse = defaultParse,
|
|
74
|
+
placeholder,
|
|
75
|
+
addLabel = "Add",
|
|
76
|
+
removeLabel = defaultRemoveLabel,
|
|
77
|
+
empty,
|
|
78
|
+
disabled = false,
|
|
79
|
+
id,
|
|
80
|
+
className,
|
|
81
|
+
"aria-label": ariaLabel,
|
|
82
|
+
"aria-invalid": ariaInvalid,
|
|
83
|
+
"aria-describedby": ariaDescribedBy,
|
|
84
|
+
}: TagListInputProps) {
|
|
85
|
+
const [draft, setDraft] = React.useState("")
|
|
86
|
+
const [error, setError] = React.useState<string | null>(null)
|
|
87
|
+
const inputRef = React.useRef<HTMLInputElement>(null)
|
|
88
|
+
const errorId = React.useId()
|
|
89
|
+
const invalid = markedInvalid(ariaInvalid, error)
|
|
90
|
+
const describedBy =
|
|
91
|
+
[ariaDescribedBy, error ? errorId : undefined].filter(Boolean).join(" ") ||
|
|
92
|
+
undefined
|
|
93
|
+
|
|
94
|
+
function add() {
|
|
95
|
+
const result = commitTag(values, draft, parse)
|
|
96
|
+
if (result.type === "invalid") {
|
|
97
|
+
setError(result.error)
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
setError(null)
|
|
101
|
+
setDraft("")
|
|
102
|
+
if (result.type === "added") onChange(result.values)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function remove(value: string) {
|
|
106
|
+
onChange(values.filter((item) => item !== value))
|
|
107
|
+
inputRef.current?.focus()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function onDraftKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {
|
|
111
|
+
if (event.nativeEvent.isComposing) return
|
|
112
|
+
if (event.key === "Enter") {
|
|
113
|
+
event.preventDefault()
|
|
114
|
+
add()
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
if (
|
|
118
|
+
(event.key === "Backspace" || event.key === "Delete") &&
|
|
119
|
+
draft.length === 0 &&
|
|
120
|
+
values.length > 0
|
|
121
|
+
) {
|
|
122
|
+
event.preventDefault()
|
|
123
|
+
onChange(values.slice(0, -1))
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<div
|
|
129
|
+
data-slot="tag-list-input"
|
|
130
|
+
data-disabled={disabled ? "true" : undefined}
|
|
131
|
+
className={cn("flex w-full flex-col gap-2", className)}
|
|
132
|
+
>
|
|
133
|
+
<div className="flex min-h-7 flex-wrap items-center gap-1.5">
|
|
134
|
+
{values.length === 0
|
|
135
|
+
? empty
|
|
136
|
+
: values.map((value) => (
|
|
137
|
+
<Badge
|
|
138
|
+
key={value}
|
|
139
|
+
variant="outline"
|
|
140
|
+
className="h-auto max-w-full gap-1 overflow-visible py-0.5 pr-0.5"
|
|
141
|
+
>
|
|
142
|
+
<span className="min-w-0 truncate" title={value}>
|
|
143
|
+
{value}
|
|
144
|
+
</span>
|
|
145
|
+
<Button
|
|
146
|
+
type="button"
|
|
147
|
+
variant="ghost"
|
|
148
|
+
size="icon-xs"
|
|
149
|
+
className="size-5"
|
|
150
|
+
aria-label={removeLabel(value)}
|
|
151
|
+
disabled={disabled}
|
|
152
|
+
onClick={() => remove(value)}
|
|
153
|
+
>
|
|
154
|
+
<IconX aria-hidden />
|
|
155
|
+
</Button>
|
|
156
|
+
</Badge>
|
|
157
|
+
))}
|
|
158
|
+
</div>
|
|
159
|
+
<div className="flex gap-2">
|
|
160
|
+
<Input
|
|
161
|
+
ref={inputRef}
|
|
162
|
+
id={id}
|
|
163
|
+
value={draft}
|
|
164
|
+
placeholder={placeholder}
|
|
165
|
+
disabled={disabled}
|
|
166
|
+
aria-label={ariaLabel}
|
|
167
|
+
aria-invalid={invalid || undefined}
|
|
168
|
+
aria-describedby={describedBy}
|
|
169
|
+
onChange={(event) => {
|
|
170
|
+
setDraft(event.currentTarget.value)
|
|
171
|
+
if (error) setError(null)
|
|
172
|
+
}}
|
|
173
|
+
onKeyDown={onDraftKeyDown}
|
|
174
|
+
/>
|
|
175
|
+
<Button
|
|
176
|
+
type="button"
|
|
177
|
+
variant="outline"
|
|
178
|
+
disabled={disabled || draft.trim().length === 0}
|
|
179
|
+
onClick={add}
|
|
180
|
+
>
|
|
181
|
+
<IconPlus aria-hidden />
|
|
182
|
+
{addLabel}
|
|
183
|
+
</Button>
|
|
184
|
+
</div>
|
|
185
|
+
{error ? (
|
|
186
|
+
<p id={errorId} role="alert" className="text-sm font-normal text-destructive">
|
|
187
|
+
{error}
|
|
188
|
+
</p>
|
|
189
|
+
) : null}
|
|
190
|
+
</div>
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export { TagListInput }
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
import { cn } from "../../lib/utils"
|
|
4
|
+
|
|
5
|
+
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
|
6
|
+
return (
|
|
7
|
+
<textarea
|
|
8
|
+
data-slot="textarea"
|
|
9
|
+
className={cn(
|
|
10
|
+
"moloo-field moloo-focus flex field-sizing-content min-h-16 w-full px-2.5 py-2 text-sm font-semibold text-foreground transition-colors outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:shadow-[var(--moloo-invalid-shadow)]",
|
|
11
|
+
className
|
|
12
|
+
)}
|
|
13
|
+
{...props}
|
|
14
|
+
/>
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export { Textarea }
|