@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.
- 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,149 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
5
|
+
|
|
6
|
+
import { cn } from "../../lib/utils"
|
|
7
|
+
import { Button } from "./button"
|
|
8
|
+
import { Input } from "./input"
|
|
9
|
+
import { Textarea } from "./textarea"
|
|
10
|
+
|
|
11
|
+
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|
12
|
+
return (
|
|
13
|
+
<div
|
|
14
|
+
data-slot="input-group"
|
|
15
|
+
role="group"
|
|
16
|
+
className={cn(
|
|
17
|
+
"group/input-group moloo-field moloo-focus-within relative flex h-9 w-full min-w-0 items-stretch rounded-md transition-colors outline-none",
|
|
18
|
+
"has-disabled:opacity-50",
|
|
19
|
+
"has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:shadow-[var(--moloo-invalid-shadow)]",
|
|
20
|
+
"has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto",
|
|
21
|
+
"has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5",
|
|
22
|
+
"in-data-[slot=combobox-content]:focus-within:outline-none",
|
|
23
|
+
className
|
|
24
|
+
)}
|
|
25
|
+
{...props}
|
|
26
|
+
/>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const inputGroupAddonVariants = cva(
|
|
31
|
+
"flex h-auto cursor-text items-center justify-center gap-1.5 self-stretch text-sm font-semibold text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-md [&>svg:not([class*='size-'])]:size-4",
|
|
32
|
+
{
|
|
33
|
+
variants: {
|
|
34
|
+
align: {
|
|
35
|
+
"inline-start":
|
|
36
|
+
"order-first pl-2.5 pr-1.5 has-[>button]:pl-1",
|
|
37
|
+
"inline-end":
|
|
38
|
+
"order-last pl-1.5 pr-2.5 has-[>button]:pr-1",
|
|
39
|
+
"block-start":
|
|
40
|
+
"order-first w-full justify-start px-2.5 py-2 group-has-[>input]/input-group:pt-2",
|
|
41
|
+
"block-end":
|
|
42
|
+
"order-last w-full justify-start px-2.5 py-2 group-has-[>input]/input-group:pb-2",
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
defaultVariants: {
|
|
46
|
+
align: "inline-start",
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
function InputGroupAddon({
|
|
52
|
+
className,
|
|
53
|
+
align = "inline-start",
|
|
54
|
+
...props
|
|
55
|
+
}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
|
|
56
|
+
return (
|
|
57
|
+
<div
|
|
58
|
+
role="group"
|
|
59
|
+
data-slot="input-group-addon"
|
|
60
|
+
data-align={align}
|
|
61
|
+
className={cn(inputGroupAddonVariants({ align }), className)}
|
|
62
|
+
onClick={(e) => {
|
|
63
|
+
if ((e.target as HTMLElement).closest("button")) {
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
e.currentTarget.parentElement?.querySelector("input")?.focus()
|
|
67
|
+
}}
|
|
68
|
+
{...props}
|
|
69
|
+
/>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function InputGroupButton({
|
|
74
|
+
className,
|
|
75
|
+
type = "button",
|
|
76
|
+
variant = "ghost",
|
|
77
|
+
size = "xs",
|
|
78
|
+
...props
|
|
79
|
+
}: Omit<React.ComponentProps<typeof Button>, "size" | "type"> & {
|
|
80
|
+
size?: "xs" | "sm" | "icon-xs" | "icon-sm"
|
|
81
|
+
type?: "button" | "submit" | "reset"
|
|
82
|
+
}) {
|
|
83
|
+
return (
|
|
84
|
+
<Button
|
|
85
|
+
type={type}
|
|
86
|
+
data-size={size}
|
|
87
|
+
variant={variant}
|
|
88
|
+
size={size}
|
|
89
|
+
className={cn(
|
|
90
|
+
"shadow-none hover:shadow-none active:shadow-none hover:translate-none active:translate-none",
|
|
91
|
+
className
|
|
92
|
+
)}
|
|
93
|
+
{...props}
|
|
94
|
+
/>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
|
|
99
|
+
return (
|
|
100
|
+
<span
|
|
101
|
+
className={cn(
|
|
102
|
+
"flex items-center gap-2 text-sm font-semibold text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
|
|
103
|
+
className
|
|
104
|
+
)}
|
|
105
|
+
{...props}
|
|
106
|
+
/>
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function InputGroupInput({
|
|
111
|
+
className,
|
|
112
|
+
...props
|
|
113
|
+
}: React.ComponentProps<typeof Input>) {
|
|
114
|
+
return (
|
|
115
|
+
<Input
|
|
116
|
+
data-slot="input-group-control"
|
|
117
|
+
className={cn(
|
|
118
|
+
"h-full min-w-0 flex-1 rounded-none border-0 bg-transparent px-2.5 py-0 shadow-none outline-none focus-visible:outline-none disabled:bg-transparent aria-invalid:shadow-none dark:bg-transparent dark:disabled:bg-transparent",
|
|
119
|
+
className
|
|
120
|
+
)}
|
|
121
|
+
{...props}
|
|
122
|
+
/>
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function InputGroupTextarea({
|
|
127
|
+
className,
|
|
128
|
+
...props
|
|
129
|
+
}: React.ComponentProps<"textarea">) {
|
|
130
|
+
return (
|
|
131
|
+
<Textarea
|
|
132
|
+
data-slot="input-group-control"
|
|
133
|
+
className={cn(
|
|
134
|
+
"min-h-16 flex-1 resize-none rounded-none border-0 bg-transparent py-2 shadow-none outline-none focus-visible:outline-none disabled:bg-transparent aria-invalid:shadow-none dark:bg-transparent dark:disabled:bg-transparent",
|
|
135
|
+
className
|
|
136
|
+
)}
|
|
137
|
+
{...props}
|
|
138
|
+
/>
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export {
|
|
143
|
+
InputGroup,
|
|
144
|
+
InputGroupAddon,
|
|
145
|
+
InputGroupButton,
|
|
146
|
+
InputGroupText,
|
|
147
|
+
InputGroupInput,
|
|
148
|
+
InputGroupTextarea,
|
|
149
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { OTPInput, OTPInputContext } from "input-otp"
|
|
5
|
+
|
|
6
|
+
import { cn } from "../../lib/utils"
|
|
7
|
+
import { IconMinus } from "@tabler/icons-react"
|
|
8
|
+
|
|
9
|
+
function InputOTP({
|
|
10
|
+
className,
|
|
11
|
+
containerClassName,
|
|
12
|
+
...props
|
|
13
|
+
}: React.ComponentProps<typeof OTPInput> & {
|
|
14
|
+
containerClassName?: string
|
|
15
|
+
}) {
|
|
16
|
+
return (
|
|
17
|
+
<OTPInput
|
|
18
|
+
data-slot="input-otp"
|
|
19
|
+
containerClassName={cn(
|
|
20
|
+
"flex items-center has-disabled:opacity-50",
|
|
21
|
+
containerClassName
|
|
22
|
+
)}
|
|
23
|
+
spellCheck={false}
|
|
24
|
+
className={cn("disabled:cursor-not-allowed", className)}
|
|
25
|
+
{...props}
|
|
26
|
+
/>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|
31
|
+
return (
|
|
32
|
+
<div
|
|
33
|
+
data-slot="input-otp-group"
|
|
34
|
+
className={cn("flex items-center isolate", className)}
|
|
35
|
+
{...props}
|
|
36
|
+
/>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function InputOTPSlot({
|
|
41
|
+
index,
|
|
42
|
+
className,
|
|
43
|
+
...props
|
|
44
|
+
}: React.ComponentProps<"div"> & {
|
|
45
|
+
index: number
|
|
46
|
+
}) {
|
|
47
|
+
const inputOTPContext = React.useContext(OTPInputContext)
|
|
48
|
+
const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {}
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div
|
|
52
|
+
data-slot="input-otp-slot"
|
|
53
|
+
data-active={isActive}
|
|
54
|
+
className={cn(
|
|
55
|
+
"relative flex size-9 items-center justify-center border-2 border-border bg-[var(--field-bg)] text-sm font-semibold transition-colors outline-none",
|
|
56
|
+
"rounded-none not-first:-ml-[2px] first:rounded-l-md last:rounded-r-md",
|
|
57
|
+
"aria-invalid:border-destructive",
|
|
58
|
+
"data-[active=true]:z-10 data-[active=true]:border-destructive",
|
|
59
|
+
className
|
|
60
|
+
)}
|
|
61
|
+
{...props}
|
|
62
|
+
>
|
|
63
|
+
{char}
|
|
64
|
+
{hasFakeCaret && (
|
|
65
|
+
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
|
66
|
+
<div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
|
|
67
|
+
</div>
|
|
68
|
+
)}
|
|
69
|
+
</div>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
|
|
74
|
+
return (
|
|
75
|
+
<div
|
|
76
|
+
data-slot="input-otp-separator"
|
|
77
|
+
className="flex items-center [&_svg:not([class*='size-'])]:size-4"
|
|
78
|
+
role="separator"
|
|
79
|
+
{...props}
|
|
80
|
+
>
|
|
81
|
+
<IconMinus />
|
|
82
|
+
</div>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type StepDirection = "up" | "down"
|
|
2
|
+
|
|
3
|
+
export function commitStep(input: HTMLInputElement, direction: StepDirection): void {
|
|
4
|
+
if (input.disabled || input.readOnly) return
|
|
5
|
+
|
|
6
|
+
const previous = input.value
|
|
7
|
+
try {
|
|
8
|
+
if (direction === "up") input.stepUp()
|
|
9
|
+
else input.stepDown()
|
|
10
|
+
} catch (error) {
|
|
11
|
+
if (error instanceof DOMException && error.name === "InvalidStateError") return
|
|
12
|
+
throw error
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (input.value === previous) return
|
|
16
|
+
const view = input.ownerDocument.defaultView
|
|
17
|
+
if (view == null) return
|
|
18
|
+
input.dispatchEvent(new view.Event("input", { bubbles: true }))
|
|
19
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { Input as InputPrimitive } from "@base-ui/react/input"
|
|
5
|
+
import { IconChevronDown, IconChevronUp } from "@tabler/icons-react"
|
|
6
|
+
|
|
7
|
+
import { cn } from "../../lib/utils"
|
|
8
|
+
import { commitStep, type StepDirection } from "./input-step"
|
|
9
|
+
|
|
10
|
+
type InputProps = React.ComponentProps<"input"> & {
|
|
11
|
+
stepper?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function StepButton({
|
|
15
|
+
direction,
|
|
16
|
+
disabled,
|
|
17
|
+
onStep,
|
|
18
|
+
}: {
|
|
19
|
+
direction: StepDirection
|
|
20
|
+
disabled: boolean
|
|
21
|
+
onStep: (event: React.MouseEvent<HTMLButtonElement>, direction: StepDirection) => void
|
|
22
|
+
}) {
|
|
23
|
+
const Icon = direction === "up" ? IconChevronUp : IconChevronDown
|
|
24
|
+
return (
|
|
25
|
+
<button
|
|
26
|
+
type="button"
|
|
27
|
+
tabIndex={-1}
|
|
28
|
+
aria-label={direction === "up" ? "Increase" : "Decrease"}
|
|
29
|
+
disabled={disabled}
|
|
30
|
+
className="flex flex-1 items-center justify-center text-muted-foreground hover:text-foreground disabled:pointer-events-none"
|
|
31
|
+
onMouseDown={(event) => {
|
|
32
|
+
event.preventDefault()
|
|
33
|
+
}}
|
|
34
|
+
onClick={(event) => {
|
|
35
|
+
onStep(event, direction)
|
|
36
|
+
}}
|
|
37
|
+
>
|
|
38
|
+
<Icon className="pointer-events-none size-3" aria-hidden="true" />
|
|
39
|
+
</button>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function SteppedInput({
|
|
44
|
+
className,
|
|
45
|
+
disabled,
|
|
46
|
+
readOnly,
|
|
47
|
+
...props
|
|
48
|
+
}: React.ComponentProps<"input">) {
|
|
49
|
+
function step(event: React.MouseEvent<HTMLButtonElement>, direction: StepDirection) {
|
|
50
|
+
const input = event.currentTarget.closest("[data-slot=input-stepper]")?.querySelector("input")
|
|
51
|
+
if (!(input instanceof HTMLInputElement)) return
|
|
52
|
+
commitStep(input, direction)
|
|
53
|
+
input.focus({ preventScroll: true })
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const buttonsDisabled = disabled === true || readOnly === true
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<span
|
|
60
|
+
data-slot="input-stepper"
|
|
61
|
+
className={cn(
|
|
62
|
+
"moloo-field moloo-focus-within relative flex h-9 w-full min-w-0 items-stretch transition-colors has-[input:disabled]:pointer-events-none has-[input:disabled]:cursor-not-allowed has-[input:disabled]:opacity-50 has-[[aria-invalid=true]]:border-destructive has-[[aria-invalid=true]]:shadow-[var(--moloo-invalid-shadow)]",
|
|
63
|
+
className,
|
|
64
|
+
)}
|
|
65
|
+
>
|
|
66
|
+
<InputPrimitive
|
|
67
|
+
data-slot="input"
|
|
68
|
+
{...props}
|
|
69
|
+
type="number"
|
|
70
|
+
disabled={disabled}
|
|
71
|
+
readOnly={readOnly}
|
|
72
|
+
className="moloo-number-input h-full w-full min-w-0 rounded-none border-0 bg-transparent py-1 pl-2.5 pr-7 text-sm font-semibold text-foreground shadow-none outline-none placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed"
|
|
73
|
+
/>
|
|
74
|
+
<span className="absolute inset-y-0 right-0 flex w-5 flex-col">
|
|
75
|
+
<StepButton direction="up" disabled={buttonsDisabled} onStep={step} />
|
|
76
|
+
<StepButton direction="down" disabled={buttonsDisabled} onStep={step} />
|
|
77
|
+
</span>
|
|
78
|
+
</span>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function Input({ className, type, stepper, ...props }: InputProps) {
|
|
83
|
+
if (type === "number" && stepper !== false) {
|
|
84
|
+
return <SteppedInput className={className} {...props} />
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<InputPrimitive
|
|
89
|
+
type={type}
|
|
90
|
+
data-slot="input"
|
|
91
|
+
className={cn(
|
|
92
|
+
"moloo-field moloo-focus h-9 w-full min-w-0 px-2.5 py-1 text-sm font-semibold text-foreground transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:shadow-[var(--moloo-invalid-shadow)]",
|
|
93
|
+
className,
|
|
94
|
+
)}
|
|
95
|
+
{...props}
|
|
96
|
+
/>
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export { Input }
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { mergeProps } from "@base-ui/react/merge-props"
|
|
3
|
+
import { useRender } from "@base-ui/react/use-render"
|
|
4
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
5
|
+
|
|
6
|
+
import { cn } from "../../lib/utils"
|
|
7
|
+
import { Separator } from "./separator"
|
|
8
|
+
|
|
9
|
+
function ItemGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|
10
|
+
return (
|
|
11
|
+
<div
|
|
12
|
+
role="list"
|
|
13
|
+
data-slot="item-group"
|
|
14
|
+
className={cn(
|
|
15
|
+
"group/item-group flex w-full flex-col gap-4 has-data-[size=sm]:gap-2.5 has-data-[size=xs]:gap-2",
|
|
16
|
+
className
|
|
17
|
+
)}
|
|
18
|
+
{...props}
|
|
19
|
+
/>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function ItemSeparator({
|
|
24
|
+
className,
|
|
25
|
+
...props
|
|
26
|
+
}: React.ComponentProps<typeof Separator>) {
|
|
27
|
+
return (
|
|
28
|
+
<Separator
|
|
29
|
+
data-slot="item-separator"
|
|
30
|
+
orientation="horizontal"
|
|
31
|
+
className={cn("my-2", className)}
|
|
32
|
+
{...props}
|
|
33
|
+
/>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const itemVariants = cva(
|
|
38
|
+
"group/item flex w-full flex-wrap items-center rounded-md border-2 text-sm transition-colors duration-100 outline-none moloo-focus [a]:transition-colors [a]:hover:bg-muted",
|
|
39
|
+
{
|
|
40
|
+
variants: {
|
|
41
|
+
variant: {
|
|
42
|
+
default: "border-transparent",
|
|
43
|
+
outline: "border-border",
|
|
44
|
+
muted: "border-transparent bg-muted/50",
|
|
45
|
+
},
|
|
46
|
+
size: {
|
|
47
|
+
default: "gap-2.5 px-3 py-2.5",
|
|
48
|
+
sm: "gap-2.5 px-3 py-2.5",
|
|
49
|
+
xs: "gap-2 px-2.5 py-2 in-data-[slot=dropdown-menu-content]:p-0",
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
defaultVariants: {
|
|
53
|
+
variant: "default",
|
|
54
|
+
size: "default",
|
|
55
|
+
},
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
function Item({
|
|
60
|
+
className,
|
|
61
|
+
variant = "default",
|
|
62
|
+
size = "default",
|
|
63
|
+
render,
|
|
64
|
+
...props
|
|
65
|
+
}: useRender.ComponentProps<"div"> & VariantProps<typeof itemVariants>) {
|
|
66
|
+
return useRender({
|
|
67
|
+
defaultTagName: "div",
|
|
68
|
+
props: mergeProps<"div">(
|
|
69
|
+
{
|
|
70
|
+
className: cn(itemVariants({ variant, size, className })),
|
|
71
|
+
},
|
|
72
|
+
props
|
|
73
|
+
),
|
|
74
|
+
render,
|
|
75
|
+
state: {
|
|
76
|
+
slot: "item",
|
|
77
|
+
variant,
|
|
78
|
+
size,
|
|
79
|
+
},
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const itemMediaVariants = cva(
|
|
84
|
+
"flex shrink-0 items-center justify-center gap-2 group-has-data-[slot=item-description]/item:translate-y-0.5 group-has-data-[slot=item-description]/item:self-start [&_svg]:pointer-events-none",
|
|
85
|
+
{
|
|
86
|
+
variants: {
|
|
87
|
+
variant: {
|
|
88
|
+
default: "bg-transparent",
|
|
89
|
+
icon: "[&_svg:not([class*='size-'])]:size-4",
|
|
90
|
+
image:
|
|
91
|
+
"size-10 overflow-hidden rounded-md group-data-[size=sm]/item:size-8 group-data-[size=xs]/item:size-6 [&_img]:size-full [&_img]:object-cover",
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
defaultVariants: {
|
|
95
|
+
variant: "default",
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
function ItemMedia({
|
|
101
|
+
className,
|
|
102
|
+
variant = "default",
|
|
103
|
+
...props
|
|
104
|
+
}: React.ComponentProps<"div"> & VariantProps<typeof itemMediaVariants>) {
|
|
105
|
+
return (
|
|
106
|
+
<div
|
|
107
|
+
data-slot="item-media"
|
|
108
|
+
data-variant={variant}
|
|
109
|
+
className={cn(itemMediaVariants({ variant, className }))}
|
|
110
|
+
{...props}
|
|
111
|
+
/>
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function ItemContent({ className, ...props }: React.ComponentProps<"div">) {
|
|
116
|
+
return (
|
|
117
|
+
<div
|
|
118
|
+
data-slot="item-content"
|
|
119
|
+
className={cn(
|
|
120
|
+
"flex flex-1 flex-col gap-1 group-data-[size=xs]/item:gap-0 [&+[data-slot=item-content]]:flex-none",
|
|
121
|
+
className
|
|
122
|
+
)}
|
|
123
|
+
{...props}
|
|
124
|
+
/>
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function ItemTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
129
|
+
return (
|
|
130
|
+
<div
|
|
131
|
+
data-slot="item-title"
|
|
132
|
+
className={cn(
|
|
133
|
+
"line-clamp-1 flex w-fit items-center gap-2 text-sm font-semibold underline-offset-4",
|
|
134
|
+
className
|
|
135
|
+
)}
|
|
136
|
+
{...props}
|
|
137
|
+
/>
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function ItemDescription({ className, ...props }: React.ComponentProps<"p">) {
|
|
142
|
+
return (
|
|
143
|
+
<p
|
|
144
|
+
data-slot="item-description"
|
|
145
|
+
className={cn(
|
|
146
|
+
"line-clamp-2 text-left text-sm/relaxed font-normal text-muted-foreground group-data-[size=xs]/item:text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
|
|
147
|
+
className
|
|
148
|
+
)}
|
|
149
|
+
{...props}
|
|
150
|
+
/>
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function ItemActions({ className, ...props }: React.ComponentProps<"div">) {
|
|
155
|
+
return (
|
|
156
|
+
<div
|
|
157
|
+
data-slot="item-actions"
|
|
158
|
+
className={cn("flex items-center gap-2", className)}
|
|
159
|
+
{...props}
|
|
160
|
+
/>
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function ItemHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
165
|
+
return (
|
|
166
|
+
<div
|
|
167
|
+
data-slot="item-header"
|
|
168
|
+
className={cn(
|
|
169
|
+
"flex basis-full items-center justify-between gap-2",
|
|
170
|
+
className
|
|
171
|
+
)}
|
|
172
|
+
{...props}
|
|
173
|
+
/>
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function ItemFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
178
|
+
return (
|
|
179
|
+
<div
|
|
180
|
+
data-slot="item-footer"
|
|
181
|
+
className={cn(
|
|
182
|
+
"flex basis-full items-center justify-between gap-2",
|
|
183
|
+
className
|
|
184
|
+
)}
|
|
185
|
+
{...props}
|
|
186
|
+
/>
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export {
|
|
191
|
+
Item,
|
|
192
|
+
ItemMedia,
|
|
193
|
+
ItemContent,
|
|
194
|
+
ItemActions,
|
|
195
|
+
ItemGroup,
|
|
196
|
+
ItemSeparator,
|
|
197
|
+
ItemTitle,
|
|
198
|
+
ItemDescription,
|
|
199
|
+
ItemHeader,
|
|
200
|
+
ItemFooter,
|
|
201
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { cn } from "../../lib/utils"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Converts a key name to its platform symbol.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* kbdKey("cmd") // "⌘"
|
|
8
|
+
* kbdKey("shift") // "⇧"
|
|
9
|
+
* kbdKey("ctrl") // "⌃"
|
|
10
|
+
* kbdKey("alt") // "⌥"
|
|
11
|
+
* kbdKey("esc") // "⎋"
|
|
12
|
+
* kbdKey("enter") // "↵"
|
|
13
|
+
* kbdKey("backspace") // "⌫"
|
|
14
|
+
* kbdKey("tab") // "⇥"
|
|
15
|
+
* kbdKey("up") // "↑"
|
|
16
|
+
*/
|
|
17
|
+
const KEY_MAP: Record<string, string> = {
|
|
18
|
+
cmd: "⌘",
|
|
19
|
+
command: "⌘",
|
|
20
|
+
meta: "⌘",
|
|
21
|
+
ctrl: "⌃",
|
|
22
|
+
control: "⌃",
|
|
23
|
+
alt: "⌥",
|
|
24
|
+
option: "⌥",
|
|
25
|
+
shift: "⇧",
|
|
26
|
+
enter: "↵",
|
|
27
|
+
return: "↵",
|
|
28
|
+
backspace: "⌫",
|
|
29
|
+
delete: "⌦",
|
|
30
|
+
escape: "⎋",
|
|
31
|
+
esc: "⎋",
|
|
32
|
+
tab: "⇥",
|
|
33
|
+
up: "↑",
|
|
34
|
+
down: "↓",
|
|
35
|
+
left: "←",
|
|
36
|
+
right: "→",
|
|
37
|
+
space: "␣",
|
|
38
|
+
capslock: "⇪",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function kbdKey(key: string): string {
|
|
42
|
+
return KEY_MAP[key.toLowerCase()] ?? key.toUpperCase()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function Kbd({
|
|
46
|
+
className,
|
|
47
|
+
keys,
|
|
48
|
+
children,
|
|
49
|
+
...props
|
|
50
|
+
}: React.ComponentProps<"kbd"> & {
|
|
51
|
+
/** Key ids rendered with `kbdKey`, one chip each. Children are used when omitted. */
|
|
52
|
+
keys?: readonly string[]
|
|
53
|
+
}) {
|
|
54
|
+
if (keys && keys.length > 0) {
|
|
55
|
+
return (
|
|
56
|
+
<KbdGroup className={className}>
|
|
57
|
+
{keys.map((key) => (
|
|
58
|
+
<Kbd key={key}>{kbdKey(key)}</Kbd>
|
|
59
|
+
))}
|
|
60
|
+
</KbdGroup>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<kbd
|
|
66
|
+
data-slot="kbd"
|
|
67
|
+
className={cn(
|
|
68
|
+
"pointer-events-none inline-flex h-5 w-fit min-w-5 items-center justify-center gap-1 rounded-md border-2 border-border bg-muted px-1 font-sans text-xs font-semibold text-muted-foreground shadow-[0_2px_0_0] shadow-border select-none",
|
|
69
|
+
"in-data-[slot=tooltip-content]:border-background/30 in-data-[slot=tooltip-content]:bg-background/20 in-data-[slot=tooltip-content]:text-background in-data-[slot=tooltip-content]:shadow-background/20",
|
|
70
|
+
"dark:in-data-[slot=tooltip-content]:bg-background/10",
|
|
71
|
+
"[&_svg:not([class*='size-'])]:size-3",
|
|
72
|
+
className
|
|
73
|
+
)}
|
|
74
|
+
{...props}
|
|
75
|
+
>
|
|
76
|
+
{children}
|
|
77
|
+
</kbd>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function KbdGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|
82
|
+
return (
|
|
83
|
+
<div
|
|
84
|
+
data-slot="kbd-group"
|
|
85
|
+
className={cn("inline-flex items-center gap-1", className)}
|
|
86
|
+
{...props}
|
|
87
|
+
/>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export { Kbd, KbdGroup, kbdKey }
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Icon } from "@tabler/icons-react"
|
|
2
|
+
|
|
3
|
+
type IconLoader = () => Promise<{ default: Icon }>
|
|
4
|
+
|
|
5
|
+
type IconGlob = {
|
|
6
|
+
glob<Module>(pattern: string): Record<string, () => Promise<Module>>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
let paths: Record<string, IconLoader> = {}
|
|
10
|
+
try {
|
|
11
|
+
paths = (import.meta as ImportMeta & IconGlob).glob<{ default: Icon }>(
|
|
12
|
+
"/node_modules/@tabler/icons-react/dist/esm/icons/Icon*.mjs",
|
|
13
|
+
)
|
|
14
|
+
} catch {
|
|
15
|
+
paths = {}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const tablerIconLoaders: Map<string, IconLoader> = new Map(
|
|
19
|
+
Object.entries(paths).map(([path, loader]): [string, IconLoader] => {
|
|
20
|
+
const file = path.slice(path.lastIndexOf("/") + 1)
|
|
21
|
+
const name = file.endsWith(".mjs") ? file.slice(0, -".mjs".length) : file
|
|
22
|
+
return [name, loader]
|
|
23
|
+
}),
|
|
24
|
+
)
|