From 6c6dfba85b990a1859e1f4cd6da6bfa9dd8a65e6 Mon Sep 17 00:00:00 2001 From: ABILASH <114130954+Abilash-19@users.noreply.github.com> Date: Mon, 16 Feb 2026 23:25:25 +0530 Subject: [PATCH 01/14] feat: button compoent and its varients --- src/assets/Icons/Spinner.tsx | 28 ++++ src/components/Button/Button.stories.tsx | 156 ++++++++++----------- src/components/Button/Button.test.tsx | 34 ++--- src/components/Button/Button.tsx | 170 +++++++++++++++-------- src/components/Button/Button.types.tsx | 38 +++-- src/components/Modal/Modal.stories.tsx | 81 +++-------- 6 files changed, 280 insertions(+), 227 deletions(-) create mode 100644 src/assets/Icons/Spinner.tsx diff --git a/src/assets/Icons/Spinner.tsx b/src/assets/Icons/Spinner.tsx new file mode 100644 index 0000000..44721fc --- /dev/null +++ b/src/assets/Icons/Spinner.tsx @@ -0,0 +1,28 @@ +import { cn } from "@/utils"; + +export const Spinner = ({ size }: { size: string }) => { + return ( + + ); +}; diff --git a/src/components/Button/Button.stories.tsx b/src/components/Button/Button.stories.tsx index d3ee671..6310817 100644 --- a/src/components/Button/Button.stories.tsx +++ b/src/components/Button/Button.stories.tsx @@ -1,119 +1,119 @@ -import type { Meta, StoryObj } from '@storybook/react-vite'; -import { Button } from './Button'; +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { Button } from "./Button"; const meta: Meta = { - title: 'Components/Button', - component: Button, - parameters: { - layout: 'centered', + title: "Components/Button", + component: Button, + parameters: { + layout: "centered", + }, + tags: ["autodocs"], + argTypes: { + variant: { + control: "select", + options: ["primary", "secondary", "outline", "ghost", "danger"], }, - tags: ['autodocs'], - argTypes: { - variant: { - control: 'select', - options: ['primary', 'secondary', 'outline', 'ghost', 'danger'], - }, - size: { - control: 'select', - options: ['sm', 'md', 'lg'], - }, - isLoading: { - control: 'boolean', - }, - disabled: { - control: 'boolean', - }, + size: { + control: "select", + options: ["sm", "md", "lg"], }, + isLoading: { + control: "boolean", + }, + disabled: { + control: "boolean", + }, + }, }; export default meta; type Story = StoryObj; export const Primary: Story = { - args: { - children: 'Primary Button', - variant: 'primary', - }, + args: { + text: "Primary Button", + variant: "primary", + }, }; export const Secondary: Story = { - args: { - children: 'Secondary Button', - variant: 'secondary', - }, + args: { + text: "Secondary Button", + variant: "secondary", + }, }; export const Outline: Story = { - args: { - children: 'Outline Button', - variant: 'outline', - }, + args: { + text: "Outline Button", + variant: "outline", + }, }; export const Ghost: Story = { - args: { - children: 'Ghost Button', - variant: 'ghost', - }, + args: { + text: "Ghost Button", + variant: "ghost", + }, }; export const Danger: Story = { - args: { - children: 'Danger Button', - variant: 'danger', - }, + args: { + text: "Danger Button", + variant: "danger", + }, }; export const Small: Story = { - args: { - children: 'Small Button', - size: 'sm', - }, + args: { + text: "Small Button", + size: "sm", + }, }; export const Medium: Story = { - args: { - children: 'Medium Button', - size: 'md', - }, + args: { + text: "Medium Button", + size: "md", + }, }; export const Large: Story = { - args: { - children: 'Large Button', - size: 'lg', - }, + args: { + text: "Large Button", + size: "lg", + }, }; export const Loading: Story = { - args: { - children: 'Loading Button', - isLoading: true, - }, + args: { + text: "Loading Button", + isLoading: true, + }, }; export const Disabled: Story = { - args: { - children: 'Disabled Button', - disabled: true, - }, + args: { + text: "Disabled Button", + disabled: true, + }, }; export const AllVariants: Story = { - render: () => ( -
-
- - - - - -
-
- - - -
-
- ), + render: () => ( +
+
+ + + + + +
+
+ + + +
+
+ ), }; diff --git a/src/components/Button/Button.test.tsx b/src/components/Button/Button.test.tsx index af7a8e8..c1d4755 100644 --- a/src/components/Button/Button.test.tsx +++ b/src/components/Button/Button.test.tsx @@ -10,100 +10,100 @@ const render = (ui: React.ReactElement) => { }; describe("Button", () => { - it("renders with children", () => { - render(); + it("renders with text", () => { + render(); + render(); + render(); + render(); + render(); + render(); + render(); + render(); + render(); + render(); + render(); + render(); + render(); + render(); + render( ); diff --git a/src/components/Button/Button.types.tsx b/src/components/Button/Button.types.tsx index 1da534f..58efa8c 100644 --- a/src/components/Button/Button.types.tsx +++ b/src/components/Button/Button.types.tsx @@ -1,12 +1,34 @@ -import { type ButtonHTMLAttributes, type ReactNode } from 'react'; +import { type ButtonHTMLAttributes, type ReactNode } from "react"; -export type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger'; -export type ButtonSize = 'sm' | 'md' | 'lg'; +export type ButtonVariant = + | "primary" + | "secondary" + | "outline" + | "ghost" + | "danger"; +export type ButtonSize = "sm" | "md" | "lg"; export interface ButtonProps extends ButtonHTMLAttributes { - variant?: ButtonVariant; - size?: ButtonSize; - isLoading?: boolean; - children: ReactNode; + // Content + text?: string; -} \ No newline at end of file + // Visual variants + variant?: ButtonVariant; + size?: ButtonSize; + + // Icons + prefixIcon?: ReactNode; + suffixIcon?: ReactNode; + + // States + isLoading?: boolean; + disabled?: boolean; + + // Interaction + onClick?: (event: React.MouseEvent) => void; + + // Accessibility + "aria-label"?: string; + "aria-describedby"?: string; + "aria-pressed"?: boolean; +} diff --git a/src/components/Modal/Modal.stories.tsx b/src/components/Modal/Modal.stories.tsx index 23c7b31..7f2ef22 100644 --- a/src/components/Modal/Modal.stories.tsx +++ b/src/components/Modal/Modal.stories.tsx @@ -2,6 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; import { useState } from "react"; import { Modal } from "./Modal"; +import { Button } from "../Button"; const meta: Meta = { title: "Components/Modal", @@ -50,12 +51,7 @@ const ModalWrapper = (args: any) => { return ( <> - + setIsOpen(false)} /> ); @@ -72,9 +68,7 @@ export const Default: Story = { This is a modal dialog. You can close it by clicking the close button, clicking outside, or pressing the Escape key.

- + ), }, @@ -87,30 +81,10 @@ export const Sizes: Story = { return (
- - - - + + + + - + setIsOpen(false)} @@ -253,18 +224,12 @@ export const ConfirmationDialog: Story = { undone.

- - + +
@@ -286,12 +251,7 @@ export const FormModal: Story = { return ( <> - + setIsOpen(false)} @@ -319,19 +279,14 @@ export const FormModal: Story = { />
- - + +
From a81be3154ce0309240cdf50ef7234f0936092699 Mon Sep 17 00:00:00 2001 From: ABILASH <114130954+Abilash-19@users.noreply.github.com> Date: Mon, 16 Feb 2026 23:41:17 +0530 Subject: [PATCH 02/14] feat: new playground --- src/App.tsx | 557 +++++++++++++++++++++++++++++++++----- src/theme/DefaultTheme.ts | 14 +- src/utils/utils.ts | 37 +++ 3 files changed, 526 insertions(+), 82 deletions(-) create mode 100644 src/utils/utils.ts diff --git a/src/App.tsx b/src/App.tsx index b3a72e6..eb5b4b7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,11 +2,482 @@ import { useState } from "react"; import { Button } from "@/components/Button"; import { Input } from "@/components/Input"; import { Modal } from "@/components/Modal"; +import { Typography } from "@/components/Typography"; import { ThemeProvider, darkTheme } from "@/theme"; +import { useTheme } from "@/theme"; import "@/theme/index.css"; -function App() { +// Inner component that can access theme context +function PlaygroundContent() { + const { theme } = useTheme(); const [isModalOpen, setIsModalOpen] = useState(false); + const [inputValue, setInputValue] = useState(""); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [emailError, setEmailError] = useState(""); + + const handleEmailChange = (e: React.ChangeEvent) => { + setEmail(e.target.value); + if (emailError) setEmailError(""); + }; + + const validateEmail = () => { + if (!email.includes("@")) { + setEmailError("Please enter a valid email address"); + } + }; + + return ( +
+ {/* Header */} +
+
+
+
+ + UI Component Library + + + Interactive playground for testing components + +
+
+
+
+ + {/* Main Content */} +
+
+ {/* Buttons Section */} +
+
+ + Buttons + + + Interactive buttons with multiple variants and states + +
+ +
+ {/* Variants */} +
+ + Variants + +
+
+
+ + {/* Sizes */} +
+ + Sizes + +
+
+
+ + {/* States */} +
+ + States + +
+
+
+ + {/* With Actions */} +
+ + Interactive Examples + +
+
+
+
+
+ + {/* Inputs Section */} +
+
+ + Input Fields + + + Form inputs with validation and different states + +
+ +
+ {/* Basic Input */} +
+ + Basic Input + + setInputValue(e.target.value)} + /> + {inputValue && ( + + Current value:{" "} + + {inputValue} + + + )} +
+ + {/* Email with Validation */} +
+ + With Validation + + +
+ + {/* Password */} +
+ + Password Input + + setPassword(e.target.value)} + /> +
+ + {/* Disabled State */} +
+ + Disabled State + + +
+ + {/* Error State */} +
+ + Error State + + +
+
+
+ + {/* Modal Section */} +
+
+ + Modals & Dialogs + + + Accessible modal dialogs with overlay + +
+ +
+
+
+ + {/* Typography Section */} +
+
+ + Typography Scale + + + All available typography variants + +
+ +
+ Display 1 + Display 2 + Heading 1 + Heading 2 + Heading 3 + Heading 4 + Heading 5 + Heading 6 + Subtitle 1 + Subtitle 2 + + Body 1 - Regular paragraph text + + + Body 2 - Smaller paragraph text + + Caption text +
+
+ + {/* Form Example */} +
+
+ + Complete Form Example + + + Combining multiple components together + +
+ +
{ + e.preventDefault(); + console.log("Form submitted", { email, password }); + setIsModalOpen(true); + }} + className="space-y-4" + > +
+ + +
+ + setPassword(e.target.value)} + required + /> +
+
+
+
+
+
+ + {/* Modal */} + setIsModalOpen(false)} + title="Confirmation Dialog" + > +
+ + This is an example modal dialog. It demonstrates how the modal + component works with the current theme. + +
+ + 💡 Pro tip: Modals automatically trap focus and + can be closed by clicking outside or pressing the Escape key. + +
+
+
+
+
+
+ ); +} + +function App() { const [isDarkMode, setIsDarkMode] = useState(false); const purpleTheme = { @@ -31,82 +502,18 @@ function App() { return ( -
-
-
-
-

- UI Library - {isDarkMode ? "Dark" : "Purple"} Theme{" "} - {isDarkMode ? "🌙" : "💜"} -

- -
- -
-

- Buttons -

-
- - - - - - -
-
- -
-

- Inputs -

-
- - -
-
- -
-

- Modals -

- -
-
+
+ {/* Theme Switcher - Fixed Position */} +
+
- setIsModalOpen(false)} - title="Example Modal" - > -
-

- This is an example modal with purple theme. Notice the accent - color! -

-
- - -
-
-
+
); diff --git a/src/theme/DefaultTheme.ts b/src/theme/DefaultTheme.ts index 9df846d..ef12778 100644 --- a/src/theme/DefaultTheme.ts +++ b/src/theme/DefaultTheme.ts @@ -82,15 +82,15 @@ export const defaultTheme: ThemeConfig = { 950: "#083344", }, semantic: { - background: "#ffffff", - foreground: "#0f172a", - muted: "#f1f5f9", - mutedForeground: "#64748b", - border: "#e2e8f0", - ring: "#2563eb", + background: "#ffff", // Soft cream (warm white) + foreground: "#1a1a1a", // Rich charcoal (softer than pure black) + muted: "#f5f3f0", // Warm light beige + mutedForeground: "#78716c", // Warm gray (stone) + border: "#e7e5e4", // Subtle warm border + ring: "#2563eb", // Keep blue for consistency }, }, - radius: "0.25rem", // 8px - single value for all components + radius: "0.25rem", fontFamily: 'Rubik, Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', }; diff --git a/src/utils/utils.ts b/src/utils/utils.ts new file mode 100644 index 0000000..b393ce8 --- /dev/null +++ b/src/utils/utils.ts @@ -0,0 +1,37 @@ +// src/utils/color.ts + +/** + * Add opacity/alpha to a hex color + * @param color - Hex color (e.g., "#2563eb") + * @param opacity - Opacity value (0-1) + * @returns Hex color with alpha (e.g., "#2563eb80") + */ +export function withOpacity(color: string, opacity: number): string { + // Remove # if present + const hex = color.replace("#", ""); + + // Clamp opacity between 0 and 1 + const alpha = Math.max(0, Math.min(1, opacity)); + + // Convert to 0-255 and then to hex + const alphaHex = Math.round(alpha * 255) + .toString(16) + .padStart(2, "0"); + + return `#${hex}${alphaHex}`; +} + +/** + * Convert hex to rgba string + * @param color - Hex color + * @param opacity - Opacity (0-1) + * @returns rgba string (e.g., "rgba(37, 99, 235, 0.5)") + */ +export function hexToRgba(color: string, opacity: number): string { + const hex = color.replace("#", ""); + const r = parseInt(hex.substring(0, 2), 16); + const g = parseInt(hex.substring(2, 4), 16); + const b = parseInt(hex.substring(4, 6), 16); + + return `rgba(${r}, ${g}, ${b}, ${opacity})`; +} From 19df2f867411c2c7a74856b23a7a5b8fda910b5a Mon Sep 17 00:00:00 2001 From: ABILASH <114130954+Abilash-19@users.noreply.github.com> Date: Mon, 16 Feb 2026 23:47:13 +0530 Subject: [PATCH 03/14] fix: util import --- src/components/Button/Button.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx index 0ae8788..25478e4 100644 --- a/src/components/Button/Button.tsx +++ b/src/components/Button/Button.tsx @@ -4,7 +4,7 @@ import { cn } from "@/utils"; import { useTheme } from "@/theme"; import { type ButtonProps, type ButtonSize } from "./Button.types"; import { Spinner } from "@/assets/Icons/Spinner"; -import { withOpacity } from "@/utils/Utils"; +import { withOpacity } from "@/utils/utils"; const iconSizes: Record = { sm: "h-3.5 w-3.5", From 7d43b55631381ac13e0a1817001e2236b779e6f4 Mon Sep 17 00:00:00 2001 From: ABILASH <114130954+Abilash-19@users.noreply.github.com> Date: Mon, 16 Feb 2026 23:58:39 +0530 Subject: [PATCH 04/14] fix: removed dependbot --- .github/dependabot.yml | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 475ff71..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,16 +0,0 @@ -version: 2 -updates: - - package-ecosystem: 'npm' - directory: '/' - schedule: - interval: 'weekly' - day: 'monday' - commit-message: - prefix: 'chore' - include: 'scope' - open-pull-requests-limit: 10 - reviewers: - - 'your-username' - labels: - - 'dependencies' - - 'automated' From 634b877083d3a7d7e60aacf309235727b0e342f1 Mon Sep 17 00:00:00 2001 From: ABILASH <114130954+Abilash-19@users.noreply.github.com> Date: Wed, 18 Feb 2026 20:23:52 +0530 Subject: [PATCH 05/14] refactor: chnage the button text to children and removed icon props --- src/App.tsx | 44 ++++++++++++------------ src/assets/Icons/Dummy.tsx | 19 ++++++++++ src/components/Button/Button.stories.tsx | 27 +++++++++------ src/components/Button/Button.tsx | 37 ++++++-------------- src/components/Button/Button.types.tsx | 2 +- 5 files changed, 69 insertions(+), 60 deletions(-) create mode 100644 src/assets/Icons/Dummy.tsx diff --git a/src/App.tsx b/src/App.tsx index eb5b4b7..d516613 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,7 +7,7 @@ import { ThemeProvider, darkTheme } from "@/theme"; import { useTheme } from "@/theme"; import "@/theme/index.css"; -// Inner component that can access theme context +// Inner component that can access theme conchildren function PlaygroundContent() { const { theme } = useTheme(); const [isModalOpen, setIsModalOpen] = useState(false); @@ -96,13 +96,13 @@ function PlaygroundContent() {
@@ -117,9 +117,9 @@ function PlaygroundContent() { Sizes
-
@@ -134,8 +134,8 @@ function PlaygroundContent() { States
-
@@ -153,12 +153,12 @@ function PlaygroundContent() {
@@ -356,12 +356,12 @@ function PlaygroundContent() { Subtitle 1 Subtitle 2 - Body 1 - Regular paragraph text + Body 1 - Regular paragraph children - Body 2 - Smaller paragraph text + Body 2 - Smaller paragraph children - Caption text + Caption children @@ -414,11 +414,11 @@ function PlaygroundContent() { required />
-
diff --git a/src/assets/Icons/Dummy.tsx b/src/assets/Icons/Dummy.tsx new file mode 100644 index 0000000..35fb645 --- /dev/null +++ b/src/assets/Icons/Dummy.tsx @@ -0,0 +1,19 @@ +export const DummyIcon = () => { + return ( + + + + + + ); +}; diff --git a/src/components/Button/Button.stories.tsx b/src/components/Button/Button.stories.tsx index 6310817..6f3cebc 100644 --- a/src/components/Button/Button.stories.tsx +++ b/src/components/Button/Button.stories.tsx @@ -1,5 +1,6 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; import { Button } from "./Button"; +import { DummyIcon } from "../../assets/Icons/Dummy"; const meta: Meta = { title: "Components/Button", @@ -31,73 +32,79 @@ type Story = StoryObj; export const Primary: Story = { args: { - text: "Primary Button", + children: "Primary Button", variant: "primary", }, }; export const Secondary: Story = { args: { - text: "Secondary Button", + children: "Secondary Button", variant: "secondary", }, }; export const Outline: Story = { args: { - text: "Outline Button", + children: "Outline Button", variant: "outline", }, }; export const Ghost: Story = { args: { - text: "Ghost Button", + children: "Ghost Button", variant: "ghost", }, }; export const Danger: Story = { args: { - text: "Danger Button", + children: "Danger Button", variant: "danger", }, }; export const Small: Story = { args: { - text: "Small Button", + children: "Small Button", size: "sm", }, }; export const Medium: Story = { args: { - text: "Medium Button", + children: "Medium Button", size: "md", }, }; export const Large: Story = { args: { - text: "Large Button", + children: "Large Button", size: "lg", }, }; export const Loading: Story = { args: { - text: "Loading Button", + children: "Loading Button", isLoading: true, }, }; export const Disabled: Story = { args: { - text: "Disabled Button", + children: "Disabled Button", disabled: true, }, }; +export const IconOnly: Story = { + args: { + children: , + size: "md", + }, +}; export const AllVariants: Story = { render: () => ( diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx index 25478e4..054559d 100644 --- a/src/components/Button/Button.tsx +++ b/src/components/Button/Button.tsx @@ -20,7 +20,7 @@ const Button = forwardRef( size = "md", isLoading = false, disabled, - text, + children, prefixIcon, suffixIcon, type = "button", @@ -51,14 +51,14 @@ const Button = forwardRef( }; const sizes = { - sm: "h-8 px-3 text-sm min-w-[4rem]", - md: "h-10 px-4 text-sm min-w-[5rem]", - lg: "h-12 px-6 text-base min-w-[6rem]", + sm: "h-8 px-3 text-sm", + md: "h-10 px-4 text-sm", + lg: "h-12 px-6 text-base", }; // Adjust padding for icon-only buttons const iconOnlyStyles = - !text && (prefixIcon || suffixIcon) + !children && (prefixIcon || suffixIcon) ? { sm: "h-8 w-8 px-0 min-w-0", md: "h-10 w-10 px-0 min-w-0", @@ -126,8 +126,9 @@ const Button = forwardRef( } // Determine if button needs an accessible label - const isIconOnly = !text && (prefixIcon || suffixIcon); - const needsAriaLabel = (isLoading && !text) || (isIconOnly && !ariaLabel); + const isIconOnly = !children && (prefixIcon || suffixIcon); + const needsAriaLabel = + (isLoading && !children) || (isIconOnly && !ariaLabel); const computedAriaLabel = ariaLabel || (needsAriaLabel ? "Button" : undefined); @@ -156,28 +157,10 @@ const Button = forwardRef( {isLoading ? ( <> - {text && {text}} + {children && {children}} ) : ( - <> - {prefixIcon && ( - - )} - {text && {text}} - {suffixIcon && ( - - )} - + children )} ); diff --git a/src/components/Button/Button.types.tsx b/src/components/Button/Button.types.tsx index 58efa8c..8f1d33d 100644 --- a/src/components/Button/Button.types.tsx +++ b/src/components/Button/Button.types.tsx @@ -10,7 +10,7 @@ export type ButtonSize = "sm" | "md" | "lg"; export interface ButtonProps extends ButtonHTMLAttributes { // Content - text?: string; + children?: ReactNode; // Visual variants variant?: ButtonVariant; From 80f7741238baad962dde14ab4d37e65703bb55d4 Mon Sep 17 00:00:00 2001 From: ABILASH <114130954+Abilash-19@users.noreply.github.com> Date: Thu, 19 Feb 2026 00:08:58 +0530 Subject: [PATCH 06/14] refactor(theme): migrate to three-layer semantic token architecture --- src/App.tsx | 280 ++++++++++-------- src/assets/Icons/CloseIcon.tsx | 4 +- src/components/Button/Button.tsx | 81 +++-- src/components/Input/Input.tsx | 104 ++++--- src/components/Modal/Modal.tsx | 50 +--- src/components/Typography/Typography.tsx | 8 +- .../Typography/Typography.types.tsx | 4 + src/theme/DarkTheme.ts | 89 +++++- tailwind.config.js | 268 +---------------- 9 files changed, 364 insertions(+), 524 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index d516613..8ac7048 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,11 +3,21 @@ import { Button } from "@/components/Button"; import { Input } from "@/components/Input"; import { Modal } from "@/components/Modal"; import { Typography } from "@/components/Typography"; -import { ThemeProvider, darkTheme } from "@/theme"; +import { ThemeProvider } from "@/theme"; import { useTheme } from "@/theme"; import "@/theme/index.css"; -// Inner component that can access theme conchildren +function ThemeToggle() { + const { toggleColorMode, theme } = useTheme(); + return ( +
+ +
+ ); +} + function PlaygroundContent() { const { theme } = useTheme(); const [isModalOpen, setIsModalOpen] = useState(false); @@ -27,20 +37,25 @@ function PlaygroundContent() { } }; + const sectionStyles = { + backgroundColor: theme.tokens.surface, + "--ring-color": theme.tokens.border, + } as React.CSSProperties; + return (
{/* Header */}
@@ -51,7 +66,11 @@ function PlaygroundContent() { Interactive playground for testing components @@ -66,10 +85,8 @@ function PlaygroundContent() {
{/* Buttons Section */}
@@ -77,92 +94,109 @@ function PlaygroundContent() { Interactive buttons with multiple variants and states
- {/* Variants */}
Variants
- + + + +
- {/* Sizes */}
Sizes
- + +
- {/* States */}
States
- +
- {/* With Actions */}
Interactive Examples
@@ -170,10 +204,8 @@ function PlaygroundContent() { {/* Inputs Section */}
@@ -181,20 +213,27 @@ function PlaygroundContent() { Form inputs with validation and different states
- {/* Basic Input */}
Basic Input @@ -208,12 +247,16 @@ function PlaygroundContent() { Current value:{" "} {inputValue} @@ -221,13 +264,16 @@ function PlaygroundContent() { )}
- {/* Email with Validation */}
With Validation @@ -242,13 +288,16 @@ function PlaygroundContent() { />
- {/* Password */}
Password Input @@ -261,13 +310,16 @@ function PlaygroundContent() { />
- {/* Disabled State */}
Disabled State @@ -279,13 +331,16 @@ function PlaygroundContent() { />
- {/* Error State */}
Error State @@ -300,10 +355,8 @@ function PlaygroundContent() { {/* Modal Section */}
@@ -311,26 +364,26 @@ function PlaygroundContent() { Accessible modal dialogs with overlay
-
-
{/* Typography Section */}
@@ -338,12 +391,15 @@ function PlaygroundContent() { All available typography variants
-
Display 1 Display 2 @@ -356,21 +412,19 @@ function PlaygroundContent() { Subtitle 1 Subtitle 2 - Body 1 - Regular paragraph children + Body 1 - Regular paragraph text - Body 2 - Smaller paragraph children + Body 2 - Smaller paragraph text - Caption children + Caption text
{/* Form Example */}
@@ -378,12 +432,15 @@ function PlaygroundContent() { Combining multiple components together
-
{ e.preventDefault(); @@ -414,17 +471,18 @@ function PlaygroundContent() { required />
-
@@ -445,31 +503,34 @@ function PlaygroundContent() {
💡 Pro tip: Modals automatically trap focus and can be closed by clicking outside or pressing the Escape key.
+
@@ -478,41 +539,10 @@ function PlaygroundContent() { } function App() { - const [isDarkMode, setIsDarkMode] = useState(false); - - const purpleTheme = { - colors: { - primary: { - 50: "#faf5ff", - 100: "#f3e8ff", - 200: "#e9d5ff", - 300: "#d8b4fe", - 400: "#c084fc", - 500: "#a855f7", - 600: "#9333ea", - 700: "#7e22ce", - 800: "#6b21a8", - 900: "#581c87", - 950: "#3b0764", - }, - }, - }; - - const currentTheme = isDarkMode ? darkTheme : purpleTheme; - return ( - +
- {/* Theme Switcher - Fixed Position */} -
-
- +
diff --git a/src/assets/Icons/CloseIcon.tsx b/src/assets/Icons/CloseIcon.tsx index 72eade9..fe30c24 100644 --- a/src/assets/Icons/CloseIcon.tsx +++ b/src/assets/Icons/CloseIcon.tsx @@ -14,7 +14,9 @@ export const CloseIcon = () => { strokeLinecap="round" strokeLinejoin="round" className="h-5 w-5" - style={{ color: theme.colors.semantic.foreground }} + style={ + { "--typography-color": theme.tokens.foreground } as React.CSSProperties + } > diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx index 054559d..8dc2b5c 100644 --- a/src/components/Button/Button.tsx +++ b/src/components/Button/Button.tsx @@ -1,10 +1,8 @@ -// src/components/Button/Button.tsx import { forwardRef } from "react"; import { cn } from "@/utils"; import { useTheme } from "@/theme"; import { type ButtonProps, type ButtonSize } from "./Button.types"; import { Spinner } from "@/assets/Icons/Spinner"; -import { withOpacity } from "@/utils/utils"; const iconSizes: Record = { sm: "h-3.5 w-3.5", @@ -56,7 +54,6 @@ const Button = forwardRef( lg: "h-12 px-6 text-base", }; - // Adjust padding for icon-only buttons const iconOnlyStyles = !children && (prefixIcon || suffixIcon) ? { @@ -66,66 +63,56 @@ const Button = forwardRef( } : null; - // Theme-derived style variables const themeStyles: Record = { - "--radius": theme.radius, - "--btn-bg": theme.colors.primary[600], - "--btn-hover": theme.colors.primary[700], - "--btn-active": theme.colors.primary[800], - "--btn-ring": theme.colors.primary[500], - "--btn-text": theme.colors.semantic.foreground, - "--btn-border": theme.colors.primary[600], + "--radius": theme.shape.radiusMd, + "--btn-bg": "transparent", + "--btn-hover": "transparent", + "--btn-active": "transparent", + "--btn-text": "#ffffff", + "--btn-border": "transparent", + "--btn-ring": theme.tokens.ring, }; - // Apply variant-specific colors switch (variant) { case "primary": - themeStyles["--btn-bg"] = theme.colors.primary[600]; - themeStyles["--btn-hover"] = theme.colors.primary[700]; - themeStyles["--btn-active"] = theme.colors.primary[800]; - themeStyles["--btn-ring"] = theme.colors.primary[500]; + themeStyles["--btn-bg"] = theme.palette.primary[600]; + themeStyles["--btn-hover"] = theme.palette.primary[700]; + themeStyles["--btn-active"] = theme.palette.primary[800]; + themeStyles["--btn-ring"] = theme.palette.primary[500]; break; + case "secondary": - themeStyles["--btn-bg"] = theme.colors.secondary[100]; - themeStyles["--btn-hover"] = theme.colors.secondary[200]; - themeStyles["--btn-active"] = theme.colors.secondary[300]; - themeStyles["--btn-text"] = theme.colors.secondary[900]; - themeStyles["--btn-ring"] = theme.colors.secondary[500]; + themeStyles["--btn-bg"] = theme.tokens.surface; + themeStyles["--btn-hover"] = theme.tokens.surfaceHover; + themeStyles["--btn-active"] = theme.tokens.elevated; + themeStyles["--btn-text"] = theme.tokens.foreground; + themeStyles["--btn-ring"] = theme.tokens.ring; break; + case "outline": - themeStyles["--btn-border"] = theme.colors.primary[600]; - themeStyles["--btn-text"] = theme.colors.primary[600]; - themeStyles["--btn-hover"] = withOpacity( - theme.colors.primary[600], - 0.1, - ); - themeStyles["--btn-active"] = withOpacity( - theme.colors.primary[600], - 0.2, - ); - themeStyles["--btn-ring"] = theme.colors.primary[500]; + themeStyles["--btn-border"] = theme.tokens.border; + themeStyles["--btn-text"] = theme.tokens.accent; + themeStyles["--btn-hover"] = theme.tokens.surfaceHover; + themeStyles["--btn-active"] = theme.tokens.surfaceHover; + themeStyles["--btn-ring"] = theme.tokens.ring; break; + case "ghost": - themeStyles["--btn-text"] = theme.colors.semantic.foreground; - themeStyles["--btn-hover"] = withOpacity( - theme.colors.semantic.foreground, - 0.08, - ); - themeStyles["--btn-active"] = withOpacity( - theme.colors.semantic.foreground, - 0.16, - ); - themeStyles["--btn-ring"] = theme.colors.primary[500]; + themeStyles["--btn-text"] = theme.tokens.foreground; + themeStyles["--btn-hover"] = theme.tokens.surface; + themeStyles["--btn-active"] = theme.tokens.surfaceHover; + themeStyles["--btn-ring"] = theme.tokens.ring; break; + case "danger": - themeStyles["--btn-bg"] = theme.colors.error[600]; - themeStyles["--btn-hover"] = theme.colors.error[700]; - themeStyles["--btn-active"] = theme.colors.error[800]; - themeStyles["--btn-ring"] = theme.colors.error[500]; + themeStyles["--btn-bg"] = theme.palette.error[600]; + themeStyles["--btn-hover"] = theme.palette.error[700]; + themeStyles["--btn-active"] = theme.palette.error[800]; + themeStyles["--btn-text"] = "#ffffff"; + themeStyles["--btn-ring"] = theme.palette.error[500]; break; } - // Determine if button needs an accessible label const isIconOnly = !children && (prefixIcon || suffixIcon); const needsAriaLabel = (isLoading && !children) || (isIconOnly && !ariaLabel); diff --git a/src/components/Input/Input.tsx b/src/components/Input/Input.tsx index 1fe96cd..53c1207 100644 --- a/src/components/Input/Input.tsx +++ b/src/components/Input/Input.tsx @@ -1,4 +1,4 @@ -import { forwardRef, useId, type InputHTMLAttributes } from "react"; +import { forwardRef, useId, type InputHTMLAttributes, useMemo } from "react"; import { cn } from "@/utils"; import { useTheme } from "@/theme"; @@ -7,6 +7,8 @@ export interface InputProps extends InputHTMLAttributes { error?: string; helperText?: string; inputSize?: "sm" | "md" | "lg"; + leftIcon?: React.ReactNode; + rightIcon?: React.ReactNode; } const Input = forwardRef( @@ -19,6 +21,9 @@ const Input = forwardRef( inputSize = "md", id, disabled, + required, + leftIcon, + rightIcon, ...props }, ref, @@ -26,69 +31,88 @@ const Input = forwardRef( const { theme } = useTheme(); const generatedId = useId(); const inputId = id || generatedId; + const errorId = error ? `${inputId}-error` : undefined; const helperId = helperText ? `${inputId}-helper` : undefined; - const baseStyles = - "flex w-full border bg-white px-3 py-2 text-sm ring-offset-background placeholder:text-secondary-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-secondary-800 dark:text-secondary-100 dark:placeholder:text-secondary-500"; + const describedBy = useMemo(() => { + return [errorId, helperId].filter(Boolean).join(" ") || undefined; + }, [errorId, helperId]); const sizes = { - sm: "h-8 text-xs", - md: "h-10", + sm: "h-9 text-sm", + md: "h-11 text-sm", lg: "h-12 text-base", }; - const themeStyles: Record = { - "--radius": theme.radius, - "--input-border": error - ? theme.colors.error[500] - : theme.colors.secondary[300], - "--input-ring": error - ? theme.colors.error[500] - : theme.colors.primary[500], - "--label-text": theme.colors.secondary[700], - "--error-text": theme.colors.error[600], - "--helper-text": theme.colors.secondary[500], - }; + const themeStyles = { + "--radius": theme.shape.radiusMd, + "--border": error ? theme.palette.error[500] : theme.tokens.border, + "--ring": error ? theme.palette.error[500] : theme.tokens.ring, + "--bg": theme.tokens.input, + "--bg-hover": theme.tokens.inputHover, + "--text": theme.tokens.foreground, + "--placeholder": theme.tokens.foregroundSubtle, + "--label": theme.tokens.foreground, + "--error": theme.palette.error[500], + "--helper": theme.tokens.foregroundMuted, + } as React.CSSProperties; return ( -
+
{label && ( )} - + > + {leftIcon && ( + {leftIcon} + )} + + + + {rightIcon && ( + {rightIcon} + )} +
+ {error && ( - )} + {helperText && !error && ( -

+

{helperText}

)} diff --git a/src/components/Modal/Modal.tsx b/src/components/Modal/Modal.tsx index be396da..316fe47 100644 --- a/src/components/Modal/Modal.tsx +++ b/src/components/Modal/Modal.tsx @@ -1,4 +1,3 @@ -// src/components/Modal/Modal.tsx import { useEffect, useRef, type FC, type MouseEvent } from "react"; import { createPortal } from "react-dom"; import { cn } from "@/utils"; @@ -7,18 +6,6 @@ import type { ModalProps } from "./Modal.types"; import { MODAL_SIZES } from "./Modal.types"; import { CloseIcon } from "@/assets/Icons/CloseIcon"; -/** - * Modal component with backdrop, keyboard controls, and focus management - * - * @example - * ```tsx - * const [isOpen, setIsOpen] = useState(false); - * - * setIsOpen(false)} title="My Modal"> - *

Modal content here

- *
- * ``` - */ export const Modal: FC = ({ id, isOpen, @@ -34,7 +21,6 @@ export const Modal: FC = ({ const { theme } = useTheme(); const modalRef = useRef(null); - // Handle Escape key and body scroll lock useEffect(() => { const handleEscape = (event: KeyboardEvent) => { if (event.key === "Escape" && isOpen && !preventEscapeClose) { @@ -45,8 +31,6 @@ export const Modal: FC = ({ if (isOpen) { document.addEventListener("keydown", handleEscape); document.body.style.overflow = "hidden"; - - // Focus the modal when it opens modalRef.current?.focus(); } @@ -56,22 +40,22 @@ export const Modal: FC = ({ }; }, [isOpen, onClose, preventEscapeClose]); - // Handle backdrop click const handleBackdropClick = (event: MouseEvent) => { if (event.target === event.currentTarget && !preventBackdropClose) { onClose(); } }; - // Theme CSS variables - const themeStyles: Record = { - "--radius": theme.radius, - "--modal-bg": theme.colors.semantic.background, - "--modal-text": theme.colors.semantic.foreground, - "--modal-title": theme.colors.semantic.foreground, - "--modal-ring": theme.colors.primary[500], - "--modal-close-hover": theme.colors.primary[100], - }; + // FIXED + const themeStyles = { + "--radius": theme.shape.radiusMd, + "--modal-bg": theme.tokens.elevated, + "--modal-border": theme.tokens.border, + "--modal-text": theme.tokens.foreground, + "--modal-title": theme.tokens.foreground, + "--modal-ring": theme.tokens.ring, + "--modal-close-hover": theme.tokens.surfaceHover, + } as React.CSSProperties; if (!isOpen) return null; @@ -80,7 +64,6 @@ export const Modal: FC = ({ className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm" onClick={handleBackdropClick} style={themeStyles as React.CSSProperties} - aria-hidden={!isOpen} >
= ({ aria-modal="true" aria-labelledby={title ? "modal-title" : undefined} className={cn( - "relative w-full rounded-[var(--radius)] bg-[var(--modal-bg)] p-6 shadow-xl dark:bg-secondary-800 modal-content-scroll ", + "relative w-full rounded-[var(--radius)] bg-[var(--modal-bg)] border border-[var(--modal-border)] p-6 shadow-xl modal-content-scroll", MODAL_SIZES[size], "animate-slide-in-up animate-zoom-in-95 duration-200", className, )} tabIndex={-1} > - {/* Close Button */} {showCloseButton && ( )} - {/* Title */} {title && ( )} - {/* Content */} -
- {children} -
+
{children}
, document.body, diff --git a/src/components/Typography/Typography.tsx b/src/components/Typography/Typography.tsx index 8671a01..3d3aaca 100644 --- a/src/components/Typography/Typography.tsx +++ b/src/components/Typography/Typography.tsx @@ -7,6 +7,7 @@ import type { TypographyWeight, } from "./Typography.types"; import { VARIANT_ELEMENT_MAP, VARIANT_WEIGHT_MAP } from "./Typography.types"; +import { useTheme } from "@/theme"; /** * Typography component for consistent text styling across the application @@ -29,12 +30,15 @@ export const Typography: React.FC = ({ onClick, ...rest }) => { + const { theme } = useTheme(); // Determine the HTML element to render const Element = as || VARIANT_ELEMENT_MAP[variant] || "p"; // Determine font weight const fontWeight = weight || VARIANT_WEIGHT_MAP[variant]; + const color = theme.tokens.foreground; + // Build class names const classes = cn( // Base styles @@ -53,8 +57,8 @@ export const Typography: React.FC = ({ return ( diff --git a/src/components/Typography/Typography.types.tsx b/src/components/Typography/Typography.types.tsx index 54fa61a..a18a5b5 100644 --- a/src/components/Typography/Typography.types.tsx +++ b/src/components/Typography/Typography.types.tsx @@ -72,6 +72,10 @@ export interface TypographyProps { * onClick handler */ onClick?: React.MouseEventHandler; + /** + * onClick handler + */ + htmlFor?: string; } /** diff --git a/src/theme/DarkTheme.ts b/src/theme/DarkTheme.ts index eecabe1..b225e54 100644 --- a/src/theme/DarkTheme.ts +++ b/src/theme/DarkTheme.ts @@ -1,17 +1,76 @@ -import type { ThemeConfig } from './Types'; -import { defaultTheme } from './DefaultTheme'; +// src/theme/darkTheme.ts +// Dark theme is a clean override — it does NOT spread defaultTheme's tokens. +// Instead it defines its own full token set referencing the same palette. +// This avoids stale light-mode values silently leaking into dark mode. + +import type { ThemeConfig } from "./Types"; +import { defaultTheme } from "./DefaultTheme"; + +const { palette, neutral } = defaultTheme; + +// Dark-mode neutral overrides — same hue family, just inverted lightness +const darkNeutral: ThemeConfig["neutral"] = { + 50: "#0b0f14", + 100: "#0f1620", + 200: "#141e2a", + 300: "#1a2535", + 400: "#2d3d52", + 500: "#4a6080", + 600: "#6b8aad", + 700: "#94aec8", + 800: "#c5d5e8", + 900: "#f0f5fb", +}; export const darkTheme: ThemeConfig = { - ...defaultTheme, - colors: { - ...defaultTheme.colors, - semantic: { - background: '#020617', - foreground: '#f8fafc', - muted: '#1e293b', - mutedForeground: '#94a3b8', - border: '#1e293b', - ring: '#3b82f6', - }, - }, -}; \ No newline at end of file + name: "dark", + colorMode: "dark", + palette, // palette scales are the same — only tokens change + neutral, // keep original neutral for reference if needed + + tokens: { + // Layout + background: darkNeutral[50], + surface: darkNeutral[100], + surfaceHover: darkNeutral[200], + elevated: darkNeutral[200], + + // Text + foreground: darkNeutral[900], + foregroundMuted: darkNeutral[700], + foregroundSubtle: darkNeutral[600], + foregroundInverse: "#0b0f14", + + // Borders + border: darkNeutral[300], + borderSubtle: darkNeutral[200], + borderStrong: darkNeutral[400], + + // Inputs + input: darkNeutral[200], + inputHover: darkNeutral[300], + inputFocus: darkNeutral[100], + + // Focus + ring: palette.primary[400], + ringOffset: darkNeutral[100], + + // Accent — lighter shade works better on dark backgrounds + accent: palette.primary[500], + accentForeground: "#ffffff", + accentHover: palette.primary[400], + + // Status — darker surfaces, lighter text for dark mode + successSurface: palette.success[900], + successForeground: palette.success[300], + warningSurface: palette.warning[900], + warningForeground: palette.warning[300], + errorSurface: palette.error[900], + errorForeground: palette.error[300], + infoSurface: palette.info[900], + infoForeground: palette.info[300], + }, + + shape: defaultTheme.shape, + typography: defaultTheme.typography, +}; diff --git a/tailwind.config.js b/tailwind.config.js index 1be37a9..5bd1efd 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -5,271 +5,23 @@ export default { theme: { extend: { colors: { - primary: { - 50: 'var(--color-primary-50)', - 100: 'var(--color-primary-100)', - 200: 'var(--color-primary-200)', - 300: 'var(--color-primary-300)', - 400: 'var(--color-primary-400)', - 500: 'var(--color-primary-500)', - 600: 'var(--color-primary-600)', - 700: 'var(--color-primary-700)', - 800: 'var(--color-primary-800)', - 900: 'var(--color-primary-900)', - 950: 'var(--color-primary-950)', - }, - secondary: { - 50: 'var(--color-secondary-50)', - 100: 'var(--color-secondary-100)', - 200: 'var(--color-secondary-200)', - 300: 'var(--color-secondary-300)', - 400: 'var(--color-secondary-400)', - 500: 'var(--color-secondary-500)', - 600: 'var(--color-secondary-600)', - 700: 'var(--color-secondary-700)', - 800: 'var(--color-secondary-800)', - 900: 'var(--color-secondary-900)', - 950: 'var(--color-secondary-950)', - }, - // Semantic colors - success: { - 50: 'var(--color-success-50)', - 100: 'var(--color-success-100)', - 200: 'var(--color-success-200)', - 300: 'var(--color-success-300)', - 400: 'var(--color-success-400)', - 500: 'var(--color-success-500)', - 600: 'var(--color-success-600)', - 700: 'var(--color-success-700)', - 800: 'var(--color-success-800)', - 900: 'var(--color-success-900)', - }, - warning: { - 50: 'var(--color-warning-50)', - 100: 'var(--color-warning-100)', - 200: 'var(--color-warning-200)', - 300: 'var(--color-warning-300)', - 400: 'var(--color-warning-400)', - 500: 'var(--color-warning-500)', - 600: 'var(--color-warning-600)', - 700: 'var(--color-warning-700)', - 800: 'var(--color-warning-800)', - 900: 'var(--color-warning-900)', - }, - error: { - 50: 'var(--color-error-50)', - 100: 'var(--color-error-100)', - 200: 'var(--color-error-200)', - 300: 'var(--color-error-300)', - 400: 'var(--color-error-400)', - 500: 'var(--color-error-500)', - 600: 'var(--color-error-600)', - 700: 'var(--color-error-700)', - 800: 'var(--color-error-800)', - 900: 'var(--color-error-900)', - }, - info: { - 50: 'var(--color-info-50)', - 100: 'var(--color-info-100)', - 200: 'var(--color-info-200)', - 300: 'var(--color-info-300)', - 400: 'var(--color-info-400)', - 500: 'var(--color-info-500)', - 600: 'var(--color-info-600)', - 700: 'var(--color-info-700)', - 800: 'var(--color-info-800)', - 900: 'var(--color-info-900)', - }, - // UI colors background: 'var(--color-background)', + surface: 'var(--color-surface)', foreground: 'var(--color-foreground)', - card: { - DEFAULT: 'var(--color-card)', - foreground: 'var(--color-card-foreground)', - }, - popover: { - DEFAULT: 'var(--color-popover)', - foreground: 'var(--color-popover-foreground)', - }, - muted: { - DEFAULT: 'var(--color-muted)', - foreground: 'var(--color-muted-foreground)', - }, - accent: { - DEFAULT: 'var(--color-accent)', - foreground: 'var(--color-accent-foreground)', - }, - destructive: { - DEFAULT: 'var(--color-destructive)', - foreground: 'var(--color-destructive-foreground)', - }, + 'foreground-muted': 'var(--color-foreground-muted)', border: 'var(--color-border)', - input: 'var(--color-input)', + accent: 'var(--color-accent)', ring: 'var(--color-ring)', + // ...etc }, - // Typography - fontFamily: { - sans: [ - 'Rubik', - 'Inter', - 'ui-sans-serif', - 'system-ui', - '-apple-system', - 'BlinkMacSystemFont', - 'Segoe UI', - 'Roboto', - 'Helvetica Neue', - 'Arial', - 'sans-serif', - ], - mono: [ - 'JetBrains Mono', - 'Fira Code', - 'Consolas', - 'Monaco', - 'Courier New', - 'monospace', - ], - display: [ - 'Rubik', - 'Inter', - 'Lexend', - 'ui-sans-serif', - 'system-ui', - 'sans-serif', - ], - }, - fontSize: { - xs: ['0.75rem', { lineHeight: '1rem', letterSpacing: '0.01em' }], - sm: ['0.875rem', { lineHeight: '1.25rem', letterSpacing: '0.01em' }], - base: ['1rem', { lineHeight: '1.5rem', letterSpacing: '0' }], - lg: ['1.125rem', { lineHeight: '1.75rem', letterSpacing: '-0.01em' }], - xl: ['1.25rem', { lineHeight: '1.75rem', letterSpacing: '-0.01em' }], - '2xl': ['1.5rem', { lineHeight: '2rem', letterSpacing: '-0.02em' }], - '3xl': ['1.875rem', { lineHeight: '2.25rem', letterSpacing: '-0.02em' }], - '4xl': ['2.25rem', { lineHeight: '2.5rem', letterSpacing: '-0.02em' }], - '5xl': ['3rem', { lineHeight: '1', letterSpacing: '-0.02em' }], - '6xl': ['3.75rem', { lineHeight: '1', letterSpacing: '-0.02em' }], - '7xl': ['4.5rem', { lineHeight: '1', letterSpacing: '-0.02em' }], - '8xl': ['6rem', { lineHeight: '1', letterSpacing: '-0.02em' }], - '9xl': ['8rem', { lineHeight: '1', letterSpacing: '-0.02em' }], - }, - fontWeight: { - thin: '100', - extralight: '200', - light: '300', - normal: '400', - medium: '500', - semibold: '600', - bold: '700', - extrabold: '800', - black: '900', - }, - // Spacing (8px grid system) - spacing: { - px: '1px', - 0: '0', - 0.5: '0.125rem', // 2px - 1: '0.25rem', // 4px - 1.5: '0.375rem', // 6px - 2: '0.5rem', // 8px - 2.5: '0.625rem', // 10px - 3: '0.75rem', // 12px - 3.5: '0.875rem', // 14px - 4: '1rem', // 16px - 5: '1.25rem', // 20px - 6: '1.5rem', // 24px - 7: '1.75rem', // 28px - 8: '2rem', // 32px - 9: '2.25rem', // 36px - 10: '2.5rem', // 40px - 11: '2.75rem', // 44px - 12: '3rem', // 48px - 14: '3.5rem', // 56px - 16: '4rem', // 64px - 20: '5rem', // 80px - 24: '6rem', // 96px - 28: '7rem', // 112px - 32: '8rem', // 128px - 36: '9rem', // 144px - 40: '10rem', // 160px - 44: '11rem', // 176px - 48: '12rem', // 192px - 52: '13rem', // 208px - 56: '14rem', // 224px - 60: '15rem', // 240px - 64: '16rem', // 256px - 72: '18rem', // 288px - 80: '20rem', // 320px - 96: '24rem', // 384px - }, - // Border radius borderRadius: { - none: '0', - sm: '0.25rem', // 4px - DEFAULT: '0.375rem', // 6px - md: '0.5rem', // 8px - lg: '0.75rem', // 12px - xl: '1rem', // 16px - '2xl': '1.5rem', // 24px - '3xl': '2rem', // 32px - full: '9999px', + sm: 'var(--radius-sm)', + md: 'var(--radius-md)', + lg: 'var(--radius-lg)', }, - // Box shadows - boxShadow: { - sm: '0 1px 2px 0 rgb(0 0 0 / 0.05)', - DEFAULT: '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)', - md: '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)', - lg: '0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)', - xl: '0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)', - '2xl': '0 25px 50px -12px rgb(0 0 0 / 0.25)', - inner: 'inset 0 2px 4px 0 rgb(0 0 0 / 0.05)', - none: 'none', - }, - // Animation - keyframes: { - 'fade-in': { - '0%': { opacity: '0' }, - '100%': { opacity: '1' }, - }, - 'fade-out': { - '0%': { opacity: '1' }, - '100%': { opacity: '0' }, - }, - 'slide-in-up': { - '0%': { transform: 'translateY(10px)', opacity: '0' }, - '100%': { transform: 'translateY(0)', opacity: '1' }, - }, - 'slide-in-down': { - '0%': { transform: 'translateY(-10px)', opacity: '0' }, - '100%': { transform: 'translateY(0)', opacity: '1' }, - }, - 'slide-in-left': { - '0%': { transform: 'translateX(-10px)', opacity: '0' }, - '100%': { transform: 'translateX(0)', opacity: '1' }, - }, - 'slide-in-right': { - '0%': { transform: 'translateX(10px)', opacity: '0' }, - '100%': { transform: 'translateX(0)', opacity: '1' }, - }, - 'scale-in': { - '0%': { transform: 'scale(0.95)', opacity: '0' }, - '100%': { transform: 'scale(1)', opacity: '1' }, - }, - }, - animation: { - 'fade-in': 'fade-in 0.2s ease-out', - 'fade-out': 'fade-out 0.2s ease-out', - 'slide-in-up': 'slide-in-up 0.3s ease-out', - 'slide-in-down': 'slide-in-down 0.3s ease-out', - 'slide-in-left': 'slide-in-left 0.3s ease-out', - 'slide-in-right': 'slide-in-right 0.3s ease-out', - 'scale-in': 'scale-in 0.2s ease-out', - }, - // Transitions - transitionTimingFunction: { - 'bounce-in': 'cubic-bezier(0.68, -0.55, 0.265, 1.55)', - 'smooth': 'cubic-bezier(0.4, 0, 0.2, 1)', + fontFamily: { + sans: 'var(--font-sans)', + mono: 'var(--font-mono)', }, }, }, From e1bc15843925a1a2d934f1df9715e821dea3a7eb Mon Sep 17 00:00:00 2001 From: ABILASH <114130954+Abilash-19@users.noreply.github.com> Date: Thu, 19 Feb 2026 00:10:29 +0530 Subject: [PATCH 07/14] refactor(theme): migrate to three-layer semantic token architecture #2 --- src/theme/DefaultTheme.ts | 233 +++++++++++++-------- src/theme/ThemeProvider.tsx | 158 +++++++++++--- src/theme/Types.ts | 115 ++++++++-- src/theme/index.tsx | 29 +-- src/theme/utils.ts | 122 +++++++++++ src/theme/utils/CSSVariablesDeclaration.ts | 54 ----- src/theme/utils/DeepMerge.ts | 28 --- 7 files changed, 498 insertions(+), 241 deletions(-) create mode 100644 src/theme/utils.ts delete mode 100644 src/theme/utils/CSSVariablesDeclaration.ts delete mode 100644 src/theme/utils/DeepMerge.ts diff --git a/src/theme/DefaultTheme.ts b/src/theme/DefaultTheme.ts index ef12778..3b12050 100644 --- a/src/theme/DefaultTheme.ts +++ b/src/theme/DefaultTheme.ts @@ -1,96 +1,149 @@ // src/theme/defaultTheme.ts +// The light theme. Semantic tokens reference palette values by variable — not +// raw hex — so it's always clear WHERE a color comes from. + import type { ThemeConfig } from "./Types"; +// ─── Palette (shared between light/dark) ──────────────────────────────────── + +const palette = { + primary: { + 50: "#eff6ff", + 100: "#dbeafe", + 200: "#bfdbfe", + 300: "#93c5fd", + 400: "#60a5fa", + 500: "#3b82f6", + 600: "#2563eb", + 700: "#1d4ed8", + 800: "#1e40af", + 900: "#1e3a8a", + }, + success: { + 50: "#f0fdf4", + 100: "#dcfce7", + 200: "#bbf7d0", + 300: "#86efac", + 400: "#4ade80", + 500: "#22c55e", + 600: "#16a34a", + 700: "#15803d", + 800: "#166534", + 900: "#14532d", + }, + warning: { + 50: "#fffbeb", + 100: "#fef3c7", + 200: "#fde68a", + 300: "#fcd34d", + 400: "#fbbf24", + 500: "#f59e0b", + 600: "#d97706", + 700: "#b45309", + 800: "#92400e", + 900: "#78350f", + }, + error: { + 50: "#fef2f2", + 100: "#fee2e2", + 200: "#fecaca", + 300: "#fca5a5", + 400: "#f87171", + 500: "#ef4444", + 600: "#dc2626", + 700: "#b91c1c", + 800: "#991b1b", + 900: "#7f1d1d", + }, + info: { + 50: "#ecfeff", + 100: "#cffafe", + 200: "#a5f3fc", + 300: "#67e8f9", + 400: "#22d3ee", + 500: "#06b6d4", + 600: "#0891b2", + 700: "#0e7490", + 800: "#155e75", + 900: "#164e63", + }, +} satisfies ThemeConfig["palette"]; + +const neutral: ThemeConfig["neutral"] = { + 50: "#f9fafb", + 100: "#f3f4f6", + 200: "#e5e7eb", + 300: "#d1d5db", + 400: "#9ca3af", + 500: "#6b7280", + 600: "#4b5563", + 700: "#374151", + 800: "#1f2937", + 900: "#111827", +}; + +// ─── Light theme ───────────────────────────────────────────────────────────── + export const defaultTheme: ThemeConfig = { - colors: { - primary: { - 50: "#eff6ff", - 100: "#dbeafe", - 200: "#bfdbfe", - 300: "#93c5fd", - 400: "#60a5fa", - 500: "#3b82f6", - 600: "#2563eb", - 700: "#1d4ed8", - 800: "#1e40af", - 900: "#1e3a8a", - 950: "#172554", - }, - secondary: { - 50: "#f8fafc", - 100: "#f1f5f9", - 200: "#e2e8f0", - 300: "#cbd5e1", - 400: "#94a3b8", - 500: "#64748b", - 600: "#475569", - 700: "#334155", - 800: "#1e293b", - 900: "#0f172a", - 950: "#020617", - }, - success: { - 50: "#f0fdf4", - 100: "#dcfce7", - 200: "#bbf7d0", - 300: "#86efac", - 400: "#4ade80", - 500: "#22c55e", - 600: "#16a34a", - 700: "#15803d", - 800: "#166534", - 900: "#14532d", - 950: "#052e16", - }, - warning: { - 50: "#fffbeb", - 100: "#fef3c7", - 200: "#fde68a", - 300: "#fcd34d", - 400: "#fbbf24", - 500: "#f59e0b", - 600: "#d97706", - 700: "#b45309", - 800: "#92400e", - 900: "#78350f", - 950: "#451a03", - }, - error: { - 50: "#fef2f2", - 100: "#fee2e2", - 200: "#fecaca", - 300: "#fca5a5", - 400: "#f87171", - 500: "#ef4444", - 600: "#dc2626", - 700: "#b91c1c", - 800: "#991b1b", - 900: "#7f1d1d", - 950: "#450a0a", - }, - info: { - 50: "#ecfeff", - 100: "#cffafe", - 200: "#a5f3fc", - 300: "#67e8f9", - 400: "#22d3ee", - 500: "#06b6d4", - 600: "#0891b2", - 700: "#0e7490", - 800: "#155e75", - 900: "#164e63", - 950: "#083344", - }, - semantic: { - background: "#ffff", // Soft cream (warm white) - foreground: "#1a1a1a", // Rich charcoal (softer than pure black) - muted: "#f5f3f0", // Warm light beige - mutedForeground: "#78716c", // Warm gray (stone) - border: "#e7e5e4", // Subtle warm border - ring: "#2563eb", // Keep blue for consistency - }, + name: "light", + colorMode: "light", + palette, + neutral, + + tokens: { + // Layout + background: neutral[50], + surface: "#ffffff", + surfaceHover: neutral[100], + elevated: "#ffffff", + + // Text + foreground: neutral[900], + foregroundMuted: neutral[500], + foregroundSubtle: neutral[400], + foregroundInverse: "#ffffff", + + // Borders + border: neutral[200], + borderSubtle: neutral[100], + borderStrong: neutral[300], + + // Inputs + input: "#ffffff", + inputHover: neutral[50], + inputFocus: "#ffffff", + + // Focus + ring: palette.primary[500], + ringOffset: "#ffffff", + + // Accent + accent: palette.primary[600], + accentForeground: "#ffffff", + accentHover: palette.primary[700], + + // Status + successSurface: palette.success[50], + successForeground: palette.success[700], + warningSurface: palette.warning[50], + warningForeground: palette.warning[700], + errorSurface: palette.error[50], + errorForeground: palette.error[700], + infoSurface: palette.info[50], + infoForeground: palette.info[700], + }, + + shape: { + radiusSm: "0.25rem", + radiusMd: "0.375rem", + radiusLg: "0.5rem", + radiusFull: "9999px", + }, + + typography: { + fontFamilySans: + 'Rubik, Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', + fontFamilyMono: + '"JetBrains Mono", "Fira Code", "Cascadia Code", Menlo, monospace', }, - radius: "0.25rem", - fontFamily: - 'Rubik, Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', }; diff --git a/src/theme/ThemeProvider.tsx b/src/theme/ThemeProvider.tsx index 7c92c23..9751255 100644 --- a/src/theme/ThemeProvider.tsx +++ b/src/theme/ThemeProvider.tsx @@ -1,49 +1,139 @@ // src/theme/ThemeProvider.tsx -import { useEffect, useMemo, type ReactNode } from "react"; -import { ThemeContext } from "./ThemeContext"; +// React context + hook for theme access and switching. +// Applies CSS variables to :root on every theme change. + +import { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useState, + type ReactNode, +} from "react"; + +import type { ThemeConfig, ThemeOverride } from "./Types"; import { defaultTheme } from "./DefaultTheme"; -import { deepMerge } from "./utils/DeepMerge"; -import { applyCSSVariables } from "./utils/CSSVariablesDeclaration"; -import type { ThemeConfig, DeepPartial } from "./Types"; +import { darkTheme } from "./DarkTheme"; +import { applyTheme, mergeTheme } from "./utils"; -export interface ThemeProviderProps { - /** - * Custom theme configuration to override defaults - */ - theme?: DeepPartial; +// ─── Registry ──────────────────────────────────────────────────────────────── +// Themes are registered by name. Extend this map to add custom themes. + +const THEME_REGISTRY: Record = { + light: defaultTheme, + dark: darkTheme, +}; + +export function registerTheme(theme: ThemeConfig): void { + THEME_REGISTRY[theme.name] = theme; +} + +// ─── Context ───────────────────────────────────────────────────────────────── + +interface ThemeContextValue { + theme: ThemeConfig; + /** Switch to a registered theme by name */ + setTheme: (name: string) => void; + /** Toggle between light and dark */ + toggleColorMode: () => void; + /** Apply an ad-hoc override to the current theme */ + overrideTheme: (override: ThemeOverride) => void; + /** Reset any ad-hoc overrides */ + resetTheme: () => void; +} + +const ThemeContext = createContext(null); + +// ─── Provider ──────────────────────────────────────────────────────────────── + +interface ThemeProviderProps { + children: ReactNode; + /** Initial theme name from THEME_REGISTRY (default: "light") */ + defaultThemeName?: string; /** - * Child components + * Optional storage key. When provided, the chosen theme name is persisted + * to localStorage and restored on next load. */ - children: ReactNode; + storageKey?: string; } -/** - * ThemeProvider component - * Wraps your app to provide theme context and apply CSS variables - * - * @example - * ```tsx - * - * - * - * ``` - */ -export const ThemeProvider = ({ theme, children }: ThemeProviderProps) => { - // Merge custom theme with default theme - const mergedTheme = useMemo( - () => (theme ? deepMerge(defaultTheme, theme) : defaultTheme), - [theme], +export function ThemeProvider({ + children, + defaultThemeName = "light", + storageKey, +}: ThemeProviderProps) { + const [activeThemeName, setActiveThemeName] = useState(() => { + if (storageKey) { + return localStorage.getItem(storageKey) ?? defaultThemeName; + } + return defaultThemeName; + }); + + const [override, setOverride] = useState(null); + + const baseTheme = THEME_REGISTRY[activeThemeName] ?? defaultTheme; + const theme = useMemo( + () => (override ? mergeTheme(baseTheme, override) : baseTheme), + [baseTheme, override], ); - // Apply CSS variables whenever theme changes + // Apply CSS variables whenever the theme changes useEffect(() => { - applyCSSVariables(mergedTheme); - }, [mergedTheme]); + applyTheme(theme); + }, [theme]); + + const setTheme = useCallback( + (name: string) => { + if (!THEME_REGISTRY[name]) { + console.warn(`[ThemeProvider] Theme "${name}" is not registered.`); + return; + } + setActiveThemeName(name); + setOverride(null); + if (storageKey) localStorage.setItem(storageKey, name); + }, + [storageKey], + ); - // Memoize context value to prevent unnecessary re-renders - const value = useMemo(() => ({ theme: mergedTheme }), [mergedTheme]); + const toggleColorMode = useCallback(() => { + const next = theme.colorMode === "light" ? "dark" : "light"; + const nextTheme = Object.values(THEME_REGISTRY).find( + (t) => t.colorMode === next, + ); + if (nextTheme) setTheme(nextTheme.name); + }, [theme.colorMode, setTheme]); + + const overrideTheme = useCallback((o: ThemeOverride) => { + setOverride((prev) => (prev ? { ...prev, ...o } : o)); + }, []); + + const resetTheme = useCallback(() => setOverride(null), []); + + const value = useMemo( + () => ({ theme, setTheme, toggleColorMode, overrideTheme, resetTheme }), + [theme, setTheme, toggleColorMode, overrideTheme, resetTheme], + ); return ( {children} ); -}; +} + +// ─── Hook ──────────────────────────────────────────────────────────────────── + +export function useTheme(): ThemeContextValue { + const ctx = useContext(ThemeContext); + if (!ctx) { + throw new Error("useTheme must be used within a "); + } + return ctx; +} + +// ─── Token accessor hook ────────────────────────────────────────────────────── +// Convenience hook that returns just the semantic tokens. +// Use in components that only need color values. + +export function useTokens() { + return useTheme().theme.tokens; +} diff --git a/src/theme/Types.ts b/src/theme/Types.ts index 22f1e1d..57b6f27 100644 --- a/src/theme/Types.ts +++ b/src/theme/Types.ts @@ -1,5 +1,7 @@ // src/theme/Types.ts +// ─── Primitive Scales ──────────────────────────────────────────────────────── + export interface ColorScale { 50: string; 100: string; @@ -11,38 +13,109 @@ export interface ColorScale { 700: string; 800: string; 900: string; - 950: string; } -export interface SemanticColors { - background: string; - foreground: string; - muted: string; - mutedForeground: string; - border: string; - ring: string; -} +// ─── Brand Palette ─────────────────────────────────────────────────────────── +// Raw color scales. No semantics here — just the palette. -export interface ThemeColors { +export interface BrandPalette { primary: ColorScale; - secondary: ColorScale; success: ColorScale; warning: ColorScale; error: ColorScale; info: ColorScale; - semantic: SemanticColors; } +// ─── Semantic Tokens ───────────────────────────────────────────────────────── +// Semantic tokens reference ROLES, not raw colors. +// Each value should ultimately resolve to a value from BrandPalette or NeutralScale. + +export interface SemanticTokens { + // Layout + background: string; + surface: string; + surfaceHover: string; + elevated: string; + + // Text + foreground: string; + foregroundMuted: string; + foregroundSubtle: string; + foregroundInverse: string; + + // Borders + border: string; + borderSubtle: string; + borderStrong: string; + + // Interactive surfaces + input: string; + inputHover: string; + inputFocus: string; + + // Focus / accessibility + ring: string; + ringOffset: string; + + // Accent (derived from brand.primary) + accent: string; + accentForeground: string; + accentHover: string; + + // Status surfaces (backgrounds for status banners, badges, etc.) + successSurface: string; + successForeground: string; + warningSurface: string; + warningForeground: string; + errorSurface: string; + errorForeground: string; + infoSurface: string; + infoForeground: string; +} + +// ─── Theme Spacing & Shape ─────────────────────────────────────────────────── + +export interface ThemeShape { + radiusSm: string; + radiusMd: string; + radiusLg: string; + radiusFull: string; +} + +export interface ThemeTypography { + fontFamilySans: string; + fontFamilyMono: string; +} + +// ─── Full Theme Config ─────────────────────────────────────────────────────── + export interface ThemeConfig { - colors: ThemeColors; - radius: string; - fontFamily: string; + /** Human-readable name used for debugging and storage keys */ + name: string; + /** Color mode for html class / data-theme attribute */ + colorMode: "light" | "dark"; + /** Raw palette — use only to build semantic tokens */ + palette: BrandPalette; + neutral: ColorScale; + /** Semantic tokens — consume these in your components */ + tokens: SemanticTokens; + shape: ThemeShape; + typography: ThemeTypography; } -export type DeepPartial = { - [P in keyof T]?: T[P] extends (infer U)[] - ? DeepPartial[] - : T[P] extends object - ? DeepPartial - : T[P]; +// ─── Theme Override ─────────────────────────────────────────────────────────── +// A safe partial override type. Palette overrides are deep-partial; +// token overrides are also partial — missing keys fall back to the base theme. + +export type PaletteOverride = { + [K in keyof BrandPalette]?: Partial; }; + +export interface ThemeOverride { + name?: string; + palette?: PaletteOverride; + neutral?: Partial; + tokens?: Partial; + shape?: Partial; + typography?: Partial; +} diff --git a/src/theme/index.tsx b/src/theme/index.tsx index bdf5e78..d3f62e4 100644 --- a/src/theme/index.tsx +++ b/src/theme/index.tsx @@ -1,22 +1,23 @@ // src/theme/index.ts +// Public API — import everything from here, not from individual files. -// Types export type { - ThemeConfig, - ThemeColors, ColorScale, - SemanticColors, + BrandPalette, + SemanticTokens, + ThemeShape, + ThemeTypography, + ThemeConfig, + ThemeOverride, + PaletteOverride, } from "./Types"; -// Themes export { defaultTheme } from "./DefaultTheme"; export { darkTheme } from "./DarkTheme"; - -// Context & Provider -export { ThemeContext, useTheme } from "./ThemeContext"; -export type { ThemeContextValue } from "./ThemeContext"; -export { ThemeProvider } from "./ThemeProvider"; -export type { ThemeProviderProps } from "./ThemeProvider"; - -export { deepMerge } from "./utils/DeepMerge"; -export { applyCSSVariables } from "./utils/CSSVariablesDeclaration"; +export { themeToVars, applyCSSVars, applyTheme, mergeTheme } from "./utils"; +export { + ThemeProvider, + useTheme, + useTokens, + registerTheme, +} from "./ThemeProvider"; diff --git a/src/theme/utils.ts b/src/theme/utils.ts new file mode 100644 index 0000000..f7dc9c9 --- /dev/null +++ b/src/theme/utils.ts @@ -0,0 +1,122 @@ +// src/theme/utils.ts +// Two core utilities: +// 1. themeToVars() — converts a ThemeConfig into a flat CSS custom property map +// 2. mergeTheme() — safely merges a ThemeOverride onto a base ThemeConfig + +import type { ColorScale, ThemeConfig, ThemeOverride } from "./Types"; + +// ─── CSS Variable Generation ───────────────────────────────────────────────── +// Converts a ThemeConfig into a flat record of CSS custom properties. +// Call this once on mount (or on theme switch) and inject into :root / a class. +// +// Output example: +// { "--color-background": "#ffffff", "--color-accent": "#2563eb", ... } + +type CSSVarMap = Record; + +export function themeToVars(theme: ThemeConfig): CSSVarMap { + const vars: CSSVarMap = {}; + + // Semantic tokens + for (const [key, value] of Object.entries(theme.tokens)) { + vars[`--color-${kebab(key)}`] = value; + } + + // Neutral scale + for (const [step, value] of Object.entries(theme.neutral)) { + vars[`--color-neutral-${step}`] = value; + } + + // Brand palette + for (const [scaleName, scale] of Object.entries(theme.palette) as [ + string, + ColorScale, + ][]) { + for (const [step, value] of Object.entries(scale) as [string, string][]) { + vars[`--color-${scaleName}-${step}`] = value; + } + } + + // Shape + vars["--radius-sm"] = theme.shape.radiusSm; + vars["--radius-md"] = theme.shape.radiusMd; + vars["--radius-lg"] = theme.shape.radiusLg; + vars["--radius-full"] = theme.shape.radiusFull; + + // Typography + vars["--font-sans"] = theme.typography.fontFamilySans; + vars["--font-mono"] = theme.typography.fontFamilyMono; + + return vars; +} + +// ─── Apply to DOM ──────────────────────────────────────────────────────────── +// Applies a CSSVarMap to an element (defaults to :root). +// Call on mount and whenever the theme changes. + +export function applyCSSVars( + vars: CSSVarMap, + target: HTMLElement = document.documentElement, +): void { + for (const [property, value] of Object.entries(vars)) { + target.style.setProperty(property, value); + } +} + +// Convenience: convert a ThemeConfig directly onto an element +export function applyTheme( + theme: ThemeConfig, + target: HTMLElement = document.documentElement, +): void { + const vars = themeToVars(theme); + applyCSSVars(vars, target); + + // Set data-theme attribute so CSS can scope selectors if needed + target.setAttribute("data-theme", theme.name); + + // Optionally set color-scheme for browser chrome (scrollbars, inputs, etc.) + target.style.colorScheme = theme.colorMode; +} + +// ─── Theme Override Merging ────────────────────────────────────────────────── +// Safely merges a ThemeOverride onto a base ThemeConfig. +// Guarantees a fully-shaped ThemeConfig is always returned. + +export function mergeTheme( + base: ThemeConfig, + override: ThemeOverride, +): ThemeConfig { + // Deep-merge palette scale overrides + const palette = { ...base.palette }; + if (override.palette) { + for (const [scaleName, scaleOverride] of Object.entries(override.palette)) { + const key = scaleName as keyof typeof palette; + if (scaleOverride) { + palette[key] = { ...palette[key], ...scaleOverride }; + } + } + } + + return { + name: override.name ?? base.name, + colorMode: base.colorMode, + palette, + neutral: override.neutral + ? { ...base.neutral, ...override.neutral } + : base.neutral, + tokens: override.tokens + ? { ...base.tokens, ...override.tokens } + : base.tokens, + shape: override.shape ? { ...base.shape, ...override.shape } : base.shape, + typography: override.typography + ? { ...base.typography, ...override.typography } + : base.typography, + }; +} + +// ─── Helpers ───────────────────────────────────────────────────────────────── + +/** camelCase → kebab-case */ +function kebab(str: string): string { + return str.replace(/([A-Z])/g, (c) => `-${c.toLowerCase()}`); +} diff --git a/src/theme/utils/CSSVariablesDeclaration.ts b/src/theme/utils/CSSVariablesDeclaration.ts deleted file mode 100644 index fe8e832..0000000 --- a/src/theme/utils/CSSVariablesDeclaration.ts +++ /dev/null @@ -1,54 +0,0 @@ -// src/theme/utils/applyCSSVariables.ts -import type { ThemeConfig } from '../Types'; - -/** - * Apply theme values as CSS variables to the document root - * @param theme - Theme configuration to apply - */ -export const applyCSSVariables = (theme: ThemeConfig): void => { - const root = document.documentElement; - - // Apply Primary Colors - Object.entries(theme.colors.primary).forEach(([key, value]) => { - root.style.setProperty(`--color-primary-${key}`, value); - }); - - // Apply Secondary Colors - Object.entries(theme.colors.secondary).forEach(([key, value]) => { - root.style.setProperty(`--color-secondary-${key}`, value); - }); - - // Apply Success Colors - Object.entries(theme.colors.success).forEach(([key, value]) => { - root.style.setProperty(`--color-success-${key}`, value); - }); - - // Apply Warning Colors - Object.entries(theme.colors.warning).forEach(([key, value]) => { - root.style.setProperty(`--color-warning-${key}`, value); - }); - - // Apply Error Colors - Object.entries(theme.colors.error).forEach(([key, value]) => { - root.style.setProperty(`--color-error-${key}`, value); - }); - - // Apply Info Colors - Object.entries(theme.colors.info).forEach(([key, value]) => { - root.style.setProperty(`--color-info-${key}`, value); - }); - - // Apply Semantic Colors - Object.entries(theme.colors.semantic).forEach(([key, value]) => { - // Convert camelCase to kebab-case (e.g., mutedForeground -> muted-foreground) - const kebabKey = key.replace(/([A-Z])/g, '-$1').toLowerCase(); - root.style.setProperty(`--color-${kebabKey}`, value); - }); - - // Apply Border Radius - root.style.setProperty('--radius', theme.radius); - - // Apply Font Family - root.style.setProperty('--font-sans', theme.fontFamily); - root.style.fontFamily = theme.fontFamily; -}; \ No newline at end of file diff --git a/src/theme/utils/DeepMerge.ts b/src/theme/utils/DeepMerge.ts deleted file mode 100644 index 6c15eef..0000000 --- a/src/theme/utils/DeepMerge.ts +++ /dev/null @@ -1,28 +0,0 @@ -// src/theme/utils/deepMerge.ts - -/** - * Deep merge two objects - * @param target - Base object - * @param source - Object to merge into target - * @returns Merged object - */ -export const deepMerge = (target: T, source: any): T => { - const output = { ...target } as any; - - for (const key in source) { - if ( - source[key] && - typeof source[key] === "object" && - !Array.isArray(source[key]) - ) { - output[key] = deepMerge( - target[key as keyof T] as Record, - source[key] as Record, - ); - } else if (source[key] !== undefined) { - output[key] = source[key]; - } - } - - return output as T; -}; From a09a1be40e66fed68b20deaefa8ca2fabd3802f6 Mon Sep 17 00:00:00 2001 From: ABILASH <114130954+Abilash-19@users.noreply.github.com> Date: Thu, 19 Feb 2026 00:28:45 +0530 Subject: [PATCH 08/14] fix: eslint issues --- src/components/Button/Button.test.tsx | 38 +++++++++++++-------------- src/components/index.ts | 16 +++++------ 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/components/Button/Button.test.tsx b/src/components/Button/Button.test.tsx index c1d4755..685c35d 100644 --- a/src/components/Button/Button.test.tsx +++ b/src/components/Button/Button.test.tsx @@ -10,59 +10,59 @@ const render = (ui: React.ReactElement) => { }; describe("Button", () => { - it("renders with text", () => { - render( -
- ); -} - -function PlaygroundContent() { - const { theme } = useTheme(); - const [isModalOpen, setIsModalOpen] = useState(false); - const [inputValue, setInputValue] = useState(""); - const [email, setEmail] = useState(""); - const [password, setPassword] = useState(""); - const [emailError, setEmailError] = useState(""); - - const handleEmailChange = (e: React.ChangeEvent) => { - setEmail(e.target.value); - if (emailError) setEmailError(""); - }; - - const validateEmail = () => { - if (!email.includes("@")) { - setEmailError("Please enter a valid email address"); - } - }; - - const sectionStyles = { - backgroundColor: theme.tokens.surface, - "--ring-color": theme.tokens.border, - } as React.CSSProperties; - - return ( -
- {/* Header */} -
-
-
-
- - UI Component Library - - - Interactive playground for testing components - -
-
-
-
- - {/* Main Content */} -
-
- {/* Buttons Section */} -
-
- - Buttons - - - Interactive buttons with multiple variants and states - -
- -
-
- - Variants - -
- - - - - -
-
- -
- - Sizes - -
- - - -
-
- -
- - States - -
- - -
-
- -
- - Interactive Examples - -
- - -
-
-
-
- - {/* Inputs Section */} -
-
- - Input Fields - - - Form inputs with validation and different states - -
- -
-
- - Basic Input - - setInputValue(e.target.value)} - /> - {inputValue && ( - - Current value:{" "} - - {inputValue} - - - )} -
- -
- - With Validation - - -
- -
- - Password Input - - setPassword(e.target.value)} - /> -
- -
- - Disabled State - - -
- -
- - Error State - - -
-
-
- - {/* Modal Section */} -
-
- - Modals & Dialogs - - - Accessible modal dialogs with overlay - -
-
- -
-
- - {/* Typography Section */} -
-
- - Typography Scale - - - All available typography variants - -
-
- Display 1 - Display 2 - Heading 1 - Heading 2 - Heading 3 - Heading 4 - Heading 5 - Heading 6 - Subtitle 1 - Subtitle 2 - - Body 1 - Regular paragraph text - - - Body 2 - Smaller paragraph text - - Caption text -
-
- - {/* Form Example */} -
-
- - Complete Form Example - - - Combining multiple components together - -
-
{ - e.preventDefault(); - console.log("Form submitted", { email, password }); - setIsModalOpen(true); - }} - className="space-y-4" - > -
- - -
- - setPassword(e.target.value)} - required - /> -
- - -
-
-
-
-
- - {/* Modal */} - setIsModalOpen(false)} - title="Confirmation Dialog" - > -
- - This is an example modal dialog. It demonstrates how the modal - component works with the current theme. - -
- - 💡 Pro tip: Modals automatically trap focus and - can be closed by clicking outside or pressing the Escape key. - -
-
- - -
-
-
-
- ); -} - function App() { return ( -
- - -
+
); } diff --git a/src/Playground.tsx b/src/Playground.tsx new file mode 100644 index 0000000..e273c86 --- /dev/null +++ b/src/Playground.tsx @@ -0,0 +1,504 @@ +import { useState } from "react"; +import { Button } from "@/components/Button"; +import { Input } from "@/components/Input"; +import { Modal } from "@/components/Modal"; +import { Typography } from "@/components/Typography"; +import { Accordion } from "@/components/Accordion"; +import { useTheme } from "@/theme"; + +function ThemeToggle() { + const { toggleColorMode, theme } = useTheme(); + return ( +
+ +
+ ); +} + +export default function Playground() { + const { theme } = useTheme(); + const [isModalOpen, setIsModalOpen] = useState(false); + const [inputValue, setInputValue] = useState(""); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [emailError, setEmailError] = useState(""); + + const handleEmailChange = (e: React.ChangeEvent) => { + setEmail(e.target.value); + if (emailError) setEmailError(""); + }; + + const validateEmail = () => { + if (!email.includes("@")) { + setEmailError("Please enter a valid email address"); + } + }; + + const sectionStyles = { + backgroundColor: theme.tokens.surface, + "--ring-color": theme.tokens.border, + } as React.CSSProperties; + + const mutedColor = { + "--typography-color": theme.tokens.foregroundMuted, + } as React.CSSProperties; + + return ( +
+ + + {/* Header */} +
+
+
+
+ + UI Component Library + + + Interactive playground for testing components + +
+
+
+
+ + {/* Main Content */} +
+
+ {/* Buttons Section */} +
+
+ + Buttons + + + Interactive buttons with multiple variants and states + +
+ +
+
+ + Variants + +
+ + + + + +
+
+ +
+ + Sizes + +
+ + + +
+
+ +
+ + States + +
+ + +
+
+ +
+ + Interactive Examples + +
+ + +
+
+
+
+ + {/* Inputs Section */} +
+
+ + Input Fields + + + Form inputs with validation and different states + +
+ +
+
+ + Basic Input + + setInputValue(e.target.value)} + /> + {inputValue && ( + + Current value:{" "} + + {inputValue} + + + )} +
+ +
+ + With Validation + + +
+ +
+ + Password Input + + setPassword(e.target.value)} + /> +
+ +
+ + Disabled State + + +
+ +
+ + Error State + + +
+
+
+ + {/* Accordion Section */} +
+
+ + Accordion + + + Expandable content sections using native details/summary + +
+ +
+ + Install the package using npm or yarn, then import the + components you need. Each component is fully typed and supports + theming out of the box. + + + Wrap your app in a ThemeProvider and use the useTheme hook to + access theme tokens. Toggle between light and dark themes with a + single function call. + + + All components follow WAI-ARIA guidelines. This accordion uses + native HTML details/summary elements for built-in keyboard + navigation and screen reader support. + + + You should not be able to see this content. + +
+
+ + {/* Modal Section */} +
+
+ + Modals & Dialogs + + + Accessible modal dialogs with overlay + +
+
+ +
+
+ + {/* Typography Section */} +
+
+ + Typography Scale + + + All available typography variants + +
+
+ Display 1 + Display 2 + Heading 1 + Heading 2 + Heading 3 + Heading 4 + Heading 5 + Heading 6 + Subtitle 1 + Subtitle 2 + + Body 1 - Regular paragraph text + + + Body 2 - Smaller paragraph text + + Caption text +
+
+ + {/* Form Example */} +
+
+ + Complete Form Example + + + Combining multiple components together + +
+
{ + e.preventDefault(); + console.log("Form submitted", { email, password }); + setIsModalOpen(true); + }} + className="space-y-4" + > +
+ + +
+ + setPassword(e.target.value)} + required + /> +
+ + +
+
+
+
+
+ + {/* Modal */} + setIsModalOpen(false)} + title="Confirmation Dialog" + > +
+ + This is an example modal dialog. It demonstrates how the modal + component works with the current theme. + +
+ + 💡 Pro tip: Modals automatically trap focus and + can be closed by clicking outside or pressing the Escape key. + +
+
+ + +
+
+
+
+ ); +} From 9b8809f2b0877c3ac84c9291246372d75faaae8b Mon Sep 17 00:00:00 2001 From: ABILASH <114130954+Abilash-19@users.noreply.github.com> Date: Tue, 24 Feb 2026 23:21:36 +0530 Subject: [PATCH 11/14] style: theme color and style changes --- src/theme/DarkTheme.ts | 20 ++++++++++---------- src/theme/DefaultTheme.ts | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/theme/DarkTheme.ts b/src/theme/DarkTheme.ts index b225e54..db8ec0c 100644 --- a/src/theme/DarkTheme.ts +++ b/src/theme/DarkTheme.ts @@ -10,16 +10,16 @@ const { palette, neutral } = defaultTheme; // Dark-mode neutral overrides — same hue family, just inverted lightness const darkNeutral: ThemeConfig["neutral"] = { - 50: "#0b0f14", - 100: "#0f1620", - 200: "#141e2a", - 300: "#1a2535", - 400: "#2d3d52", - 500: "#4a6080", - 600: "#6b8aad", - 700: "#94aec8", - 800: "#c5d5e8", - 900: "#f0f5fb", + 50: "#080808", + 100: "#101010", + 200: "#181818", + 300: "#202020", + 400: "#383838", + 500: "#585858", + 600: "#787878", + 700: "#a0a0a0", + 800: "#c8c8c8", + 900: "#f2f2f2", }; export const darkTheme: ThemeConfig = { diff --git a/src/theme/DefaultTheme.ts b/src/theme/DefaultTheme.ts index 3b12050..c12130d 100644 --- a/src/theme/DefaultTheme.ts +++ b/src/theme/DefaultTheme.ts @@ -106,7 +106,7 @@ export const defaultTheme: ThemeConfig = { // Borders border: neutral[200], borderSubtle: neutral[100], - borderStrong: neutral[300], + borderStrong: neutral[400], // Inputs input: "#ffffff", From 33a6133ad366d4c8e593f3e1f2aead9375a463d0 Mon Sep 17 00:00:00 2001 From: ABILASH <114130954+Abilash-19@users.noreply.github.com> Date: Wed, 25 Feb 2026 00:12:12 +0530 Subject: [PATCH 12/14] feat(accordion): implement accessible Accordion with default and borderless variants --- src/assets/Icons/Chevron.tsx | 24 +++ .../Accordion/Accordion.stories.tsx | 132 ++++++++++++++ src/components/Accordion/Accordion.test.tsx | 162 ++++++++++++++++++ src/components/Accordion/Accordion.tsx | 117 +++++++++++++ src/components/Accordion/Accordion.types.tsx | 10 ++ src/components/Accordion/index.ts | 2 + src/components/index.ts | 6 +- 7 files changed, 448 insertions(+), 5 deletions(-) create mode 100644 src/assets/Icons/Chevron.tsx create mode 100644 src/components/Accordion/Accordion.stories.tsx create mode 100644 src/components/Accordion/Accordion.test.tsx create mode 100644 src/components/Accordion/Accordion.tsx create mode 100644 src/components/Accordion/Accordion.types.tsx create mode 100644 src/components/Accordion/index.ts diff --git a/src/assets/Icons/Chevron.tsx b/src/assets/Icons/Chevron.tsx new file mode 100644 index 0000000..5d6d0c3 --- /dev/null +++ b/src/assets/Icons/Chevron.tsx @@ -0,0 +1,24 @@ +import { cn } from "@/utils"; + +export const Chevron = ({ className }: { className?: string }) => { + return ( + + ); +}; diff --git a/src/components/Accordion/Accordion.stories.tsx b/src/components/Accordion/Accordion.stories.tsx new file mode 100644 index 0000000..99501ed --- /dev/null +++ b/src/components/Accordion/Accordion.stories.tsx @@ -0,0 +1,132 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { Accordion } from "./Accordion"; + +const meta: Meta = { + title: "Components/Accordion", + component: Accordion, + parameters: { + layout: "centered", + }, + tags: ["autodocs"], + argTypes: { + isDefaultOpen: { + control: "boolean", + }, + isDisabled: { + control: "boolean", + }, + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + title: "What is a UI Library?", + children: + "A UI library is a collection of reusable components that help developers build consistent and polished user interfaces quickly.", + }, +}; + +export const WithSubtitle: Story = { + args: { + title: "Getting Started", + subtitle: "Learn how to install and use the library", + children: + "Install the package using npm or yarn, then import the components you need into your project. Each component is fully typed and supports theming out of the box.", + }, +}; + +export const OpenByDefault: Story = { + args: { + title: "Already Expanded", + subtitle: "This accordion starts open", + isDefaultOpen: true, + children: + "This content is visible immediately because isDefaultOpen is set to true.", + }, +}; + +export const Disabled: Story = { + args: { + title: "Disabled Accordion", + subtitle: "This cannot be toggled", + isDisabled: true, + children: "You should not be able to see this content.", + }, +}; + +export const WithRichContent: Story = { + args: { + title: "Shipping Information", + subtitle: "Delivery options and timelines", + isDefaultOpen: true, + children: ( +
+

We offer the following shipping options:

+
    +
  • + Standard — 5-7 business days +
  • +
  • + Express — 2-3 business days +
  • +
  • + Overnight — Next business day +
  • +
+

Free shipping on orders over $50.

+
+ ), + }, +}; + +export const AccordionGroup: Story = { + render: () => ( +
+ + Welcome to our application. This section covers the basics you need to + get started. + + + Explore the powerful features built into every component, including + theming, accessibility, and responsive design. + + + Find answers to frequently asked questions about installation, + configuration, and customization. + +
+ ), +}; + +export const FAQ: Story = { + render: () => ( +
+ + Run npm install @awesome-ui/library in your project + directory. + + + Yes! Wrap your app in a ThemeProvider and toggle between light and dark + themes. + + + All components follow WAI-ARIA guidelines and support keyboard + navigation. + + + Absolutely. Every component accepts a className prop and uses CSS + variables for theming, making customization straightforward. + +
+ ), +}; diff --git a/src/components/Accordion/Accordion.test.tsx b/src/components/Accordion/Accordion.test.tsx new file mode 100644 index 0000000..32cd102 --- /dev/null +++ b/src/components/Accordion/Accordion.test.tsx @@ -0,0 +1,162 @@ +import React from "react"; +import { describe, it, expect, vi } from "vitest"; +import { render, screen, fireEvent } from "@testing-library/react"; +import { Accordion } from "./Accordion"; + +// ─── Mocks ─────────────────────────────────────────────────────────────────── + +vi.mock("@/theme", () => ({ + useTheme: () => ({ + theme: { + tokens: { + surface: "#ffffff", + surfaceHover: "#f5f5f5", + borderStrong: "#e0e0e0", + foreground: "#111111", + foregroundMuted: "#888888", + }, + shape: { + radiusMd: "8px", + }, + }, + }), +})); + +vi.mock("@/utils", () => ({ + cn: (...args: any[]) => args.filter(Boolean).join(" "), +})); + +vi.mock("@/assets/Icons/Chevron", () => ({ + Chevron: ({ className }: { className?: string }) => ( + + ), +})); + +// ─── Helpers ───────────────────────────────────────────────────────────────── + +const renderAccordion = (props = {}) => + render( + +

Accordion content

+
, + ); + +// ─── Tests ─────────────────────────────────────────────────────────────────── + +describe("Accordion", () => { + describe("Rendering", () => { + it("renders a details element", () => { + const { container } = renderAccordion(); + const details = container.querySelector("details"); + expect(details).toBeInTheDocument(); + }); + + it("renders title and subtitle", () => { + renderAccordion({ + title: "Main Title", + subtitle: "Secondary text", + }); + + expect(screen.getByText("Main Title")).toBeInTheDocument(); + expect(screen.getByText("Secondary text")).toBeInTheDocument(); + }); + + it("renders children content", () => { + renderAccordion(); + expect(screen.getByText("Accordion content")).toBeInTheDocument(); + }); + + it("renders chevron icon", () => { + renderAccordion(); + expect(screen.getByTestId("chevron")).toBeInTheDocument(); + }); + }); + + describe("Props & State", () => { + it("is closed by default", () => { + const { container } = renderAccordion(); + const details = container.querySelector("details") as HTMLDetailsElement; + expect(details.open).toBe(false); + }); + + it("is open when isDefaultOpen is true", () => { + const { container } = renderAccordion({ isDefaultOpen: true }); + const details = container.querySelector("details") as HTMLDetailsElement; + expect(details.open).toBe(true); + }); + + it("applies disabled styles", () => { + const { container } = renderAccordion({ isDisabled: true }); + const details = container.querySelector("details"); + + expect(details).toHaveClass("pointer-events-none"); + expect(details).toHaveClass("opacity-50"); + }); + }); + + describe("Ref Forwarding", () => { + it("forwards ref to details element", () => { + const ref = React.createRef(); + + render( + + Content + , + ); + + expect(ref.current).toBeInstanceOf(HTMLDetailsElement); + }); + }); + + describe("ID Handling", () => { + it("uses provided id", () => { + const { container } = renderAccordion({ id: "custom-id" }); + const details = container.querySelector("details"); + + expect(details).toHaveAttribute("id", "custom-id"); + }); + + it("generates id when none is provided", () => { + const { container } = render(C1); + const details = container.querySelector("details"); + + expect(details?.id).toBeTruthy(); + }); + }); + + describe("Interaction", () => { + it("toggles open state when summary is clicked", () => { + const { container } = renderAccordion(); + const details = container.querySelector("details") as HTMLDetailsElement; + const summary = container.querySelector("summary") as HTMLElement; + + expect(details.open).toBe(false); + + fireEvent.click(summary); + expect(details.open).toBe(true); + + fireEvent.click(summary); + expect(details.open).toBe(false); + }); + }); + + describe("Styling", () => { + it("applies CSS variables from theme", () => { + const { container } = renderAccordion(); + const details = container.querySelector("details") as HTMLElement; + + const style = details.getAttribute("style") || ""; + expect(style).toContain("--accordion-bg: #ffffff"); + expect(style).toContain("--accordion-border: #e0e0e0"); + }); + + it("applies custom className", () => { + const { container } = renderAccordion({ + className: "custom-test-class", + }); + + const details = container.querySelector("details"); + expect(details).toHaveClass("custom-test-class"); + }); + }); +}); diff --git a/src/components/Accordion/Accordion.tsx b/src/components/Accordion/Accordion.tsx new file mode 100644 index 0000000..be4ac9a --- /dev/null +++ b/src/components/Accordion/Accordion.tsx @@ -0,0 +1,117 @@ +import React, { forwardRef, useId } from "react"; +import { cn } from "@/utils"; +import { useTheme } from "@/theme"; +import { Chevron } from "@/assets/Icons/Chevron"; +import type { AccordionProps } from "./Accordion.types"; + +const Accordion = forwardRef( + ( + { + id, + className, + title, + subtitle, + children, + isDefaultOpen = false, + isDisabled = false, + variant = "borderless", + }, + ref, + ) => { + const { theme } = useTheme(); + const generatedId = useId(); + const accordionId = id || generatedId; + + const themeStyles = { + "--accordion-bg": theme.tokens.surface, + "--accordion-bg-hover": theme.tokens.surfaceHover, + "--accordion-border": theme.tokens.borderStrong, + "--accordion-text": theme.tokens.foreground, + "--accordion-muted": theme.tokens.foregroundMuted, + "--accordion-radius": theme.shape.radiusMd, + } as React.CSSProperties; + + const isBorderless = variant === "borderless"; + + return ( +
+ +
+ {title && ( + + {title} + + )} + {subtitle && ( + + {subtitle} + + )} +
+ + +
+ +
+ {children} +
+
+ ); + }, +); + +Accordion.displayName = "Accordion"; + +export { Accordion }; diff --git a/src/components/Accordion/Accordion.types.tsx b/src/components/Accordion/Accordion.types.tsx new file mode 100644 index 0000000..5425215 --- /dev/null +++ b/src/components/Accordion/Accordion.types.tsx @@ -0,0 +1,10 @@ +export interface AccordionProps { + id?: string; + className?: string; + title?: React.ReactNode; + subtitle?: React.ReactNode; + children?: React.ReactNode; + isDefaultOpen?: boolean; + isDisabled?: boolean; + variant?: "default" | "borderless"; +} diff --git a/src/components/Accordion/index.ts b/src/components/Accordion/index.ts new file mode 100644 index 0000000..f8d1df5 --- /dev/null +++ b/src/components/Accordion/index.ts @@ -0,0 +1,2 @@ +export * from "./Accordion"; +export * from "./Accordion.types"; diff --git a/src/components/index.ts b/src/components/index.ts index cd77b6c..c8ba2d8 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,3 +1,4 @@ +export * from "./Accordion"; export * from "./Button"; export * from "./Input"; export * from "./Modal"; @@ -6,8 +7,3 @@ export * from "./Typography"; // ── Theme ── export { ThemeProvider, useTheme, defaultTheme, darkTheme } from "../theme"; export type { ThemeConfig, ColorScale } from "../theme"; - -// ── Explicit type re-exports (avoids isolatedModules issues) ── -export type { InputProps } from "./Input"; -export type { ButtonProps } from "./Button"; -export type { TypographyProps } from "./Typography"; From 9c91879185b14d973bd271ecaeb1a34c5653c69a Mon Sep 17 00:00:00 2001 From: ABILASH <114130954+Abilash-19@users.noreply.github.com> Date: Wed, 25 Feb 2026 00:13:37 +0530 Subject: [PATCH 13/14] fix: removed unnecessary exports and fixed the path --- src/components/Button/index.ts | 2 +- src/components/Input/Input.tsx | 2 +- src/components/Typography/Typography.tsx | 3 +-- src/components/Typography/Typography.types.tsx | 5 ++--- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/components/Button/index.ts b/src/components/Button/index.ts index 20e8e1a..9e89e3e 100644 --- a/src/components/Button/index.ts +++ b/src/components/Button/index.ts @@ -1,2 +1,2 @@ export { Button } from "./Button"; -export type { ButtonProps, ButtonSize, ButtonVariant } from "./Button.types"; +export type { ButtonSize, ButtonVariant } from "./Button.types"; diff --git a/src/components/Input/Input.tsx b/src/components/Input/Input.tsx index 53c1207..e9c42a8 100644 --- a/src/components/Input/Input.tsx +++ b/src/components/Input/Input.tsx @@ -47,7 +47,7 @@ const Input = forwardRef( const themeStyles = { "--radius": theme.shape.radiusMd, - "--border": error ? theme.palette.error[500] : theme.tokens.border, + "--border": error ? theme.palette.error[500] : theme.tokens.borderStrong, "--ring": error ? theme.palette.error[500] : theme.tokens.ring, "--bg": theme.tokens.input, "--bg-hover": theme.tokens.inputHover, diff --git a/src/components/Typography/Typography.tsx b/src/components/Typography/Typography.tsx index 3d3aaca..bc97dba 100644 --- a/src/components/Typography/Typography.tsx +++ b/src/components/Typography/Typography.tsx @@ -1,6 +1,5 @@ -// src/components/Typography/Typography.tsx import React from "react"; -import { cn } from "../../utils/cn"; +import { cn } from "@/utils/cn"; import type { TypographyProps, TypographyVariant, diff --git a/src/components/Typography/Typography.types.tsx b/src/components/Typography/Typography.types.tsx index a18a5b5..853d647 100644 --- a/src/components/Typography/Typography.types.tsx +++ b/src/components/Typography/Typography.types.tsx @@ -1,6 +1,5 @@ -// src/components/Typography/Typography.types.ts - -import type { JSX } from "react"; +import type React from "react"; +import { type JSX } from "react"; export type TypographyVariant = | "h1" From f7765345c1f3fba581ad70b0dee1a385857452e1 Mon Sep 17 00:00:00 2001 From: ABILASH <114130954+Abilash-19@users.noreply.github.com> Date: Wed, 25 Feb 2026 00:14:52 +0530 Subject: [PATCH 14/14] refactor(app): separate Playground into its own component --- src/Playground.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Playground.tsx b/src/Playground.tsx index e273c86..056d087 100644 --- a/src/Playground.tsx +++ b/src/Playground.tsx @@ -311,7 +311,10 @@ export default function Playground() {
- + Getting Started} + subtitle="Installation guide" + > Install the package using npm or yarn, then import the components you need. Each component is fully typed and supports theming out of the box.