@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.
@@ -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,9 @@
1
+ import { cn } from "@/lib/utils"
2
+
3
+ export interface TextProps {
4
+ className?: string
5
+ children?: React.ReactNode
6
+ }
7
+ export default function Text(props: Readonly<TextProps>) {
8
+ return <p className={cn(props.className)}>{props.children}</p>
9
+ }
@@ -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
@@ -0,0 +1,7 @@
1
+ import clsx from "clsx"
2
+ import type { ClassValue } from "clsx"
3
+ import { twMerge } from "tailwind-merge"
4
+
5
+ export function cn(...classList: ClassValue[]) {
6
+ return twMerge(clsx(...classList))
7
+ }
package/next.config.ts ADDED
@@ -0,0 +1,8 @@
1
+ import type { NextConfig } from "next";
2
+
3
+ const nextConfig: NextConfig = {
4
+ /* config options here */
5
+ reactStrictMode: true,
6
+ };
7
+
8
+ export default nextConfig;
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,6 @@
1
+ import "@/styles/globals.css";
2
+ import type { AppProps } from "next/app";
3
+
4
+ export default function App({ Component, pageProps }: AppProps) {
5
+ return <Component {...pageProps} />;
6
+ }
@@ -0,0 +1,13 @@
1
+ import { Html, Head, Main, NextScript } from "next/document";
2
+
3
+ export default function Document() {
4
+ return (
5
+ <Html lang="en">
6
+ <Head />
7
+ <body className="antialiased">
8
+ <Main />
9
+ <NextScript />
10
+ </body>
11
+ </Html>
12
+ );
13
+ }
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ export default function Home() {
2
+ return <div>Hello, world!</div>;
3
+ }
@@ -0,0 +1,7 @@
1
+ const config = {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ }
6
+
7
+ export default config
Binary file
@@ -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>
@@ -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>