@modlin/ui 0.0.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.
- package/README.md +1 -0
- package/biome.json +36 -0
- package/components/alert.tsx +79 -0
- package/components/avatar.tsx +26 -0
- package/components/badge.tsx +31 -0
- package/components/button.tsx +135 -0
- package/components/card.tsx +85 -0
- package/components/checkbox.tsx +68 -0
- package/components/content.tsx +10 -0
- package/components/divider.tsx +17 -0
- package/components/footer.tsx +10 -0
- package/components/global.ts +34 -0
- package/components/header.tsx +22 -0
- package/components/heading.tsx +91 -0
- package/components/input.tsx +87 -0
- package/components/label.tsx +36 -0
- package/components/radio.tsx +33 -0
- package/components/select.tsx +127 -0
- package/components/stack.tsx +55 -0
- package/components/switch.tsx +79 -0
- package/components/text.tsx +9 -0
- package/components/textarea.tsx +47 -0
- package/components/toast.tsx +13 -0
- package/components/tooltip.tsx +13 -0
- package/components/typography.tsx +70 -0
- package/lib/utils.ts +7 -0
- package/next.config.ts +8 -0
- package/package.json +32 -0
- package/pages/_app.tsx +6 -0
- package/pages/_document.tsx +13 -0
- package/pages/api/hello.ts +10 -0
- package/pages/index.tsx +3 -0
- package/postcss.config.mjs +7 -0
- package/public/favicon.ico +0 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/next.svg +1 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/styles/globals.css +26 -0
- package/tailwind.config.js +4 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ChangeEventHandler,
|
|
3
|
+
type FocusEventHandler,
|
|
4
|
+
type HTMLInputAutoCompleteAttribute,
|
|
5
|
+
type InputEventHandler,
|
|
6
|
+
forwardRef,
|
|
7
|
+
} from "react"
|
|
8
|
+
import { input_variant, type Variant } from "./global"
|
|
9
|
+
import { cn } from "@/lib/utils"
|
|
10
|
+
|
|
11
|
+
export interface InputProps {
|
|
12
|
+
variant?: Variant
|
|
13
|
+
|
|
14
|
+
type?: "text" | "password" | "email" | "number" | "tel" | "url" | "file"
|
|
15
|
+
inputMode?:
|
|
16
|
+
| "search"
|
|
17
|
+
| "text"
|
|
18
|
+
| "email"
|
|
19
|
+
| "tel"
|
|
20
|
+
| "url"
|
|
21
|
+
| "none"
|
|
22
|
+
| "numeric"
|
|
23
|
+
| "decimal"
|
|
24
|
+
placeholder?: string
|
|
25
|
+
defaultValue?: string
|
|
26
|
+
|
|
27
|
+
name?: string
|
|
28
|
+
pattern?: string // validation
|
|
29
|
+
min?: number | string // validation
|
|
30
|
+
max?: number | string // validation
|
|
31
|
+
maxLength?: number // validation
|
|
32
|
+
minLength?: number // validation
|
|
33
|
+
required?: boolean // validation
|
|
34
|
+
|
|
35
|
+
readOnly?: boolean
|
|
36
|
+
disabled?: boolean
|
|
37
|
+
|
|
38
|
+
value?: string
|
|
39
|
+
onChange?: ChangeEventHandler<HTMLInputElement>
|
|
40
|
+
onInput?: InputEventHandler<HTMLInputElement>
|
|
41
|
+
onBlur?: FocusEventHandler<HTMLInputElement>
|
|
42
|
+
|
|
43
|
+
id?: string
|
|
44
|
+
autoComplete?: HTMLInputAutoCompleteAttribute
|
|
45
|
+
// onChange?: (value: string) => void
|
|
46
|
+
className?: string
|
|
47
|
+
invalid?: boolean
|
|
48
|
+
}
|
|
49
|
+
const Input = forwardRef<HTMLInputElement, Readonly<InputProps>>(
|
|
50
|
+
(props, ref) => {
|
|
51
|
+
const { onChange } = props
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<input
|
|
55
|
+
ref={ref}
|
|
56
|
+
type={props.type ?? "text"}
|
|
57
|
+
inputMode={props.inputMode}
|
|
58
|
+
placeholder={props.placeholder}
|
|
59
|
+
defaultValue={props.defaultValue}
|
|
60
|
+
value={props.value}
|
|
61
|
+
name={props.name}
|
|
62
|
+
min={props.min}
|
|
63
|
+
max={props.max}
|
|
64
|
+
minLength={props.minLength}
|
|
65
|
+
maxLength={props.maxLength}
|
|
66
|
+
pattern={props.pattern}
|
|
67
|
+
required={props.required}
|
|
68
|
+
readOnly={props.readOnly}
|
|
69
|
+
disabled={props.disabled}
|
|
70
|
+
onChange={onChange}
|
|
71
|
+
onBlur={props.onBlur}
|
|
72
|
+
onInvalid={e => e.preventDefault()}
|
|
73
|
+
id={props.id}
|
|
74
|
+
autoComplete={props.autoComplete}
|
|
75
|
+
data-invalid={props.invalid}
|
|
76
|
+
className={cn(
|
|
77
|
+
"flex items-center w-full h-12 px-4",
|
|
78
|
+
"peer rounded-2xl",
|
|
79
|
+
"transition-duration-150 transition-[box-shadow_color] ease-in",
|
|
80
|
+
input_variant[props.variant ?? "primary"],
|
|
81
|
+
props.className,
|
|
82
|
+
)}
|
|
83
|
+
/>
|
|
84
|
+
)
|
|
85
|
+
},
|
|
86
|
+
)
|
|
87
|
+
export default Input
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { cn } from "@/lib/utils"
|
|
2
|
+
import type { ReactNode } from "react"
|
|
3
|
+
|
|
4
|
+
export interface LabelProps {
|
|
5
|
+
htmlFor: string
|
|
6
|
+
label?: string
|
|
7
|
+
children: ReactNode
|
|
8
|
+
|
|
9
|
+
className?: string
|
|
10
|
+
|
|
11
|
+
// size?: "sm" | "md" | "lg"
|
|
12
|
+
// tone?: "default" | "muted" | "error" | "success" | "warning"
|
|
13
|
+
// weight?: "regular" | "medium" | "bold"
|
|
14
|
+
// align?: "left" | "center" | "right"
|
|
15
|
+
|
|
16
|
+
// required?: boolean
|
|
17
|
+
// disabled?: boolean
|
|
18
|
+
// truncate?: boolean
|
|
19
|
+
// maxLines?: number
|
|
20
|
+
}
|
|
21
|
+
export default function Label(props: LabelProps) {
|
|
22
|
+
return (
|
|
23
|
+
<label
|
|
24
|
+
htmlFor={props.htmlFor}
|
|
25
|
+
aria-label={props.label}
|
|
26
|
+
className={cn(
|
|
27
|
+
"flex items-center gap-2",
|
|
28
|
+
"peer-disabled:text-(--disabled) invalid:text-(--red) text-sm font-medium leading-4",
|
|
29
|
+
"select-none",
|
|
30
|
+
props.className,
|
|
31
|
+
)}
|
|
32
|
+
>
|
|
33
|
+
{props.children}
|
|
34
|
+
</label>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface RadioGroupItemProps {
|
|
2
|
+
value: string
|
|
3
|
+
name?: string
|
|
4
|
+
id?: string
|
|
5
|
+
}
|
|
6
|
+
export function RadioGroupItem(props: Readonly<RadioGroupItemProps>) {
|
|
7
|
+
return (
|
|
8
|
+
<input
|
|
9
|
+
type="radio"
|
|
10
|
+
value={props.value}
|
|
11
|
+
name={props.name}
|
|
12
|
+
id={props.id}
|
|
13
|
+
></input>
|
|
14
|
+
)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface RadioGroupProps {
|
|
18
|
+
default?: string
|
|
19
|
+
name?: string
|
|
20
|
+
required?: boolean
|
|
21
|
+
children: React.ReactNode
|
|
22
|
+
}
|
|
23
|
+
export function RadioGroup(props: Readonly<RadioGroupProps>) {
|
|
24
|
+
return (
|
|
25
|
+
<fieldset
|
|
26
|
+
defaultValue={props.default}
|
|
27
|
+
name={props.name}
|
|
28
|
+
className="grid gap-3"
|
|
29
|
+
>
|
|
30
|
+
{props.children}
|
|
31
|
+
</fieldset>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
import { IconChevronDown } from "@tabler/icons-react"
|
|
3
|
+
import Button from "./button"
|
|
4
|
+
import Text from "./text"
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
import React, { type ReactElement, type ReactNode, useState } from "react"
|
|
7
|
+
|
|
8
|
+
export interface SelectTriggerProps {
|
|
9
|
+
placeholder?: string
|
|
10
|
+
id?: string
|
|
11
|
+
children?: string
|
|
12
|
+
}
|
|
13
|
+
export function SelectTrigger(props: SelectTriggerProps) {
|
|
14
|
+
return (
|
|
15
|
+
<button
|
|
16
|
+
type="button"
|
|
17
|
+
className={cn("flex items-center", "bg-(--background)")}
|
|
18
|
+
>
|
|
19
|
+
{props.placeholder}
|
|
20
|
+
</button>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function Dropdown() {
|
|
25
|
+
return (
|
|
26
|
+
<div className="grid">
|
|
27
|
+
<SelectTrigger placeholder="Select a gender" />
|
|
28
|
+
<Button variant="outline" size="xl" className="text-(--description)">
|
|
29
|
+
<Text className="flex grow-1">Country</Text>
|
|
30
|
+
<IconChevronDown />
|
|
31
|
+
</Button>
|
|
32
|
+
</div>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface SelectOptionProps {
|
|
37
|
+
value: string
|
|
38
|
+
children: ReactNode
|
|
39
|
+
className?: string
|
|
40
|
+
onClick?: () => void
|
|
41
|
+
}
|
|
42
|
+
export function SelectOption(
|
|
43
|
+
props: Readonly<SelectOptionProps>,
|
|
44
|
+
): ReactElement<SelectOptionProps> {
|
|
45
|
+
return (
|
|
46
|
+
<button
|
|
47
|
+
type="button"
|
|
48
|
+
role="option"
|
|
49
|
+
value={props.value}
|
|
50
|
+
onClick={props.onClick}
|
|
51
|
+
className={cn(
|
|
52
|
+
"flex items-center h-12 px-4 gap-4",
|
|
53
|
+
"select-none hover:bg-(--secondary)",
|
|
54
|
+
// "[&>svg]:size-4 [&>svg]:scale-125",
|
|
55
|
+
props.className
|
|
56
|
+
)}
|
|
57
|
+
>
|
|
58
|
+
{props.children}
|
|
59
|
+
</button>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface SelectProps {
|
|
64
|
+
defaultValue?: string
|
|
65
|
+
placeholder: string
|
|
66
|
+
value?: string
|
|
67
|
+
name?: string
|
|
68
|
+
id?: string
|
|
69
|
+
children: ReactElement<SelectOptionProps>[]
|
|
70
|
+
}
|
|
71
|
+
export function Select(props: SelectProps) {
|
|
72
|
+
const [expanded, setExpanded] = useState(false)
|
|
73
|
+
const [value, setValue] = useState(props.defaultValue)
|
|
74
|
+
const [selected, setSelected] = useState<ReactNode>(props.placeholder)
|
|
75
|
+
|
|
76
|
+
const children: ReactElement[] = []
|
|
77
|
+
for (let i = 0; i < props.children.length; i++) {
|
|
78
|
+
const child = props.children[i]
|
|
79
|
+
children.push(
|
|
80
|
+
React.cloneElement(props.children[i], {
|
|
81
|
+
onClick() {
|
|
82
|
+
setValue(child.props.value)
|
|
83
|
+
setSelected(child.props.children)
|
|
84
|
+
setExpanded(false)
|
|
85
|
+
},
|
|
86
|
+
}),
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div className="relative flex flex-col w-full max-w-56">
|
|
92
|
+
<button
|
|
93
|
+
type="button"
|
|
94
|
+
role="combobox"
|
|
95
|
+
aria-expanded={expanded}
|
|
96
|
+
aria-autocomplete="none"
|
|
97
|
+
aria-haspopup="listbox"
|
|
98
|
+
value={value}
|
|
99
|
+
name={props.name}
|
|
100
|
+
id={props.id}
|
|
101
|
+
onClick={() => setExpanded(!expanded)}
|
|
102
|
+
// onBlur={() => setExpanded(false)}
|
|
103
|
+
className={cn(
|
|
104
|
+
"flex items-center gap-x-4",
|
|
105
|
+
"[&>svg]:size-4 [&>svg]:scale-125",
|
|
106
|
+
"focus:underline",
|
|
107
|
+
)}
|
|
108
|
+
>
|
|
109
|
+
{selected}
|
|
110
|
+
<IconChevronDown size={16} />
|
|
111
|
+
</button>
|
|
112
|
+
{expanded ? (
|
|
113
|
+
<ul
|
|
114
|
+
id={props.id}
|
|
115
|
+
className={cn(
|
|
116
|
+
"absolute flex flex-col w-full top-[100%] overflow-hidden",
|
|
117
|
+
"rounded-2xl",
|
|
118
|
+
"bg-(--background) shadow-[0_0_16px_0_var(--shadow)]",
|
|
119
|
+
"animate-appear-tc",
|
|
120
|
+
)}
|
|
121
|
+
>
|
|
122
|
+
{children}
|
|
123
|
+
</ul>
|
|
124
|
+
) : null}
|
|
125
|
+
</div>
|
|
126
|
+
)
|
|
127
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { cn } from "@/lib/utils"
|
|
2
|
+
|
|
3
|
+
interface StackProps {
|
|
4
|
+
align?: "start" | "center" | "end"
|
|
5
|
+
justify?: "start" | "center" | "end"
|
|
6
|
+
gap?: number
|
|
7
|
+
direction?: "uphold" | "reverse"
|
|
8
|
+
children: React.ReactNode
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function Stack({
|
|
12
|
+
align,
|
|
13
|
+
justify,
|
|
14
|
+
gap,
|
|
15
|
+
direction,
|
|
16
|
+
children,
|
|
17
|
+
...props
|
|
18
|
+
}: React.HTMLAttributes<HTMLElement> & Readonly<StackProps>) {
|
|
19
|
+
return (
|
|
20
|
+
<div
|
|
21
|
+
{...props}
|
|
22
|
+
className={cn(`flex flex-col`, props.className)}
|
|
23
|
+
style={{
|
|
24
|
+
alignItems: align,
|
|
25
|
+
justifyContent: justify,
|
|
26
|
+
gap: gap,
|
|
27
|
+
...props.style,
|
|
28
|
+
}}
|
|
29
|
+
>
|
|
30
|
+
{children}
|
|
31
|
+
</div>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
export { Stack as VStack }
|
|
35
|
+
|
|
36
|
+
export function HStack(
|
|
37
|
+
props: React.HTMLAttributes<HTMLDivElement> & StackProps,
|
|
38
|
+
) {
|
|
39
|
+
return (
|
|
40
|
+
<div
|
|
41
|
+
{...props}
|
|
42
|
+
className={`${props.className} flex`}
|
|
43
|
+
style={{
|
|
44
|
+
alignItems: props.align,
|
|
45
|
+
justifyContent: props.justify,
|
|
46
|
+
gap: props.gap,
|
|
47
|
+
...props.style,
|
|
48
|
+
}}
|
|
49
|
+
>
|
|
50
|
+
{props.children}
|
|
51
|
+
</div>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export default Stack
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
import { cn } from "@/lib/utils"
|
|
3
|
+
import { useEffect, useState } from "react"
|
|
4
|
+
|
|
5
|
+
const sizes = {
|
|
6
|
+
sm: "w-9 h-5 p-0.5",
|
|
7
|
+
lg: "w-14 h-8 p-1",
|
|
8
|
+
}
|
|
9
|
+
const thumb_sizes = {
|
|
10
|
+
sm: "w-4 h-4",
|
|
11
|
+
lg: "w-6 h-6",
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface SwitchProps {
|
|
15
|
+
checked?: boolean
|
|
16
|
+
onChange?(checked: boolean): void
|
|
17
|
+
disabled?: boolean
|
|
18
|
+
// loading?: boolean
|
|
19
|
+
|
|
20
|
+
size?: "sm" | "lg"
|
|
21
|
+
|
|
22
|
+
label?: string
|
|
23
|
+
|
|
24
|
+
required?: boolean // web
|
|
25
|
+
name?: string // web
|
|
26
|
+
value?: string // web
|
|
27
|
+
id?: string // web
|
|
28
|
+
}
|
|
29
|
+
export default function Switch(props: Readonly<SwitchProps>) {
|
|
30
|
+
const [checked, setChecked] = useState(props.checked ?? false)
|
|
31
|
+
|
|
32
|
+
const onChange = props.onChange
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (onChange) onChange(checked)
|
|
35
|
+
}, [checked, onChange])
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<>
|
|
39
|
+
<input
|
|
40
|
+
type="checkbox"
|
|
41
|
+
checked={checked}
|
|
42
|
+
onChange={() => setChecked(v => !v)}
|
|
43
|
+
disabled={props.disabled}
|
|
44
|
+
aria-label={props.label}
|
|
45
|
+
required={props.required}
|
|
46
|
+
name={props.id}
|
|
47
|
+
value={props.value}
|
|
48
|
+
id={props.id}
|
|
49
|
+
className="peer hidden"
|
|
50
|
+
/>
|
|
51
|
+
<button
|
|
52
|
+
type="button"
|
|
53
|
+
role="switch"
|
|
54
|
+
data-state={checked ? "checked" : "unchecked"}
|
|
55
|
+
aria-checked={checked}
|
|
56
|
+
onClick={() => setChecked(v => !v)}
|
|
57
|
+
disabled={props.disabled}
|
|
58
|
+
className={cn(
|
|
59
|
+
sizes[props.size ?? "lg"],
|
|
60
|
+
"rounded-full hover:cursor-pointer",
|
|
61
|
+
"transition transition-all transition-duration-150 ease",
|
|
62
|
+
"data-[state=checked]:bg-black dark:data-[state=checked]:bg-white",
|
|
63
|
+
"data-[state=unchecked]:bg-black/10 dark:data-[state=unchecked]:bg-white/10",
|
|
64
|
+
)}
|
|
65
|
+
>
|
|
66
|
+
<div
|
|
67
|
+
data-state={checked ? "checked" : "unchecked"}
|
|
68
|
+
className={cn(
|
|
69
|
+
thumb_sizes[props.size ?? "lg"],
|
|
70
|
+
"rounded-full",
|
|
71
|
+
"transition transition-all transition-duration-150 ease",
|
|
72
|
+
"bg-white dark:bg-black",
|
|
73
|
+
"data-[state=unchecked]:translate-x-0 data-[state=checked]:translate-x-[100%]",
|
|
74
|
+
)}
|
|
75
|
+
></div>
|
|
76
|
+
</button>
|
|
77
|
+
</>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { cn } from "@/lib/utils"
|
|
2
|
+
|
|
3
|
+
export interface TextareaProps {
|
|
4
|
+
type?: "text" | "password" | "email" | "number" | "tel" | "url" | "file"
|
|
5
|
+
disabled?: boolean
|
|
6
|
+
placeholder?: string
|
|
7
|
+
value?: string
|
|
8
|
+
maxLength?: number
|
|
9
|
+
minLength?: number
|
|
10
|
+
required?: boolean
|
|
11
|
+
name?: string
|
|
12
|
+
id?: string
|
|
13
|
+
onChange?: (value: string) => void
|
|
14
|
+
width?: number | string
|
|
15
|
+
className?: string
|
|
16
|
+
}
|
|
17
|
+
export default function Textarea(props: Readonly<TextareaProps>) {
|
|
18
|
+
const { onChange } = props
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<textarea
|
|
22
|
+
disabled={props.disabled}
|
|
23
|
+
placeholder={props.placeholder}
|
|
24
|
+
value={props.value}
|
|
25
|
+
maxLength={props.maxLength}
|
|
26
|
+
minLength={props.minLength}
|
|
27
|
+
required={props.required}
|
|
28
|
+
name={props.name}
|
|
29
|
+
id={props.id}
|
|
30
|
+
onChange={onChange ? e => onChange(e.target.value) : undefined}
|
|
31
|
+
style={{
|
|
32
|
+
width: props.width,
|
|
33
|
+
}}
|
|
34
|
+
className={cn(
|
|
35
|
+
"transition-duration-150 transition-all ease-in",
|
|
36
|
+
"h-12 rounded-2xl px-4",
|
|
37
|
+
"flex items-center",
|
|
38
|
+
"bg-white/25 backdrop-blur-lg dark:bg-black/25",
|
|
39
|
+
"placeholder:text-black/50 placeholder:dark:text-white/50",
|
|
40
|
+
"disabled:text-black/25 disabled:dark:text-white/25",
|
|
41
|
+
"inset-ring inset-ring-black/25 dark:inset-ring-white/25",
|
|
42
|
+
"focus:inset-ring-black/50 focus:dark:inset-ring-white/50",
|
|
43
|
+
props.className,
|
|
44
|
+
)}
|
|
45
|
+
/>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface ToastOptions {
|
|
2
|
+
description?: string
|
|
3
|
+
}
|
|
4
|
+
export function toast(title: string, options?: ToastOptions) {
|
|
5
|
+
const toast = document.createElement("div")
|
|
6
|
+
toast.className =
|
|
7
|
+
"transition transition-all transition-duration-250 ease-out absolute top-[-64px] p-4 rounded-2xl text-bold bg-white dark:bg-black inset-ring inset-ring-black/25"
|
|
8
|
+
toast.replaceChildren(title)
|
|
9
|
+
const toasts = document.getElementById("toasts")
|
|
10
|
+
if (toasts) toasts.appendChild(toast)
|
|
11
|
+
toast.style.top = "0px"
|
|
12
|
+
return false
|
|
13
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface TooltipProps {
|
|
2
|
+
children: string
|
|
3
|
+
}
|
|
4
|
+
export default function Tooltip(props: TooltipProps) {
|
|
5
|
+
return (
|
|
6
|
+
<div>
|
|
7
|
+
<div className="p-2 rounded-lg text-xs text-white bg-black dark:bg-white">
|
|
8
|
+
<p>{props.children}</p>
|
|
9
|
+
</div>
|
|
10
|
+
<button type="button">Hover</button>
|
|
11
|
+
</div>
|
|
12
|
+
)
|
|
13
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { forwardRef, type ReactNode } from "react"
|
|
2
|
+
import { cn } from "@/lib/utils"
|
|
3
|
+
|
|
4
|
+
export interface TypographyProps {
|
|
5
|
+
children?: ReactNode
|
|
6
|
+
className?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const InlineCode = forwardRef<HTMLElement, TypographyProps>(
|
|
10
|
+
(props, ref) => {
|
|
11
|
+
return (
|
|
12
|
+
<code
|
|
13
|
+
ref={ref}
|
|
14
|
+
className={cn(
|
|
15
|
+
"bg-(--muted) relative rounded px-[2px] leading-12 font-mono text-sm font-medium",
|
|
16
|
+
props.className,
|
|
17
|
+
)}
|
|
18
|
+
>
|
|
19
|
+
{props.children}
|
|
20
|
+
</code>
|
|
21
|
+
)
|
|
22
|
+
},
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
export const Lead = forwardRef<HTMLParagraphElement, TypographyProps>(
|
|
26
|
+
(props, ref) => {
|
|
27
|
+
return (
|
|
28
|
+
<p
|
|
29
|
+
ref={ref}
|
|
30
|
+
className={cn("text-(--muted-foreground) text-xl", props.className)}
|
|
31
|
+
>
|
|
32
|
+
{props.children}
|
|
33
|
+
</p>
|
|
34
|
+
)
|
|
35
|
+
},
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
export const Large = forwardRef<HTMLDivElement, TypographyProps>(
|
|
39
|
+
(props, ref) => {
|
|
40
|
+
return (
|
|
41
|
+
<div ref={ref} className={cn("text-lg font-semibold", props.className)}>
|
|
42
|
+
{props.children}
|
|
43
|
+
</div>
|
|
44
|
+
)
|
|
45
|
+
},
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
export const Small = forwardRef<HTMLElement, TypographyProps>((props, ref) => {
|
|
49
|
+
return (
|
|
50
|
+
<small
|
|
51
|
+
ref={ref}
|
|
52
|
+
className={cn("text-sm leading-none font-medium", props.className)}
|
|
53
|
+
>
|
|
54
|
+
{props.children}
|
|
55
|
+
</small>
|
|
56
|
+
)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
export const Muted = forwardRef<HTMLParagraphElement, TypographyProps>(
|
|
60
|
+
(props, ref) => {
|
|
61
|
+
return (
|
|
62
|
+
<p
|
|
63
|
+
ref={ref}
|
|
64
|
+
className={cn("text-(--muted-foreground) text-sm", props.className)}
|
|
65
|
+
>
|
|
66
|
+
{props.children}
|
|
67
|
+
</p>
|
|
68
|
+
)
|
|
69
|
+
},
|
|
70
|
+
)
|
package/lib/utils.ts
ADDED
package/next.config.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@modlin/ui",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"main": "src/index.ts",
|
|
5
|
+
"exports": {
|
|
6
|
+
"./*": "./components/*.tsx"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"dev": "next dev --turbopack",
|
|
10
|
+
"build": "next build --turbopack",
|
|
11
|
+
"start": "next start",
|
|
12
|
+
"lint": "biome check",
|
|
13
|
+
"format": "biome format --write"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@tabler/icons-react": "^3.35.0",
|
|
17
|
+
"@types/bun": "^1.2.22",
|
|
18
|
+
"clsx": "^2.1.1",
|
|
19
|
+
"next": "15.5.3",
|
|
20
|
+
"react": "19.1.1",
|
|
21
|
+
"react-dom": "19.1.1",
|
|
22
|
+
"tailwind-merge": "^3.3.1"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"typescript": "^5.9.2",
|
|
26
|
+
"@types/react": "^19.1.13",
|
|
27
|
+
"@types/react-dom": "^19.1.9",
|
|
28
|
+
"@tailwindcss/postcss": "^4.1.13",
|
|
29
|
+
"tailwindcss": "^4.1.13",
|
|
30
|
+
"@biomejs/biome": "2.2.4"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/pages/_app.tsx
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
|
2
|
+
import type { NextApiRequest, NextApiResponse } from "next"
|
|
3
|
+
|
|
4
|
+
type Data = {
|
|
5
|
+
name: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default function handler(req: NextApiRequest, res: NextApiResponse<Data>) {
|
|
9
|
+
res.status(200).json({ name: "John Doe" })
|
|
10
|
+
}
|
package/pages/index.tsx
ADDED
|
Binary file
|
package/public/file.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
package/public/globe.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|