@modlin/ui 0.0.242 → 0.0.244

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/LICENSE ADDED
@@ -0,0 +1,27 @@
1
+ The Modlin Distributable License (MDL)
2
+
3
+ Copyright (c) 2025 Modlin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the “Software”), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ Any use, distribution, modification, integration, or implementation of the
16
+ Software in another project must include clear and visible credit to the
17
+ Software and to its original author. This credit must be placed in all public
18
+ distributions, documentation, and any “About” or “Credits” section of the
19
+ implementing project.
20
+
21
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
27
+ THE SOFTWARE.
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json.schemastore.org/package",
3
3
  "name": "@modlin/ui",
4
4
  "description": "A lightweight UI library built on the Suffix design system for consistent, fast, and elegant interfaces.",
5
- "version": "0.0.242",
5
+ "version": "0.0.244",
6
6
  "main": "src/index.ts",
7
7
  "type": "module",
8
8
  "exports": {
package/src/alert.tsx CHANGED
@@ -54,7 +54,7 @@ export function Alert(props: Readonly<AlertProps>) {
54
54
  className={cn(
55
55
  "grid grid-cols-[16px_1fr] auto-rows-auto gap-x-4 gap-y-1 items-center justify-center",
56
56
  "p-4 rounded-2xl",
57
- "inset-ring inset-ring-black/15 dark:inset-ring-white/10",
57
+ "inset-ring inset-ring-border",
58
58
  props.className,
59
59
  )}
60
60
  >
@@ -68,7 +68,7 @@ Alert.Destructive = (props: Readonly<AlertProps>) => {
68
68
  className={cn(
69
69
  "grid grid-cols-[16px_1fr] auto-rows-auto gap-x-4 gap-y-1 items-center justify-center",
70
70
  "p-4 rounded-2xl",
71
- "inset-ring inset-ring-black/15 dark:inset-ring-white/10",
71
+ "inset-ring inset-ring-border",
72
72
  "text-red-600 dark:text-red-400",
73
73
  props.className,
74
74
  )}
package/src/avatar.tsx CHANGED
@@ -1,3 +1,5 @@
1
+ "use client"
2
+ import { useState } from "react"
1
3
  import { cn } from "./utils"
2
4
  import Image, { type ImageProps } from "next/image"
3
5
 
@@ -5,22 +7,27 @@ export interface AvatarProps extends ImageProps {
5
7
  fallback: string
6
8
  }
7
9
  export default function Avatar({ fallback, ...props }: Readonly<AvatarProps>) {
8
- const show = true
9
-
10
+ const [error, setError] = useState(false)
11
+ if (error) {
12
+ return (
13
+ <span
14
+ className={cn(
15
+ "relative flex shrink-0 size-8 overflow-hidden rounded-full bg-background inset-ring inset-ring-border text-sm font-medium leading-none",
16
+ props.className,
17
+ )}
18
+ >
19
+ <span className="flex items-center justify-center w-full font-mono text-sm">{fallback.slice(0, 3).toUpperCase()}</span>
20
+ </span>
21
+ )
22
+ }
10
23
  return (
11
24
  <span
12
25
  className={cn(
13
- "relative flex shrink-0 size-8 overflow-hidden rounded-full ring ring-(--outline)",
26
+ "relative flex shrink-0 size-8 overflow-hidden rounded-full bg-background border border-border text-sm font-medium leading-none",
14
27
  props.className,
15
28
  )}
16
29
  >
17
- {show ? (
18
- <Image {...props} className="aspect-square size-full" />
19
- ) : (
20
- <span className="flex items-center justify-center w-full text-sm font-medium leading-none">
21
- {fallback.slice(0, 3).toUpperCase()}
22
- </span>
23
- )}
30
+ <Image width={32} height={32} onError={() => setError(true)} {...props} className={cn("aspect-square size-full", props.className)} />
24
31
  </span>
25
32
  )
26
33
  }
package/src/button.tsx CHANGED
@@ -2,124 +2,130 @@ import { cn } from "./utils"
2
2
  import { IconLoader2 } from "@tabler/icons-react"
3
3
  import type { MouseEvent, FocusEvent, ReactNode, ReactHTMLElement } from "react"
4
4
  import React from "react"
5
- import type { Variant, Variants } from "./global"
5
+ import type { Variant, Size, Shape } from "./globals"
6
6
 
7
- const sizeMap = {
8
- sm: cn("h-8 px-4 gap-x-1 text-sm rounded-full font-medium"),
9
- md: cn("h-9 px-4.5 gap-x-1 text-sm rounded-full font-medium"),
10
- lg: cn(
11
- "h-11 px-5 gap-x-1 text-base rounded-full font-medium",
12
- "[&>svg]:size-4",
13
- ),
14
- xl: cn(
15
- "h-12 px-6 gap-x-1 text-base rounded-full font-semibold",
16
- "[&>svg]:size-4",
17
- ),
18
- sm_: "h-8 px-2.5 rounded-lg text-sm font-medium [&>svg]:size-4",
19
- md_: cn(
20
- "h-9 px-3.5 gap-x-1 text-sm rounded-xl font-medium",
21
- "[&>svg]:size-4",
22
- ),
23
- lg_: cn(
24
- "h-11 px-4 gap-x-2 text-base rounded-2xl font-medium",
25
- "[&>svg]:size-5",
26
- ),
27
- xl_: cn(
28
- "h-12 px-4 gap-x-2 text-base rounded-2xl font-medium",
29
- "[&>svg]:size-4 [&>svg]:scale-125",
30
- ),
31
- icon: cn("p-2 rounded-full text-sm font-medium"),
32
- iconr: cn("p-2 rounded-2xl text-sm font-medium"),
33
- none: cn("overflow-visible"),
7
+ const size: Record<Size, string> = {
8
+ sm: cn("h-8 px-4 gap-x-1 text-sm font-medium"),
9
+ md: cn("h-9 px-4.5 gap-x-1 text-sm font-medium"),
10
+ lg: cn("h-11 px-5 gap-x-1 text-base font-medium", "[&>svg]:size-4"),
11
+ xl: cn("h-12 px-6 gap-x-1 text-base font-semibold", "[&>svg]:size-4"),
12
+ // sm_: "h-8 px-2.5 rounded-lg text-sm font-medium [&>svg]:size-4",
13
+ // md_: cn("h-9 px-3.5 gap-x-1 text-sm rounded-xl font-medium", "[&>svg]:size-4"),
14
+ // lg_: cn("h-11 px-4 gap-x-2 text-base rounded-2xl font-medium", "[&>svg]:size-5"),
15
+ // xl_: cn("h-12 px-4 gap-x-2 text-base rounded-2xl font-medium", "[&>svg]:size-4 [&>svg]:scale-125"),
16
+ icon: cn("p-2 text-sm font-medium"),
17
+ // iconr: cn("p-2 rounded-2xl text-sm font-medium"),
18
+ none: cn("overflow-visible"),
34
19
  }
35
- const variant: Variants = {
36
- primary:
37
- "bg-primary disabled:bg-primary/60 hover:bg-primary/85 text-background",
38
- secondary: "bg-secondary hover:bg-secondary/75",
39
- destructive: "bg-(--red) hover:bg-(--red)/85 text-white",
40
- outline: cn(
41
- "bg-background inset-ring inset-ring-outline",
42
- "hover:bg-secondary active:bg-secondary focus-visible:inset-ring-description disabled:bg-background disabled:text-description",
43
- ),
44
- ghost: "hover:bg-black/5 dark:hover:bg-white/10",
45
- link: "text-primary hover:underline",
46
- none: cn(),
47
- // jnsa: "bg-(--purple) hover:bg-(--purple)/90 text-white",
48
- // outline_red: "inset-ring inset-ring-(--red)/50 hover:bg-(--red)/5 text-(--red)",
49
- // shadcn: "rounded-xl bg-(--primary) hover:bg-(--primary)/85 text-white dark:text-black",
20
+ const variant: Record<Variant, string> = {
21
+ primary: "bg-primary disabled:bg-primary/60 hover:bg-primary/85 active:bg-primary/85 text-background",
22
+ secondary: "bg-secondary hover:bg-secondary/75",
23
+ destructive: "bg-red hover:bg-red/85 text-white",
24
+ outline: cn(
25
+ "bg-background inset-ring inset-ring-border",
26
+ "hover:bg-secondary active:bg-secondary focus-visible:inset-ring-muted-foreground disabled:bg-background disabled:text-muted-foreground",
27
+ ),
28
+ ghost: "hover:bg-secondary",
29
+ link: "text-primary hover:underline",
30
+ none: cn(),
31
+ // jnsa: "bg-(--purple) hover:bg-(--purple)/90 text-white",
32
+ // outline_red: "inset-ring inset-ring-(--red)/50 hover:bg-(--red)/5 text-(--red)",
33
+ // shadcn: "rounded-xl bg-(--primary) hover:bg-(--primary)/85 text-white dark:text-black",
50
34
  } as const
51
- const shapes = {
52
- square: "rounded-none",
53
- rounded: "",
54
- pill: "rounded-full",
35
+ const shape: Record<Shape, string> = {
36
+ square: "rounded-none",
37
+ rounded: "rounded-2xl",
38
+ pill: "rounded-full",
55
39
  }
56
40
  export interface ButtonProps {
57
- variant?: Variant
58
- size?: keyof typeof sizeMap
59
- shape?: "square" | "rounded" | "pill"
60
- // tone?: "default" | "success" | "error" | "warning"
61
- // elevation?: "none" | "xs" | "sm" | "md" | "lg" | "xl"
62
-
63
- disabled?: boolean
64
- loading?: boolean
65
-
66
- label?: string
67
- // haptics?: boolean
68
-
69
- children: ReactNode
70
- onPress?: (event: MouseEvent) => void | Promise<void>
71
- onHover?: (event: MouseEvent) => void | Promise<void> // Web
72
- onFocus?: (event: FocusEvent) => void | Promise<void> // web
73
- onBlur?: (event: FocusEvent) => void | Promise<void> // web
74
-
75
- type?: "button" | "reset" | "submit"
76
- id?: string
77
- className?: string
78
-
79
- asChild?: boolean
41
+ /** @android @ios @web */
42
+ disabled?: boolean // state
43
+ /** @android @ios @web */
44
+ loading?: boolean // state
45
+ /** @android @ios @web */
46
+ label?: string
47
+ /** @android @ios @web */
48
+ title?: string
49
+ /** @android @ios @web */
50
+ children?: ReactNode
51
+ /** @android @ios @web */
52
+ asChild?: boolean
53
+ /** @android @ios @web */
54
+ variant?: Variant
55
+ /** @android @ios @web */
56
+ size?: Size
57
+ /** @android @ios @web */
58
+ shape?: Shape
59
+ /** @android @ios */
60
+ haptics?: boolean
61
+ /** @android @ios */
62
+ full?: boolean // utility
63
+ /** @android @ios */
64
+ rounded?: number // utility
65
+ /** @web */
66
+ id?: string
67
+ /** @web */
68
+ type?: "button" | "reset" | "submit"
69
+ /** @web */
70
+ className?: string
71
+ /** @android @ios @web */
72
+ onPress?(event: MouseEvent): void | Promise<void>
73
+ /** @android @ios @web */
74
+ onPressIn?(event: MouseEvent): void | Promise<void>
75
+ /** @android @ios @web */
76
+ onPressOut?(event: MouseEvent): void | Promise<void>
77
+ /** @web */
78
+ onHover?(event: MouseEvent): void | Promise<void>
79
+ /** @web */
80
+ onFocus?(event: FocusEvent): void | Promise<void>
81
+ /** @web */
82
+ onBlur?(event: FocusEvent): void | Promise<void>
83
+ // tone?: "default" | "success" | "error" | "warning"
84
+ // elevation?: "none" | "xs" | "sm" | "md" | "lg" | "xl"
80
85
  }
81
86
  export default function Button(props: Readonly<ButtonProps>) {
82
- const className = cn(
83
- "flex items-center justify-center leading-none truncate",
84
- "select-none hover:cursor-pointer disabled:hover:cursor-not-allowed",
85
- "transition transition-[background-color] transition-duration-250 ease",
86
- variant[props.variant ?? "primary"],
87
- sizeMap[props.size ?? "xl"],
88
- props.className,
89
- )
87
+ const className = cn(
88
+ "line-clamp-1 flex items-center justify-center leading-none text-center",
89
+ "select-none hover:cursor-pointer disabled:hover:cursor-not-allowed",
90
+ "transition-[background-color] duration-250 ease",
91
+ size[props.size ?? "xl"],
92
+ variant[props.variant ?? "primary"],
93
+ shape[props.shape ?? "pill"],
94
+ props.className,
95
+ )
90
96
 
91
- if (props.asChild) {
92
- const children = props.children as ReactHTMLElement<HTMLElement>
93
- return React.cloneElement(children, {
94
- type: props.type,
95
- role: "button",
96
- disabled: props.disabled,
97
- "aria-label": props.label,
98
- onClick: props.onPress,
99
- onMouseOver: props.onHover,
100
- onFocus: props.onFocus,
101
- id: props.id,
102
- className: cn(className, children.props.className),
103
- })
104
- }
97
+ if (props.asChild) {
98
+ const children = props.children as ReactHTMLElement<HTMLElement>
99
+ return React.cloneElement(children, {
100
+ type: props.type,
101
+ role: "button",
102
+ disabled: props.disabled,
103
+ "aria-label": props.label,
104
+ onClick: props.onPress,
105
+ onMouseDown: props.onPressIn,
106
+ onMouseUp: props.onPressOut,
107
+ onMouseOver: props.onHover,
108
+ onFocus: props.onFocus,
109
+ id: props.id,
110
+ className: cn(className, children.props.className),
111
+ })
112
+ }
105
113
 
106
- return (
107
- <button
108
- type={props.type ?? "button"}
109
- disabled={props.loading ? true : props.disabled}
110
- aria-label={props.label}
111
- onClick={props.onPress}
112
- onMouseOver={props.onHover}
113
- onFocus={props.onFocus}
114
- onBlur={props.onBlur}
115
- id={props.id}
116
- className={className}
117
- >
118
- {props.loading ? (
119
- <IconLoader2 className="animate-spin" />
120
- ) : (
121
- props.children
122
- )}
123
- </button>
124
- )
114
+ return (
115
+ <button
116
+ type={props.type ?? "button"}
117
+ disabled={props.loading ? true : props.disabled}
118
+ aria-label={props.label}
119
+ onClick={props.onPress}
120
+ onMouseDown={props.onPressIn}
121
+ onMouseUp={props.onPressOut}
122
+ onMouseOver={props.onHover}
123
+ onFocus={props.onFocus}
124
+ onBlur={props.onBlur}
125
+ id={props.id}
126
+ className={className}
127
+ >
128
+ {props.loading ? <IconLoader2 className="animate-spin" /> : (props.title || props.children)}
129
+ </button>
130
+ )
125
131
  }
package/src/card.tsx CHANGED
@@ -2,23 +2,20 @@ import { cn } from "./utils"
2
2
  import type * as React from "react"
3
3
 
4
4
  export interface CardHeaderProps {
5
+ children?: React.ReactNode
5
6
  className?: string
6
- children: React.ReactNode
7
7
  }
8
8
  export const CardHeader: React.FC<CardHeaderProps> = props => {
9
9
  return (
10
- <header
11
- data-slot="card-header"
12
- className={cn("flex flex-col gap-4", props.className)}
13
- >
10
+ <header data-slot="card-header" className={cn("flex flex-col gap-4", props.className)}>
14
11
  {props.children}
15
12
  </header>
16
13
  )
17
14
  }
18
15
 
19
16
  export interface CardContentProps {
17
+ children?: React.ReactNode
20
18
  className?: string
21
- children: React.ReactNode
22
19
  }
23
20
  export const CardContent: React.FC<CardContentProps> = props => {
24
21
  return (
@@ -29,22 +26,19 @@ export const CardContent: React.FC<CardContentProps> = props => {
29
26
  }
30
27
 
31
28
  export interface CardFooterProps {
29
+ children?: React.ReactNode
32
30
  className?: string
33
- children: React.ReactNode
34
31
  }
35
32
  export const CardFooter: React.FC<CardFooterProps> = props => {
36
33
  return (
37
- <footer
38
- data-slot="card-footer"
39
- className={cn("flex [.border-t]:pt-4", props.className)}
40
- >
34
+ <footer data-slot="card-footer" className={cn("flex [.border-t]:pt-4", props.className)}>
41
35
  {props.children}
42
36
  </footer>
43
37
  )
44
38
  }
45
39
 
46
40
  export interface CardAction {
47
- children: React.ReactNode
41
+ children?: React.ReactNode
48
42
  className?: string
49
43
  }
50
44
  export function CardAction(props: Readonly<CardAction>) {
@@ -56,29 +50,19 @@ export function CardAction(props: Readonly<CardAction>) {
56
50
  }
57
51
 
58
52
  const size = {
59
- md: "p-4 gap-4 sm:rounded-2xl",
60
- lg: "p-6 gap-6 sm:rounded-2xl",
61
- xl: "p-8 gap-8 sm:rounded-4xl",
53
+ md: "p-4 gap-4 rounded-2xl",
54
+ lg: "p-6 gap-6 rounded-2xl",
55
+ xl: "p-8 gap-8 rounded-4xl",
62
56
  }
63
57
 
64
58
  export interface CardProps {
65
- className?: string
66
59
  size?: "lg" | "xl"
67
- children: React.ReactNode
60
+ children?: React.ReactNode
61
+ className?: string
68
62
  }
69
63
  export const Card: React.FC<CardProps> = props => {
70
64
  return (
71
- <div
72
- className={cn(
73
- "flex flex-col",
74
- size[props.size ?? "md"],
75
- "bg-background",
76
- "sm:inset-ring inset-ring-outline",
77
- props.className,
78
- )}
79
- >
80
- {props.children}
81
- </div>
65
+ <div className={cn("flex flex-col", size[props.size ?? "md"], "bg-background", "inset-ring inset-ring-border", props.className)}>{props.children}</div>
82
66
  )
83
67
  }
84
68
 
package/src/checkbox.tsx CHANGED
@@ -1,57 +1,85 @@
1
1
  "use client"
2
- import { useEffect, useState } from "react"
2
+ import { useRef, type ChangeEvent, type ReactElement, type ReactNode } from "react"
3
3
  import { cn } from "./utils"
4
4
 
5
+ export interface ViewProps {
6
+ children?: ReactNode
7
+ className?: string
8
+ }
9
+
5
10
  export interface CheckboxProps {
6
- checked?: boolean
7
- onChange?(checked: boolean): void
11
+ /** @android @ios @web */
8
12
  disabled?: boolean
9
- // loading?: boolean
13
+ /** @android @ios @web */
14
+ defaultChecked?: boolean
15
+ /** @android @ios @web */
16
+ checked?: boolean
17
+ /** @android @ios @web */
10
18
  label?: string
11
-
12
- required?: boolean // web
13
- name?: string // web
14
- value?: string // web
15
- id?: string // web
19
+ /** @web */
20
+ required?: boolean
21
+ /** @web */
22
+ name?: string
23
+ /** @web */
24
+ id?: string
25
+ /** @web */
26
+ asChild?: boolean
27
+ /** @android @ios @web */
28
+ onChange?(checked: boolean): void
29
+ // note: dropped support for RHF to add native support
30
+ /** @deprecated @use checked */
31
+ value?: string
32
+ /** @deprecated @use onChange */
33
+ onValueChange?(event: ChangeEvent<HTMLInputElement>): void
16
34
  }
17
- export default function Checkbox(props: Readonly<CheckboxProps>) {
18
- const [checked, setChecked] = useState<boolean>(props.checked ?? false)
19
-
20
- const onChange = props.onChange
21
- useEffect(() => {
22
- if (onChange) onChange(checked)
23
- }, [checked, onChange])
35
+ export default function Checkbox(props: Readonly<CheckboxProps>): ReactElement<CheckboxProps> {
36
+ const input = useRef<HTMLInputElement>(null)
37
+ const button = useRef<HTMLInputElement>(null)
38
+ let checked = props.checked || props.defaultChecked || false
24
39
 
25
40
  return (
26
41
  <>
27
42
  <input
43
+ ref={input}
28
44
  type="checkbox"
29
- checked={checked}
30
- onChange={() => setChecked(v => !v)}
31
45
  disabled={props.disabled}
32
- aria-label={props.label}
46
+ onChange={e => {
47
+ checked = !checked
48
+ e.target.checked = checked
49
+ if (button.current) {
50
+ button.current.ariaChecked = checked ? "true" : "false"
51
+ button.current.setAttribute("data-state", checked ? "checked" : "unchecked")
52
+ }
53
+ }}
54
+ checked={checked}
55
+ defaultChecked={props.defaultChecked}
33
56
  required={props.required}
57
+ aria-label={props.label}
34
58
  name={props.name}
35
59
  value={props.value}
36
60
  id={props.id}
37
61
  className="peer hidden"
38
62
  />
39
63
  <button
64
+ ref={button}
40
65
  type="button"
41
66
  role="checkbox"
42
67
  data-state={checked ? "checked" : "unchecked"}
43
68
  aria-checked={checked}
44
- onClick={() => setChecked(v => !v)}
69
+ onClick={async e => {
70
+ checked = !checked
71
+ if (input.current) input.current.checked = checked
72
+ e.currentTarget.ariaChecked = checked ? "true" : "false"
73
+ e.currentTarget.setAttribute("data-state", checked ? "checked" : "unchecked")
74
+ }}
45
75
  disabled={props.disabled}
46
76
  className={cn(
47
77
  "w-4 h-4 rounded-sm hover:cursor-pointer text-background",
48
- "transition transition-all transition-duration-150 ease",
49
- "data-[state=unchecked]:bg-white data-[state=unchecked]:dark:bg-black",
50
- "data-[state=checked]:bg-black data-[state=checked]:dark:bg-white",
51
- "data-[state=checked]:disabled:bg-black/50 dark:data-[state=checked]:disabled:bg-white/50",
52
78
  "data-[state=unchecked]:inset-ring",
53
- "data-[state=unchecked]:inset-ring-black/50 data-[state=unchecked]:dark:inset-ring-white/50",
54
- "data-[state=unchecked]:disabled:inset-ring-black/25 data-[state=unchecked]:dark:disabled:inset-ring-white/25",
79
+ "data-[state=unchecked]:bg-background data-[state=unchecked]:inset-ring-muted-foreground",
80
+ "data-[state=unchecked]:disabled:inset-ring-border",
81
+ "data-[state=checked]:bg-primary data-[state=checked]:disabled:bg-primary/50",
82
+ "transition transition-all transition-duration-150 ease",
55
83
  )}
56
84
  >
57
85
  <svg width={16} height={16} viewBox="0 0 16 16" fill="none">
package/src/divider.tsx CHANGED
@@ -6,10 +6,10 @@ export interface DividerProps {
6
6
  }
7
7
  export default function Divider(props: DividerProps) {
8
8
  return (
9
- <hr
9
+ <div
10
+ data-orientation={props.orientation ?? "horizontal"}
10
11
  className={cn(
11
- "border-(--outline)",
12
- props.orientation === "vertical" ? "h-full border-r" : undefined,
12
+ "bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
13
13
  props.className,
14
14
  )}
15
15
  />
@@ -0,0 +1,30 @@
1
+ import { Heading4 } from "./heading"
2
+ import Button from "./button"
3
+ import Input from "./input"
4
+ import Label from "./label"
5
+ import { Muted } from "./typography"
6
+
7
+ export function Example() {
8
+ return (
9
+ <div className="flex flex-1 flex-col gap-8 p-8 bg-background">
10
+ <header>
11
+ <Heading4 className="h-8">Login to your account</Heading4>
12
+ <Muted>Enter your email address below to login to your account</Muted>
13
+ </header>
14
+ <div className="flex flex-col gap-4">
15
+ <div className="flex flex-col gap-2">
16
+ <Label htmlFor="email">Email address</Label>
17
+ <Input placeholder="m@example.com" id="email" type="email" inputMode="email" autoComplete="email" autoCapitalize="none" />
18
+ </div>
19
+ <div className="flex flex-col gap-2">
20
+ <Label htmlFor="password">Password</Label>
21
+ <Input id="password" type="password" autoComplete="password" autoCapitalize="none" />
22
+ </div>
23
+ </div>
24
+ <div className="flex flex-col gap-4">
25
+ <Button title="Login" haptics />
26
+ <Button variant="outline" title="Login with Google" haptics />
27
+ </div>
28
+ </div>
29
+ )
30
+ }
package/src/globals.ts ADDED
@@ -0,0 +1,3 @@
1
+ export type Variant = "primary" | "secondary" | "destructive" | "outline" | "ghost" | "link" | "none"
2
+ export type Size = "sm" | "md" | "lg" | "xl" | "icon" | "none"
3
+ export type Shape = "square" | "rounded" | "pill"
package/src/heading.tsx CHANGED
@@ -17,7 +17,7 @@ export default function Heading(props: Readonly<HeadingProps>) {
17
17
  )
18
18
  }
19
19
 
20
- Heading.H1 = (props: Readonly<HeadingProps>) => {
20
+ export function Heading1(props: Readonly<HeadingProps>) {
21
21
  return (
22
22
  <h1
23
23
  className={cn(
@@ -29,31 +29,31 @@ Heading.H1 = (props: Readonly<HeadingProps>) => {
29
29
  </h1>
30
30
  )
31
31
  }
32
- Heading.H2 = (props: Readonly<HeadingProps>) => {
32
+ export function Heading2(props: Readonly<HeadingProps>) {
33
33
  return (
34
- <h1
34
+ <h2
35
35
  className={cn(
36
36
  "scroll-m-20 border-b pb-2 text-3xl font-semibold tracking-tight first:mt-0",
37
37
  props.className,
38
38
  )}
39
39
  >
40
40
  {props.children}
41
- </h1>
41
+ </h2>
42
42
  )
43
43
  }
44
- Heading.H3 = (props: Readonly<HeadingProps>) => {
44
+ export function Heading3(props: Readonly<HeadingProps>) {
45
45
  return (
46
- <h1
46
+ <h3
47
47
  className={cn(
48
48
  "scroll-m-20 text-2xl font-semibold tracking-tight",
49
49
  props.className,
50
50
  )}
51
51
  >
52
52
  {props.children}
53
- </h1>
53
+ </h3>
54
54
  )
55
55
  }
56
- Heading.H4 = (props: Readonly<HeadingProps>) => {
56
+ export function Heading4(props: Readonly<HeadingProps>) {
57
57
  return (
58
58
  <h4
59
59
  className={cn(
@@ -65,19 +65,19 @@ Heading.H4 = (props: Readonly<HeadingProps>) => {
65
65
  </h4>
66
66
  )
67
67
  }
68
- Heading.H5 = (props: Readonly<HeadingProps>) => {
68
+ export function Heading5(props: Readonly<HeadingProps>) {
69
69
  return (
70
- <h4
70
+ <h5
71
71
  className={cn(
72
72
  "scroll-m-20 text-lg font-medium",
73
73
  props.className,
74
74
  )}
75
75
  >
76
76
  {props.children}
77
- </h4>
77
+ </h5>
78
78
  )
79
79
  }
80
- Heading.H6 = (props: Readonly<HeadingProps>) => {
80
+ export function Heading6(props: Readonly<HeadingProps>) {
81
81
  return (
82
82
  <h6
83
83
  className={cn(
package/src/input.tsx CHANGED
@@ -1,89 +1,128 @@
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"
1
+ import { type ChangeEvent, type FocusEvent, type HTMLInputAutoCompleteAttribute, forwardRef } from "react"
2
+ import type { Variant } from "./globals"
9
3
  import { cn } from "./utils"
10
4
 
11
- export interface InputProps {
12
- variant?: Variant
5
+ export const input_variant: Record<Variant, string> = {
6
+ primary: cn(
7
+ "bg-background/25 backdrop-blur-sm",
8
+ "placeholder:text-muted-foreground disabled:text-disabled",
9
+ "inset-ring inset-ring-border disabled:inset-ring-disabled focus:inset-ring-primary/75",
10
+ // "border border-border disabled:border-disabled focus:border-primary/75",
11
+ "focus:ring-4 focus:ring-primary/10 dark:focus:ring-primary/15",
12
+ "invalid:inset-ring-red",
13
+ // "data-[invalid=true]:border data-[invalid=true]:border-red/50 data-[invalid=true]:focus:border-red",
14
+ "data-[invalid=true]:inset-ring-red/50 data-[invalid=true]:focus:inset-ring-red/75",
15
+ "data-[invalid=true]:focus:ring-4 data-[invalid=true]:focus:ring-red/10",
16
+ ),
17
+ secondary: cn(),
18
+ destructive: cn(
19
+ "bg-background",
20
+ "placeholder:text-muted-foreground disabled:text-disabled",
21
+ "inset-ring inset-ring-red/50 disabled:inset-ring-red/25 focus:inset-ring-red/75",
22
+ "focus:ring-4 focus:ring-red/10",
23
+ // "border border-red/50 disabled:border-red/25 focus:border-red",
24
+ "focus:ring-4 focus:ring-red/5",
25
+ ),
26
+ outline: cn(),
27
+ ghost: cn(),
28
+ link: cn(),
29
+ none: cn("text-()"),
30
+ }
13
31
 
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"
32
+ type InputType = "text" | "password" | "email" | "number" | "tel" | "url" | "file"
33
+ type InputMode = "search" | "text" | "email" | "tel" | "url" | "none" | "numeric" | "decimal"
34
+ type BlurEvent = FocusEvent<HTMLInputElement, Element>
35
+
36
+ export interface InputProps {
37
+ /** @android @ios @web */
24
38
  placeholder?: string
39
+ /** @android @ios @web */
25
40
  defaultValue?: string
26
-
41
+ /** @android @ios @web */
42
+ inputMode?: InputMode
43
+ /** @android @ios @web */
44
+ value?: string
45
+ /** @android @ios @web */
46
+ readOnly?: boolean
47
+ /** @android @ios @web */
48
+ maxLength?: number
49
+ /** @android @ios @web */
50
+ autoCapitalize?: "none" | "sentences" | "words" | "characters"
51
+ /** @android @ios @web */
52
+ autoComplete?: HTMLInputAutoCompleteAttribute
53
+ /** @android @ios @web */
54
+ autoCorrect?: boolean
55
+ /** @android @ios @web */
56
+ variant?: Variant
57
+ /** @web */
58
+ id?: string
59
+ /** @web */
60
+ type?: InputType
61
+ /** @web */
27
62
  name?: string
63
+ /** @web */
64
+ required?: boolean // validation
65
+ /** @web */
66
+ disabled?: boolean
67
+ /** @web */
28
68
  pattern?: string // validation
69
+ /** @web */
29
70
  min?: number | string // validation
71
+ /** @web */
30
72
  max?: number | string // validation
31
- maxLength?: number // validation
73
+ /** @web */
32
74
  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
75
+ /** @web */
47
76
  invalid?: boolean
48
- describedby?: string
77
+ /** @web */
78
+ describedby?: string
79
+ /** @web */
80
+ className?: string
81
+ /** @android @ios @web */
82
+ onChange?(event: ChangeEvent): void
83
+ /** @android @ios @web */
84
+ onFocus?(event: FocusEvent): void
85
+ /** @android @ios @web */
86
+ onBlur?(event: BlurEvent): void
49
87
  }
50
- const Input = forwardRef<HTMLInputElement, Readonly<InputProps>>(
51
- (props, ref) => {
52
- const { onChange } = props
88
+ const Input = forwardRef<HTMLInputElement, Readonly<InputProps>>((props, ref) => {
89
+ const { onChange } = props
53
90
 
54
- return (
55
- <input
56
- ref={ref}
57
- type={props.type ?? "text"}
58
- inputMode={props.inputMode}
59
- placeholder={props.placeholder}
60
- defaultValue={props.defaultValue}
61
- value={props.value}
62
- name={props.name}
63
- min={props.min}
64
- max={props.max}
65
- minLength={props.minLength}
66
- maxLength={props.maxLength}
67
- pattern={props.pattern}
68
- required={props.required}
69
- readOnly={props.readOnly}
70
- disabled={props.disabled}
71
- onChange={onChange}
72
- onBlur={props.onBlur}
73
- onInvalid={e => e.preventDefault()}
74
- id={props.id}
75
- autoComplete={props.autoComplete}
76
- data-invalid={props.invalid}
77
- aria-describedby={props.describedby}
78
- className={cn(
79
- "flex items-center w-full h-12 px-4",
80
- "peer rounded-2xl",
81
- "transition transition-duration-150 transition-[border,box-shadow] ease-in",
82
- input_variant[props.variant ?? "primary"],
83
- props.className,
84
- )}
85
- />
86
- )
87
- },
88
- )
91
+ return (
92
+ <input
93
+ ref={ref}
94
+ type={props.type ?? "text"}
95
+ inputMode={props.inputMode}
96
+ placeholder={props.placeholder}
97
+ defaultValue={props.defaultValue}
98
+ value={props.value}
99
+ name={props.name}
100
+ min={props.min}
101
+ max={props.max}
102
+ minLength={props.minLength}
103
+ maxLength={props.maxLength}
104
+ pattern={props.pattern}
105
+ required={props.required}
106
+ readOnly={props.readOnly}
107
+ disabled={props.disabled}
108
+ onChange={onChange}
109
+ onFocus={props.onFocus}
110
+ onBlur={props.onBlur}
111
+ onInvalid={e => e.preventDefault()}
112
+ id={props.id}
113
+ autoCapitalize={props.autoCapitalize}
114
+ autoComplete={props.autoComplete}
115
+ autoCorrect={props.autoCorrect ? "on" : "off"}
116
+ aria-describedby={props.describedby}
117
+ data-invalid={props.invalid}
118
+ className={cn(
119
+ "flex items-center w-full h-12 px-4",
120
+ "transition transition-duration-150 transition-[box-shadow] ease-in",
121
+ "peer rounded-2xl",
122
+ input_variant[props.variant ?? "primary"],
123
+ props.className,
124
+ )}
125
+ />
126
+ )
127
+ })
89
128
  export default Input
package/src/label.tsx CHANGED
@@ -23,12 +23,7 @@ export default function Label(props: LabelProps) {
23
23
  <label
24
24
  htmlFor={props.htmlFor}
25
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
- )}
26
+ className={cn("flex items-center gap-2", "peer-disabled:text-disabled invalid:text-red text-sm/4 font-medium", "select-none", props.className)}
32
27
  >
33
28
  {props.children}
34
29
  </label>
@@ -0,0 +1,45 @@
1
+ import type { ChangeEvent, ReactNode } from "react"
2
+ import { cn } from "./utils"
3
+
4
+ export interface SelectProps {
5
+ children?: ReactNode
6
+ name?: string
7
+ id?: string
8
+ onChange?: (event: ChangeEvent<HTMLSelectElement>) => void
9
+ }
10
+ export function Select(props: SelectProps) {
11
+ return (
12
+ <select
13
+ name={props.name}
14
+ id={props.id}
15
+ onChange={props.onChange}
16
+ className={cn(
17
+ "flex items-center h-12 px-4 rounded-2xl bg-background/25 backdrop-blur-sm inset-ring inset-ring-border",
18
+ "open:inset-ring-primary/75 open:ring-4 open:ring-primary/10 dark:open:ring-primary/15 focus:inset-ring-primary/75 focus:ring-4 focus:ring-primary/10 dark:focus:ring-primary/15",
19
+ "transition duration-150 ease-in",
20
+ )}
21
+ >
22
+ {props.children}
23
+ </select>
24
+ )
25
+ }
26
+
27
+ export interface SelectItemProps {
28
+ value: string
29
+ children?: ReactNode
30
+ className?: string
31
+ }
32
+ export function SelectItem(props: SelectItemProps) {
33
+ return (
34
+ <option
35
+ className={cn(
36
+ "gap-4 h-9 min-h-9 px-3 rounded-lg text-foreground hover:bg-secondary focus:bg-secondary checked:bg-secondary",
37
+ "transition duration-150 ease-out",
38
+ props.className,
39
+ )}
40
+ value={props.value}
41
+ >
42
+ {props.children}
43
+ </option>
44
+ )
45
+ }
package/src/global.ts DELETED
@@ -1,36 +0,0 @@
1
- import { cn } from "./utils"
2
-
3
- export type Variant =
4
- | "primary"
5
- | "secondary"
6
- | "destructive"
7
- | "outline"
8
- | "ghost"
9
- | "link"
10
- | "none"
11
- export type Variants = Record<Variant, string>
12
-
13
- export const input_variant: Variants = {
14
- primary: cn(
15
- "bg-background",
16
- "placeholder:text-description disabled:text-disabled",
17
- // "inset-ring inset-ring-disabled disabled:inset-ring-outline focus:inset-ring-primary",
18
- "border border-disabled disabled:border-outline focus:border-primary/75",
19
- "focus:ring-4 focus:ring-primary/10",
20
- "invalid:inset-ring-red",
21
- "data-[invalid=true]:border data-[invalid=true]:border-red/50 data-[invalid=true]:focus:border-red",
22
- "data-[invalid=true]:focus:ring-4 data-[invalid=true]:focus:ring-red/10",
23
- ),
24
- secondary: cn(),
25
- destructive: cn(
26
- "bg-background",
27
- "placeholder:text-description disabled:text-disabled",
28
- // "inset-ring inset-ring-(--red)/50 disabled:inset-ring-(--red)/25 focus:inset-ring-(--red)",
29
- "border border-red/50 disabled:border-red/25 focus:border-red",
30
- "focus:ring-4 focus:ring-red/5",
31
- ),
32
- outline: cn(),
33
- ghost: cn(),
34
- link: cn(),
35
- none: cn("text-()"),
36
- }