@neoptocom/neopto-ui 0.5.1 → 0.6.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/dist/index.js CHANGED
@@ -2,7 +2,6 @@ import * as React2 from 'react';
2
2
  import { useState, useMemo, useId, useRef, useCallback, useEffect } from 'react';
3
3
  import { jsx, jsxs } from 'react/jsx-runtime';
4
4
  import { createPortal } from 'react-dom';
5
- import { tv } from 'tailwind-variants';
6
5
 
7
6
  // src/components/Input.tsx
8
7
  var Input = React2.forwardRef(
@@ -94,41 +93,40 @@ function Modal({
94
93
  const container = document.body;
95
94
  return createPortal(overlay, container);
96
95
  }
97
- var styles = tv({
98
- base: "text-current",
99
- variants: {
100
- variant: {
101
- "display-lg": "text-5xl leading-tight",
102
- "display-md": "text-4xl leading-tight",
103
- "display-sm": "text-4xl leading-tight",
104
- "headline-lg": "text-3xl leading-tight",
105
- "headline-md": "text-3xl leading-tight",
106
- "headline-sm": "text-3xl leading-tight",
107
- "title-lg": "text-xl leading-tight",
108
- "title-md": "text-lg leading-tight",
109
- "title-sm": "text-base leading-tight",
110
- "label-lg": "text-sm leading-tight",
111
- "label-md": "text-xs leading-tight",
112
- "label-sm": "text-xs leading-tight",
113
- "body-lg": "text-base leading-relaxed",
114
- "body-md": "text-sm leading-relaxed",
115
- "body-sm": "text-xs leading-relaxed",
116
- "button": "text-base leading-normal"
117
- },
118
- weight: {
119
- normal: "font-normal",
120
- medium: "font-medium",
121
- semibold: "font-semibold",
122
- bold: "font-bold"
123
- },
124
- muted: {
125
- true: "text-[var(--muted-fg)]"
126
- }
127
- },
128
- defaultVariants: {
129
- weight: "normal"
130
- }
131
- });
96
+ function getTypoClasses(variant, weight = "normal", muted, className) {
97
+ const base = "text-current";
98
+ const variants = {
99
+ "display-lg": "text-5xl leading-tight",
100
+ "display-md": "text-4xl leading-tight",
101
+ "display-sm": "text-4xl leading-tight",
102
+ "headline-lg": "text-3xl leading-tight",
103
+ "headline-md": "text-3xl leading-tight",
104
+ "headline-sm": "text-3xl leading-tight",
105
+ "title-lg": "text-xl leading-tight",
106
+ "title-md": "text-lg leading-tight",
107
+ "title-sm": "text-base leading-tight",
108
+ "label-lg": "text-sm leading-tight",
109
+ "label-md": "text-xs leading-tight",
110
+ "label-sm": "text-xs leading-tight",
111
+ "body-lg": "text-base leading-relaxed",
112
+ "body-md": "text-sm leading-relaxed",
113
+ "body-sm": "text-xs leading-relaxed",
114
+ "button": "text-base leading-normal"
115
+ };
116
+ const weights = {
117
+ normal: "font-normal",
118
+ medium: "font-medium",
119
+ semibold: "font-semibold",
120
+ bold: "font-bold"
121
+ };
122
+ return [
123
+ base,
124
+ variants[variant],
125
+ weights[weight],
126
+ muted ? "text-[var(--muted-fg)]" : "",
127
+ className
128
+ ].filter(Boolean).join(" ");
129
+ }
132
130
  function Typo({
133
131
  variant,
134
132
  bold,
@@ -148,23 +146,21 @@ function Typo({
148
146
  return /* @__PURE__ */ jsx(
149
147
  Component,
150
148
  {
151
- className: styles({ variant, weight: bold, muted, className }),
149
+ className: getTypoClasses(variant, bold, muted, className),
152
150
  style: { fontFamily: getFontFamily(variant) },
153
151
  ...props,
154
152
  children
155
153
  }
156
154
  );
157
155
  }
158
- var avatarStyles = tv({
159
- base: "relative box-border flex items-center justify-center overflow-hidden rounded-full border border-[var(--border)] bg-[var(--muted)] text-[var(--fg)] select-none",
160
- variants: {
161
- size: {
162
- sm: "w-[28px] h-[28px]",
163
- md: "w-[60px] h-[60px]"
164
- }
165
- },
166
- defaultVariants: { size: "sm" }
167
- });
156
+ function getAvatarClasses(size = "sm", className) {
157
+ const base = "relative box-border flex items-center justify-center overflow-hidden rounded-full border border-[var(--border)] bg-[var(--muted)] text-[var(--fg)] select-none";
158
+ const sizes = {
159
+ sm: "w-[28px] h-[28px]",
160
+ md: "w-[60px] h-[60px]"
161
+ };
162
+ return [base, sizes[size], className].filter(Boolean).join(" ");
163
+ }
168
164
  function getInitials(name) {
169
165
  if (!name) return "\u2026";
170
166
  const words = name.trim().split(/\s+/);
@@ -193,7 +189,7 @@ function Avatar({
193
189
  return /* @__PURE__ */ jsx(
194
190
  "div",
195
191
  {
196
- className: avatarStyles({ size, className }),
192
+ className: getAvatarClasses(size, className),
197
193
  "aria-label": alt ?? name,
198
194
  role: "img",
199
195
  ...props,
@@ -305,29 +301,20 @@ function Icon({
305
301
  }
306
302
  );
307
303
  }
308
- var iconButtonStyles = tv({
309
- base: [
310
- "flex items-center justify-center rounded-full flex-shrink-0",
311
- "transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2",
312
- "focus-visible:ring-cyan-500/40 disabled:cursor-not-allowed disabled:opacity-50"
313
- ].join(" "),
314
- variants: {
315
- variant: {
316
- ghost: "bg-transparent hover:bg-[var(--muted)] active:bg-[var(--muted)]",
317
- primary: "bg-cyan-500 text-white hover:bg-cyan-400 active:bg-cyan-600",
318
- secondary: "border border-[var(--border)] bg-[var(--surface)] hover:bg-[var(--muted)] active:bg-[var(--muted)]"
319
- },
320
- size: {
321
- sm: "w-8 h-8",
322
- md: "w-10 h-10",
323
- lg: "w-12 h-12"
324
- }
325
- },
326
- defaultVariants: {
327
- variant: "ghost",
328
- size: "md"
329
- }
330
- });
304
+ function getIconButtonClasses(variant = "ghost", size = "md", className) {
305
+ const base = "flex items-center justify-center rounded-full flex-shrink-0 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-cyan-500/40 disabled:cursor-not-allowed disabled:opacity-50";
306
+ const variants = {
307
+ ghost: "bg-transparent hover:bg-[var(--muted)] active:bg-[var(--muted)]",
308
+ primary: "bg-cyan-500 text-white hover:bg-cyan-400 active:bg-cyan-600",
309
+ secondary: "border border-[var(--border)] bg-[var(--surface)] hover:bg-[var(--muted)] active:bg-[var(--muted)]"
310
+ };
311
+ const sizes = {
312
+ sm: "w-8 h-8",
313
+ md: "w-10 h-10",
314
+ lg: "w-12 h-12"
315
+ };
316
+ return [base, variants[variant], sizes[size], className].filter(Boolean).join(" ");
317
+ }
331
318
  var IconButton = React2.forwardRef(
332
319
  ({
333
320
  variant,
@@ -347,7 +334,7 @@ var IconButton = React2.forwardRef(
347
334
  "button",
348
335
  {
349
336
  ref,
350
- className: iconButtonStyles({ variant, size, className }),
337
+ className: getIconButtonClasses(variant, size, className),
351
338
  ...props,
352
339
  children: /* @__PURE__ */ jsx(
353
340
  Icon,
@@ -761,35 +748,33 @@ function Search({
761
748
  }
762
749
  );
763
750
  }
764
- var buttonStyles = tv({
765
- base: "cursor-pointer inline-flex items-center justify-center gap-2 rounded-[var(--radius-2xl)] px-[18px] h-12 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-cyan-500/40 disabled:cursor-not-allowed disabled:opacity-50",
766
- variants: {
767
- variant: {
768
- primary: "bg-cyan-500 text-white hover:bg-cyan-400 active:bg-cyan-600 disabled:bg-neutral-400",
769
- secondary: "border border-cyan-500 text-cyan-500 bg-transparent hover:bg-cyan-50 active:bg-cyan-100 disabled:border-neutral-400 disabled:text-neutral-400",
770
- ghost: "bg-transparent text-cyan-500 hover:bg-cyan-50 active:bg-cyan-100 disabled:text-neutral-400"
771
- },
772
- size: {
773
- sm: "h-9 px-3",
774
- md: "h-12 px-[18px]",
775
- lg: "h-14 px-6"
776
- },
777
- fullWidth: {
778
- true: "w-full"
779
- }
780
- },
781
- defaultVariants: {
782
- variant: "primary",
783
- size: "md"
784
- }
785
- });
751
+ function getButtonClasses(variant = "primary", size = "md", fullWidth, className) {
752
+ const base = "cursor-pointer inline-flex items-center justify-center gap-2 rounded-[var(--radius-2xl)] transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-cyan-500/40 disabled:cursor-not-allowed disabled:opacity-50";
753
+ const variants = {
754
+ primary: "bg-cyan-500 text-white hover:bg-cyan-400 active:bg-cyan-600 disabled:bg-neutral-400",
755
+ secondary: "border border-cyan-500 text-cyan-500 bg-transparent hover:bg-cyan-50 active:bg-cyan-100 disabled:border-neutral-400 disabled:text-neutral-400",
756
+ ghost: "bg-transparent text-cyan-500 hover:bg-cyan-50 active:bg-cyan-100 disabled:text-neutral-400"
757
+ };
758
+ const sizes = {
759
+ sm: "h-9 px-3",
760
+ md: "h-12 px-[18px]",
761
+ lg: "h-14 px-6"
762
+ };
763
+ return [
764
+ base,
765
+ variants[variant],
766
+ sizes[size],
767
+ fullWidth ? "w-full" : "",
768
+ className
769
+ ].filter(Boolean).join(" ");
770
+ }
786
771
  var Button = React2.forwardRef(
787
772
  ({ variant, size, fullWidth, className, children, icon, ...props }, ref) => {
788
773
  return /* @__PURE__ */ jsx(
789
774
  "button",
790
775
  {
791
776
  ref,
792
- className: buttonStyles({ variant, size, fullWidth, className }),
777
+ className: getButtonClasses(variant, size, fullWidth, className),
793
778
  ...props,
794
779
  children
795
780
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neoptocom/neopto-ui",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "private": false,
5
5
  "description": "A modern React component library built with Tailwind CSS v4 and TypeScript. Features dark mode, design tokens, and comprehensive Storybook documentation. Requires Tailwind v4+.",
6
6
  "keywords": [
@@ -40,13 +40,8 @@
40
40
  "files": [
41
41
  "dist",
42
42
  "src",
43
- "scripts/init.mjs",
44
43
  "CONSUMER_SETUP.md"
45
44
  ],
46
- "bin": {
47
- "neopto-ui": "./scripts/init.mjs",
48
- "neopto-ui-init": "./scripts/init.mjs"
49
- },
50
45
  "scripts": {
51
46
  "dev": "vite",
52
47
  "build": "tsup && npm run build:css",
@@ -63,9 +58,7 @@
63
58
  "react": ">=18",
64
59
  "react-dom": ">=18"
65
60
  },
66
- "dependencies": {
67
- "tailwind-variants": "^0.2.0"
68
- },
61
+ "dependencies": {},
69
62
  "devDependencies": {
70
63
  "@changesets/cli": "^2.27.8",
71
64
  "@storybook/addon-actions": "^8.1.0",
@@ -1,6 +1,5 @@
1
1
  import * as React from "react";
2
2
  import { useState, useMemo } from "react";
3
- import { tv, type VariantProps } from "tailwind-variants";
4
3
  import Typo from "./Typo";
5
4
 
6
5
  export type AvatarProps = {
@@ -12,21 +11,22 @@ export type AvatarProps = {
12
11
  color?: string;
13
12
  /** Accessible alt text; defaults to the person's name */
14
13
  alt?: string;
15
- } & VariantProps<typeof avatarStyles> &
16
- Omit<React.HTMLAttributes<HTMLDivElement>, "children">;
14
+ /** Avatar size */
15
+ size?: "sm" | "md";
16
+ } & Omit<React.HTMLAttributes<HTMLDivElement>, "children">;
17
17
 
18
- const avatarStyles = tv({
19
- base:
18
+ function getAvatarClasses(size: AvatarProps["size"] = "sm", className?: string): string {
19
+ const base =
20
20
  "relative box-border flex items-center justify-center overflow-hidden rounded-full " +
21
- "border border-[var(--border)] bg-[var(--muted)] text-[var(--fg)] select-none",
22
- variants: {
23
- size: {
24
- sm: "w-[28px] h-[28px]",
25
- md: "w-[60px] h-[60px]"
26
- }
27
- },
28
- defaultVariants: { size: "sm" }
29
- });
21
+ "border border-[var(--border)] bg-[var(--muted)] text-[var(--fg)] select-none";
22
+
23
+ const sizes = {
24
+ sm: "w-[28px] h-[28px]",
25
+ md: "w-[60px] h-[60px]"
26
+ };
27
+
28
+ return [base, sizes[size], className].filter(Boolean).join(" ");
29
+ }
30
30
 
31
31
  function getInitials(name: string) {
32
32
  if (!name) return "…";
@@ -59,7 +59,7 @@ export default function Avatar({
59
59
 
60
60
  return (
61
61
  <div
62
- className={avatarStyles({ size, className })}
62
+ className={getAvatarClasses(size, className)}
63
63
  aria-label={alt ?? name}
64
64
  role="img"
65
65
  {...props}
@@ -1,46 +1,54 @@
1
1
  import * as React from "react";
2
- import { tv, type VariantProps } from "tailwind-variants";
3
- import Typo from "./Typo";
4
2
 
5
- const buttonStyles = tv({
6
- base:
7
- "cursor-pointer inline-flex items-center justify-center gap-2 rounded-[var(--radius-2xl)] px-[18px] h-12 transition-colors " +
8
- "focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-cyan-500/40 " +
9
- "disabled:cursor-not-allowed disabled:opacity-50",
10
- variants: {
11
- variant: {
12
- primary: "bg-cyan-500 text-white hover:bg-cyan-400 active:bg-cyan-600 disabled:bg-neutral-400",
13
- secondary: "border border-cyan-500 text-cyan-500 bg-transparent hover:bg-cyan-50 active:bg-cyan-100 disabled:border-neutral-400 disabled:text-neutral-400",
14
- ghost: "bg-transparent text-cyan-500 hover:bg-cyan-50 active:bg-cyan-100 disabled:text-neutral-400"
15
- },
16
- size: {
17
- sm: "h-9 px-3",
18
- md: "h-12 px-[18px]",
19
- lg: "h-14 px-6"
20
- },
21
- fullWidth: {
22
- true: "w-full"
23
- }
24
- },
25
- defaultVariants: {
26
- variant: "primary",
27
- size: "md"
28
- }
29
- });
30
-
31
- type ButtonVariants = VariantProps<typeof buttonStyles>;
32
- export type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & ButtonVariants & {
3
+ export type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
4
+ /** Button visual variant */
5
+ variant?: "primary" | "secondary" | "ghost";
6
+ /** Button size */
7
+ size?: "sm" | "md" | "lg";
8
+ /** Make button full width */
9
+ fullWidth?: boolean;
33
10
  /** Icon component to display instead of text */
34
11
  icon?: React.ReactNode;
35
12
  };
36
13
 
14
+ function getButtonClasses(
15
+ variant: ButtonProps["variant"] = "primary",
16
+ size: ButtonProps["size"] = "md",
17
+ fullWidth?: boolean,
18
+ className?: string
19
+ ): string {
20
+ const base =
21
+ "cursor-pointer inline-flex items-center justify-center gap-2 rounded-[var(--radius-2xl)] transition-colors " +
22
+ "focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-cyan-500/40 " +
23
+ "disabled:cursor-not-allowed disabled:opacity-50";
24
+
25
+ const variants = {
26
+ primary: "bg-cyan-500 text-white hover:bg-cyan-400 active:bg-cyan-600 disabled:bg-neutral-400",
27
+ secondary: "border border-cyan-500 text-cyan-500 bg-transparent hover:bg-cyan-50 active:bg-cyan-100 disabled:border-neutral-400 disabled:text-neutral-400",
28
+ ghost: "bg-transparent text-cyan-500 hover:bg-cyan-50 active:bg-cyan-100 disabled:text-neutral-400"
29
+ };
30
+
31
+ const sizes = {
32
+ sm: "h-9 px-3",
33
+ md: "h-12 px-[18px]",
34
+ lg: "h-14 px-6"
35
+ };
36
+
37
+ return [
38
+ base,
39
+ variants[variant],
40
+ sizes[size],
41
+ fullWidth ? "w-full" : "",
42
+ className
43
+ ].filter(Boolean).join(" ");
44
+ }
45
+
37
46
  export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
38
47
  ({ variant, size, fullWidth, className, children, icon, ...props }, ref) => {
39
-
40
48
  return (
41
49
  <button
42
50
  ref={ref}
43
- className={buttonStyles({ variant, size, fullWidth, className })}
51
+ className={getButtonClasses(variant, size, fullWidth, className)}
44
52
  {...props}
45
53
  >
46
54
  {children}
@@ -1,45 +1,46 @@
1
1
  import * as React from "react";
2
- import { tv, type VariantProps } from "tailwind-variants";
3
2
  import Icon from "./Icon";
4
3
 
5
- const iconButtonStyles = tv({
6
- base: [
7
- "flex items-center justify-center rounded-full flex-shrink-0",
8
- "transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2",
9
- "focus-visible:ring-cyan-500/40 disabled:cursor-not-allowed disabled:opacity-50"
10
- ].join(" "),
11
- variants: {
12
- variant: {
13
- ghost: "bg-transparent hover:bg-[var(--muted)] active:bg-[var(--muted)]",
14
- primary: "bg-cyan-500 text-white hover:bg-cyan-400 active:bg-cyan-600",
15
- secondary: "border border-[var(--border)] bg-[var(--surface)] hover:bg-[var(--muted)] active:bg-[var(--muted)]"
16
- },
17
- size: {
18
- sm: "w-8 h-8",
19
- md: "w-10 h-10",
20
- lg: "w-12 h-12"
21
- }
22
- },
23
- defaultVariants: {
24
- variant: "ghost",
25
- size: "md"
26
- }
27
- });
4
+ export type IconButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
5
+ /** Button visual variant */
6
+ variant?: "ghost" | "primary" | "secondary";
7
+ /** Button size */
8
+ size?: "sm" | "md" | "lg";
9
+ /** Material Symbols icon name */
10
+ icon: string;
11
+ /** Icon size (defaults to button size) */
12
+ iconSize?: "sm" | "md" | "lg";
13
+ /** Icon fill (0 = outlined, 1 = filled) */
14
+ iconFill?: 0 | 1;
15
+ /** Optional custom className for the icon */
16
+ iconClassName?: string;
17
+ };
28
18
 
29
- type IconButtonVariants = VariantProps<typeof iconButtonStyles>;
19
+ function getIconButtonClasses(
20
+ variant: IconButtonProps["variant"] = "ghost",
21
+ size: IconButtonProps["size"] = "md",
22
+ className?: string
23
+ ): string {
24
+ const base =
25
+ "flex items-center justify-center rounded-full flex-shrink-0 " +
26
+ "transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 " +
27
+ "focus-visible:ring-cyan-500/40 disabled:cursor-not-allowed disabled:opacity-50";
30
28
 
31
- export type IconButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> &
32
- IconButtonVariants & {
33
- /** Material Symbols icon name */
34
- icon: string;
35
- /** Icon size (defaults to button size) */
36
- iconSize?: "sm" | "md" | "lg";
37
- /** Icon fill (0 = outlined, 1 = filled) */
38
- iconFill?: 0 | 1;
39
- /** Optional custom className for the icon */
40
- iconClassName?: string;
29
+ const variants = {
30
+ ghost: "bg-transparent hover:bg-[var(--muted)] active:bg-[var(--muted)]",
31
+ primary: "bg-cyan-500 text-white hover:bg-cyan-400 active:bg-cyan-600",
32
+ secondary: "border border-[var(--border)] bg-[var(--surface)] hover:bg-[var(--muted)] active:bg-[var(--muted)]"
41
33
  };
42
34
 
35
+ const sizes = {
36
+ sm: "w-8 h-8",
37
+ md: "w-10 h-10",
38
+ lg: "w-12 h-12"
39
+ };
40
+
41
+ return [base, variants[variant], sizes[size], className].filter(Boolean).join(" ");
42
+ }
43
+
43
44
  export const IconButton = React.forwardRef<HTMLButtonElement, IconButtonProps>(
44
45
  (
45
46
  {
@@ -71,7 +72,7 @@ export const IconButton = React.forwardRef<HTMLButtonElement, IconButtonProps>(
71
72
  return (
72
73
  <button
73
74
  ref={ref}
74
- className={iconButtonStyles({ variant, size, className })}
75
+ className={getIconButtonClasses(variant, size, className)}
75
76
  {...props}
76
77
  >
77
78
  <Icon
@@ -1,5 +1,4 @@
1
1
  import * as React from "react";
2
- import { tv, type VariantProps } from "tailwind-variants";
3
2
 
4
3
  export type TypoVariant =
5
4
  | "display-lg" | "display-md" | "display-sm"
@@ -11,41 +10,48 @@ export type TypoVariant =
11
10
 
12
11
  export type TypoWeight = "normal" | "medium" | "semibold" | "bold";
13
12
 
14
- const styles = tv({
15
- base: "text-current",
16
- variants: {
17
- variant: {
18
- "display-lg": "text-5xl leading-tight",
19
- "display-md": "text-4xl leading-tight",
20
- "display-sm": "text-4xl leading-tight",
21
- "headline-lg": "text-3xl leading-tight",
22
- "headline-md": "text-3xl leading-tight",
23
- "headline-sm": "text-3xl leading-tight",
24
- "title-lg": "text-xl leading-tight",
25
- "title-md": "text-lg leading-tight",
26
- "title-sm": "text-base leading-tight",
27
- "label-lg": "text-sm leading-tight",
28
- "label-md": "text-xs leading-tight",
29
- "label-sm": "text-xs leading-tight",
30
- "body-lg": "text-base leading-relaxed",
31
- "body-md": "text-sm leading-relaxed",
32
- "body-sm": "text-xs leading-relaxed",
33
- "button": "text-base leading-normal"
34
- },
35
- weight: {
36
- normal: "font-normal",
37
- medium: "font-medium",
38
- semibold: "font-semibold",
39
- bold: "font-bold"
40
- },
41
- muted: {
42
- true: "text-[var(--muted-fg)]"
43
- }
44
- },
45
- defaultVariants: {
46
- weight: "normal"
47
- }
48
- });
13
+ function getTypoClasses(
14
+ variant: TypoVariant,
15
+ weight: TypoWeight = "normal",
16
+ muted?: boolean,
17
+ className?: string
18
+ ): string {
19
+ const base = "text-current";
20
+
21
+ const variants: Record<TypoVariant, string> = {
22
+ "display-lg": "text-5xl leading-tight",
23
+ "display-md": "text-4xl leading-tight",
24
+ "display-sm": "text-4xl leading-tight",
25
+ "headline-lg": "text-3xl leading-tight",
26
+ "headline-md": "text-3xl leading-tight",
27
+ "headline-sm": "text-3xl leading-tight",
28
+ "title-lg": "text-xl leading-tight",
29
+ "title-md": "text-lg leading-tight",
30
+ "title-sm": "text-base leading-tight",
31
+ "label-lg": "text-sm leading-tight",
32
+ "label-md": "text-xs leading-tight",
33
+ "label-sm": "text-xs leading-tight",
34
+ "body-lg": "text-base leading-relaxed",
35
+ "body-md": "text-sm leading-relaxed",
36
+ "body-sm": "text-xs leading-relaxed",
37
+ "button": "text-base leading-normal"
38
+ };
39
+
40
+ const weights: Record<TypoWeight, string> = {
41
+ normal: "font-normal",
42
+ medium: "font-medium",
43
+ semibold: "font-semibold",
44
+ bold: "font-bold"
45
+ };
46
+
47
+ return [
48
+ base,
49
+ variants[variant],
50
+ weights[weight],
51
+ muted ? "text-[var(--muted-fg)]" : "",
52
+ className
53
+ ].filter(Boolean).join(" ");
54
+ }
49
55
 
50
56
  export type TypoProps<T extends React.ElementType = "span"> = {
51
57
  /**
@@ -83,7 +89,7 @@ export default function Typo<T extends React.ElementType = "span">({
83
89
 
84
90
  return (
85
91
  <Component
86
- className={styles({ variant, weight: bold, muted, className })}
92
+ className={getTypoClasses(variant, bold, muted, className)}
87
93
  style={{ fontFamily: getFontFamily(variant) }}
88
94
  {...props}
89
95
  >