From 7f36ed16496c7a619b594f1d90e5b4fde10cff68 Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Fri, 27 Mar 2026 20:16:12 +0700 Subject: [PATCH 001/199] feat(storybook): Sync dark mode with styles tokens --- apps/storybook/.storybook/preview.ts | 29 +++++++- .../src/components/button/button.stories.tsx | 70 ++++++++++++++++--- 2 files changed, 86 insertions(+), 13 deletions(-) diff --git a/apps/storybook/.storybook/preview.ts b/apps/storybook/.storybook/preview.ts index c463dd2..479141c 100644 --- a/apps/storybook/.storybook/preview.ts +++ b/apps/storybook/.storybook/preview.ts @@ -1,8 +1,22 @@ import type { Preview } from "@storybook/react"; + import "./styles.css"; -const PREVIEW: Preview = { +const preview: Preview = { parameters: { + backgrounds: { + default: "light", + options: { + light: { + name: "light", + value: "var(--color-background, #fff)", + }, + dark: { + name: "dark", + value: "var(--color-background, #0a0a0a)", + }, + }, + }, controls: { matchers: { color: /(background|color)$/i, @@ -10,6 +24,17 @@ const PREVIEW: Preview = { }, }, }, + decorators: [ + (Story, context) => { + const bg = context.globals?.backgrounds?.value; + const isDark = bg?.includes("dark") || bg?.includes("0a0a0a"); + const theme = isDark ? "dark" : "light"; + + document.documentElement.setAttribute("data-theme", theme); + + return Story(); + }, + ], }; -export default PREVIEW; +export default preview; diff --git a/packages/react/src/components/button/button.stories.tsx b/packages/react/src/components/button/button.stories.tsx index e30e7e8..8abc765 100644 --- a/packages/react/src/components/button/button.stories.tsx +++ b/packages/react/src/components/button/button.stories.tsx @@ -10,8 +10,47 @@ const meta = { layout: "centered", docs: { description: { - component: - "Accessible button component built on React Aria. Supports semantic variants, golden-ratio spacing, and pending state with spinner.", + component: [ + "Accessible button built on [React Aria Button](https://react-spectrum.adobe.com/react-aria/Button.html).", + "", + "### React Aria Props", + "", + "| Prop | Type | Description |", + "| --- | --- | --- |", + "| `isPending` | `boolean` | Pending state. Disables press/hover, announces to screen readers |", + "| `isDisabled` | `boolean` | Whether the button is disabled |", + "| `children` | `ReactNode \\| (renderProps) => ReactNode` | Button content, supports render props |", + "| `onPress` | `(e: PressEvent) => void` | Press event handler |", + "| `onPressStart` | `(e: PressEvent) => void` | Press start handler |", + "| `onPressEnd` | `(e: PressEvent) => void` | Press end handler |", + "| `onPressChange` | `(isPressed: boolean) => void` | Press state change handler |", + "| `onHoverStart` | `(e: HoverEvent) => void` | Hover start handler |", + "| `onHoverEnd` | `(e: HoverEvent) => void` | Hover end handler |", + "| `onHoverChange` | `(isHovering: boolean) => void` | Hover state change handler |", + "| `onFocus` | `(e: FocusEvent) => void` | Focus handler |", + "| `onBlur` | `(e: FocusEvent) => void` | Blur handler |", + "| `onFocusChange` | `(isFocused: boolean) => void` | Focus state change handler |", + "| `onKeyDown` | `(e: KeyboardEvent) => void` | Key down handler |", + "| `onKeyUp` | `(e: KeyboardEvent) => void` | Key up handler |", + "| `type` | `'button' \\| 'submit' \\| 'reset'` | Button type attribute |", + "| `form` | `string` | Form ID to associate |", + "| `formAction` | `string` | Form action URL |", + "| `autoFocus` | `boolean` | Auto focus on mount |", + "| `aria-label` | `string` | Accessible label |", + "| `aria-labelledby` | `string` | ID of labelling element |", + "| `aria-describedby` | `string` | ID of describing element |", + "", + "### Render Props (CSS Selectors)", + "", + "| Render Prop | CSS Selector | Description |", + "| --- | --- | --- |", + "| `isHovered` | `[data-hovered]` | Mouse hover |", + "| `isPressed` | `[data-pressed]` | Pressed/active |", + "| `isFocused` | `[data-focused]` | Focused (mouse or keyboard) |", + "| `isFocusVisible` | `[data-focus-visible]` | Keyboard focused |", + "| `isDisabled` | `[data-disabled]` | Disabled |", + "| `isPending` | `[data-pending]` | Pending/loading |", + ].join("\n"), }, }, }, @@ -48,7 +87,7 @@ const meta = { size: { control: "select", options: ["sm", "md", "lg", "xl"], - description: "Button size", + description: "Button size (golden ratio scale)", table: { type: { summary: '"sm" | "md" | "lg" | "xl"', @@ -74,7 +113,7 @@ const meta = { }, isPending: { control: "boolean", - description: "Whether the button shows a loading spinner", + description: "Shows loading spinner, disables press/hover", table: { type: { summary: "boolean", @@ -110,12 +149,7 @@ const meta = { type Story = StoryObj; -const Default: Story = { - args: { - children: "Button", - }, - render: (args) => - -``` - -### 5. Separation of Styles and Logic - -- `@fried-ui/styles` — pure CSS: tokens, `@utility`, component BEM styles -- `@fried-ui/react` — React components, behavior, accessibility - -Multi-framework ready: styles work with Vue, Svelte, vanilla HTML. - -### 6. CSS-First, No JavaScript Runtime - -- Design tokens in `@theme` -- Component styles in CSS + BEM (`fri-` prefix) + `@apply` -- No tailwind-variants, no JS runtime styling -- Dark mode: `prefers-color-scheme` + `@custom-variant dark` -- `prefers-reduced-motion` automatic - -### 7. Golden Ratio Spacing - -- x = font-size as base unit, all values derive from φ -- When size changes, x must change too (Proportional Scaling) - -### 8. Color State Mathematics - -- Hover/Active calculated via palette steps (always darken) -- Semantic tokens use `var()` from Tailwind built-in + custom palette - -## Variant System - -### Semantic Variants - -| Variant | Intent | Use when | -| ----------- | -------------------------- | ----------------------------------------- | -| `primary` | Main forward action | Primary action per context (1 per screen) | -| `secondary` | Alternative action | Secondary/alternative actions | -| `success` | Positive / confirmation | Confirm, approve, success | -| `warning` | Caution / attention | Warning, requires caution | -| `danger` | Destructive / irreversible | Delete, cancel, irreversible actions | -| `info` | Informational | Neutral informational context | -| `ghost` | Low-emphasis / dismissive | Cancel, skip, tertiary action | -| `link` | Inline text-like | Navigation, inline text action | - -### Sizes - -| Size | Use when | -| ---- | ------------------------- | -| `sm` | Compact, dense UI | -| `md` | Default, general purpose | -| `lg` | Prominent, touch-friendly | -| `xl` | Hero, display-level | - -### Default Values - -All components: `variant="primary"`, `size="md"` as defaults. - -## Component API Conventions - -- `variant` + `size` always optional (have defaults) -- State props use `is-` prefix — `isDisabled`, `isPending` (React Aria) -- Event handlers use `on-` prefix — `onPress` (React Aria) -- `className` for style overrides diff --git a/packages/react/src/components/button/Button.tsx b/packages/react/src/components/button/Button.tsx index 303dd28..a37eaa6 100644 --- a/packages/react/src/components/button/Button.tsx +++ b/packages/react/src/components/button/Button.tsx @@ -10,18 +10,28 @@ import { cn } from "src/utils/cn"; export type ButtonProps = { variant?: ButtonVariant; size?: ButtonSize; + radius?: ButtonRadius; className?: string; children?: ReactNode | ((renderProps: { isPending: boolean }) => ReactNode); } & Omit; type ButtonVariant = "primary"; type ButtonSize = "sm" | "md" | "lg" | "xl"; +type ButtonRadius = "none" | "sm" | "md" | "lg" | "xl" | "full"; function Button(props: Readonly) { - const { size = "md", variant = "primary", children, className, ...rest } = props; + const { radius = "md", size = "md", variant = "primary", children, className, ...rest } = props; const pendingClass = rest.isPending ? "fri-button--pending" : undefined; - const buttonClassName = cn("fri-button", `fri-button--${variant}`, `fri-button--${size}`, pendingClass, className); + + const buttonClassName = cn( + "fri-button", + `fri-button--${variant}`, + `fri-button--${size}`, + `fri-button--radius-${radius}`, + pendingClass, + className, + ); return ( diff --git a/packages/styles/src/components/button.css b/packages/styles/src/components/button.css index 510ca0f..8f0da57 100644 --- a/packages/styles/src/components/button.css +++ b/packages/styles/src/components/button.css @@ -1,20 +1,15 @@ .fri-button { @apply relative inline-flex items-center justify-center; - @apply leading-none font-medium whitespace-nowrap select-none no-highlight; - @apply border border-transparent; - @apply cursor-pointer; - - transition: - background-color var(--duration-fast) var(--ease-smooth), - color var(--duration-fast) var(--ease-smooth); + @apply leading-none font-medium whitespace-nowrap; + @apply cursor-pointer border border-transparent select-none no-highlight; + @apply transition-colors duration-(--duration-fast) ease-smooth motion-reduce:transition-none; --fri-button-bg: transparent; --fri-button-bg-hover: var(--fri-button-bg); --fri-button-bg-pressed: var(--fri-button-bg-hover); --fri-button-fg: currentColor; - background-color: var(--fri-button-bg); - color: var(--fri-button-fg); + @apply bg-(--fri-button-bg) text-(--fri-button-fg); &:focus-visible, &[data-focus-visible] { @@ -32,13 +27,13 @@ &:active, &[data-pressed] { - background-color: var(--fri-button-bg-pressed); + @apply bg-(--fri-button-bg-pressed); } @media (hover: hover) { &:hover, &[data-hovered] { - background-color: var(--fri-button-bg-hover); + @apply bg-(--fri-button-bg-hover); } } } @@ -52,29 +47,48 @@ .fri-button--sm { @apply gap-[8.65px] px-3.5 py-1.5 text-sm; - border-radius: calc(var(--radius) * 0.5); } .fri-button--md { @apply gap-[9.89px] px-4 py-2 text-base; - border-radius: var(--radius); } .fri-button--lg { @apply gap-[12.36px] px-5 py-2.5 text-xl; - border-radius: calc(var(--radius) * 1.5); } .fri-button--xl { @apply gap-[14.83px] px-6 py-3 text-2xl; - border-radius: calc(var(--radius) * 2); +} + +.fri-button--radius-none { + @apply rounded-none; +} + +.fri-button--radius-sm { + @apply rounded-sm; +} + +.fri-button--radius-md { + @apply rounded-md; +} + +.fri-button--radius-lg { + @apply rounded-lg; +} + +.fri-button--radius-xl { + @apply rounded-xl; +} + +.fri-button--radius-full { + @apply rounded-full; } .fri-button--pending { - color: transparent; + @apply text-transparent; } .fri-button__spinner { - @apply absolute inset-0 m-auto size-match-font animate-spin; - color: var(--fri-button-fg); + @apply absolute inset-0 m-auto size-match-font animate-spin text-(--fri-button-fg); } From 89c6579db6c0ef90f655423f118b640a9d1a2fd5 Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Fri, 27 Mar 2026 23:29:35 +0700 Subject: [PATCH 008/199] refactor(docs): Generic rules, storybook convention, API docs Add storybook.md rule, radius prop in stories, full API reference in docs. Generic all rules/agents. Add docs addon. Home page uses design tokens. --- apps/docs/src/app/page.tsx | 16 +++- .../src/content/docs/components/button.mdx | 82 ++++++++++++++--- apps/storybook/package.json | 1 + .../src/components/button/button.stories.tsx | 91 ++++++++----------- pnpm-lock.yaml | 43 ++++++++- 5 files changed, 164 insertions(+), 69 deletions(-) diff --git a/apps/docs/src/app/page.tsx b/apps/docs/src/app/page.tsx index 862eb25..2092f9f 100644 --- a/apps/docs/src/app/page.tsx +++ b/apps/docs/src/app/page.tsx @@ -1,9 +1,19 @@ +import Link from "next/link"; + function Home(): React.JSX.Element { return ( -
+
-

fried-ui docs

-

Documentation

+

fried-ui

+ +

Beautiful, accessible React components

+ + + Documentation +
); diff --git a/apps/docs/src/content/docs/components/button.mdx b/apps/docs/src/content/docs/components/button.mdx index c8d49cc..61141e4 100644 --- a/apps/docs/src/content/docs/components/button.mdx +++ b/apps/docs/src/content/docs/components/button.mdx @@ -9,19 +9,45 @@ description: An accessible button component built on React Aria with focus manag import { Button } from "@fried-ui/react/button"; ``` -### Usage +## Usage ```tsx ``` -### Disabled +## Sizes + +The `size` prop controls padding, gap, and font-size using golden ratio proportional scaling. + +```tsx + + + + +``` + +## Radius + +The `radius` prop controls border-radius independently from size. + +```tsx + + + + + + +``` + +## Disabled ```tsx ``` -### Pending +## Pending + +Shows a loading spinner and disables press/hover events. ```tsx @@ -29,12 +55,44 @@ import { Button } from "@fried-ui/react/button"; ## API Reference -| Prop | Type | Default | Description | -| ------------ | ------------------------- | ----------- | ---------------------- | -| `variant` | `"primary"` | `"primary"` | Visual style | -| `size` | `"md"` | `"md"` | Button size | -| `isDisabled` | `boolean` | `false` | Disables the button | -| `isPending` | `boolean` | `false` | Shows loading spinner | -| `onPress` | `(e: PressEvent) => void` | - | Press event handler | -| `className` | `string` | - | Additional CSS classes | -| `children` | `ReactNode` | - | Button content | +### Button Props + +| Prop | Type | Default | Description | +| ------------------ | -------------------------------------------------- | ----------- | ------------------------------------------ | +| `variant` | `"primary"` | `"primary"` | The visual variant of the button | +| `size` | `"sm" \| "md" \| "lg" \| "xl"` | `"md"` | The size of the button | +| `radius` | `"none" \| "sm" \| "md" \| "lg" \| "xl" \| "full"` | `"md"` | The border radius of the button | +| `isDisabled` | `boolean` | `false` | Whether the button is disabled | +| `isPending` | `boolean` | `false` | Whether the button shows a loading spinner | +| `onPress` | `(e: PressEvent) => void` | - | Handler called when the button is pressed | +| `onPressStart` | `(e: PressEvent) => void` | - | Handler called when press starts | +| `onPressEnd` | `(e: PressEvent) => void` | - | Handler called when press ends | +| `onPressChange` | `(isPressed: boolean) => void` | - | Handler called when press state changes | +| `onHoverStart` | `(e: HoverEvent) => void` | - | Handler called when hover starts | +| `onHoverEnd` | `(e: HoverEvent) => void` | - | Handler called when hover ends | +| `onHoverChange` | `(isHovering: boolean) => void` | - | Handler called when hover state changes | +| `onFocus` | `(e: FocusEvent) => void` | - | Handler called when focused | +| `onBlur` | `(e: FocusEvent) => void` | - | Handler called when blurred | +| `onFocusChange` | `(isFocused: boolean) => void` | - | Handler called when focus state changes | +| `onKeyDown` | `(e: KeyboardEvent) => void` | - | Handler called on key down | +| `onKeyUp` | `(e: KeyboardEvent) => void` | - | Handler called on key up | +| `type` | `"button" \| "submit" \| "reset"` | `"button"` | The button type attribute | +| `form` | `string` | - | The form element to associate with | +| `formAction` | `string` | - | The URL for form submission | +| `autoFocus` | `boolean` | `false` | Whether to auto focus on mount | +| `aria-label` | `string` | - | Accessible label | +| `aria-labelledby` | `string` | - | ID of the labelling element | +| `aria-describedby` | `string` | - | ID of the describing element | +| `className` | `string` | - | Additional CSS classes | +| `children` | `ReactNode \| (renderProps) => ReactNode` | - | Button content | + +### Render Props (CSS Selectors) + +| Render Prop | CSS Selector | Description | +| ---------------- | ---------------------- | --------------------------- | +| `isHovered` | `[data-hovered]` | Mouse hover state | +| `isPressed` | `[data-pressed]` | Pressed/active state | +| `isFocused` | `[data-focused]` | Focused (mouse or keyboard) | +| `isFocusVisible` | `[data-focus-visible]` | Keyboard focused only | +| `isDisabled` | `[data-disabled]` | Disabled state | +| `isPending` | `[data-pending]` | Pending/loading state | diff --git a/apps/storybook/package.json b/apps/storybook/package.json index 4750506..43b9f34 100644 --- a/apps/storybook/package.json +++ b/apps/storybook/package.json @@ -16,6 +16,7 @@ }, "devDependencies": { "@repo/quality": "workspace:*", + "@storybook/addon-docs": "^10.3.3", "@storybook/addon-vitest": "^10.3.3", "@storybook/react": "^10.3.3", "@storybook/react-vite": "^10.3.3", diff --git a/packages/react/src/components/button/button.stories.tsx b/packages/react/src/components/button/button.stories.tsx index 8abc765..70f1e15 100644 --- a/packages/react/src/components/button/button.stories.tsx +++ b/packages/react/src/components/button/button.stories.tsx @@ -10,47 +10,8 @@ const meta = { layout: "centered", docs: { description: { - component: [ - "Accessible button built on [React Aria Button](https://react-spectrum.adobe.com/react-aria/Button.html).", - "", - "### React Aria Props", - "", - "| Prop | Type | Description |", - "| --- | --- | --- |", - "| `isPending` | `boolean` | Pending state. Disables press/hover, announces to screen readers |", - "| `isDisabled` | `boolean` | Whether the button is disabled |", - "| `children` | `ReactNode \\| (renderProps) => ReactNode` | Button content, supports render props |", - "| `onPress` | `(e: PressEvent) => void` | Press event handler |", - "| `onPressStart` | `(e: PressEvent) => void` | Press start handler |", - "| `onPressEnd` | `(e: PressEvent) => void` | Press end handler |", - "| `onPressChange` | `(isPressed: boolean) => void` | Press state change handler |", - "| `onHoverStart` | `(e: HoverEvent) => void` | Hover start handler |", - "| `onHoverEnd` | `(e: HoverEvent) => void` | Hover end handler |", - "| `onHoverChange` | `(isHovering: boolean) => void` | Hover state change handler |", - "| `onFocus` | `(e: FocusEvent) => void` | Focus handler |", - "| `onBlur` | `(e: FocusEvent) => void` | Blur handler |", - "| `onFocusChange` | `(isFocused: boolean) => void` | Focus state change handler |", - "| `onKeyDown` | `(e: KeyboardEvent) => void` | Key down handler |", - "| `onKeyUp` | `(e: KeyboardEvent) => void` | Key up handler |", - "| `type` | `'button' \\| 'submit' \\| 'reset'` | Button type attribute |", - "| `form` | `string` | Form ID to associate |", - "| `formAction` | `string` | Form action URL |", - "| `autoFocus` | `boolean` | Auto focus on mount |", - "| `aria-label` | `string` | Accessible label |", - "| `aria-labelledby` | `string` | ID of labelling element |", - "| `aria-describedby` | `string` | ID of describing element |", - "", - "### Render Props (CSS Selectors)", - "", - "| Render Prop | CSS Selector | Description |", - "| --- | --- | --- |", - "| `isHovered` | `[data-hovered]` | Mouse hover |", - "| `isPressed` | `[data-pressed]` | Pressed/active |", - "| `isFocused` | `[data-focused]` | Focused (mouse or keyboard) |", - "| `isFocusVisible` | `[data-focus-visible]` | Keyboard focused |", - "| `isDisabled` | `[data-disabled]` | Disabled |", - "| `isPending` | `[data-pending]` | Pending/loading |", - ].join("\n"), + component: + "Accessible button built on [React Aria Button](https://react-spectrum.adobe.com/react-aria/Button.html). Supports semantic variants, golden-ratio spacing, and pending state with spinner.", }, }, }, @@ -58,44 +19,59 @@ const meta = { children: "Button", variant: "primary", size: "md", + radius: "md", }, argTypes: { children: { control: "text", - description: "Button content", + description: "Button label", table: { type: { summary: "ReactNode", }, - category: "Content", + category: "Children", }, }, variant: { control: "select", options: ["primary"], - description: "Visual style variant", + description: "The visual variant of the button", table: { type: { summary: '"primary"', }, defaultValue: { - summary: '"primary"', + summary: "primary", }, - category: "Style", + category: "Style Variants", }, }, size: { control: "select", options: ["sm", "md", "lg", "xl"], - description: "Button size (golden ratio scale)", + description: "The size of the button", table: { type: { summary: '"sm" | "md" | "lg" | "xl"', }, defaultValue: { - summary: '"md"', + summary: "md", }, - category: "Style", + category: "Style Variants", + }, + }, + radius: { + control: "select", + options: ["none", "sm", "md", "lg", "xl", "full"], + description: "The border radius of the button", + table: { + type: { + summary: '"none" | "sm" | "md" | "lg" | "xl" | "full"', + }, + defaultValue: { + summary: "md", + }, + category: "Style Variants", }, }, isDisabled: { @@ -113,7 +89,7 @@ const meta = { }, isPending: { control: "boolean", - description: "Shows loading spinner, disables press/hover", + description: "Whether the button shows a loading spinner", table: { type: { summary: "boolean", @@ -162,6 +138,19 @@ const Sizes: Story = { ), }; +const Radii: Story = { + render: () => ( +
+ + + + + + +
+ ), +}; + const Disabled: Story = { args: { children: "Disabled", @@ -176,6 +165,6 @@ const Pending: Story = { }, }; -export { Default, Sizes, Disabled, Pending }; +export { Default, Sizes, Radii, Disabled, Pending }; export default meta; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0dd4f7e..d5e37b0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -136,9 +136,12 @@ importers: '@repo/quality': specifier: workspace:* version: link:../../packages/quality + '@storybook/addon-docs': + specifier: ^10.3.3 + version: 10.3.3(@types/react@19.2.2)(esbuild@0.27.4)(rollup@4.60.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) '@storybook/addon-vitest': specifier: ^10.3.3 - version: 10.3.3(@vitest/browser-playwright@4.1.2(playwright@1.58.2)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2))(@vitest/browser@4.1.2(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))))(@vitest/runner@4.1.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vitest@4.1.2(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))) + version: 10.3.3(@vitest/browser-playwright@4.1.2(playwright@1.58.2)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2))(@vitest/browser@4.1.2(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))))(@vitest/runner@4.1.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vitest@4.1.2(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))) '@storybook/react': specifier: ^10.3.3 version: 10.3.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(typescript@5.9.2) @@ -1130,6 +1133,12 @@ packages: '@mdx-js/mdx@3.1.1': resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==} + '@mdx-js/react@3.1.1': + resolution: {integrity: sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==} + peerDependencies: + '@types/react': '>=16' + react: '>=16' + '@modelcontextprotocol/sdk@1.28.0': resolution: {integrity: sha512-gmloF+i+flI8ouQK7MWW4mOwuMh4RePBuPFAEPC6+pdqyWOUMDOixb6qZ69owLJpz6XmyllCouc4t8YWO+E2Nw==} engines: {node: '>=18'} @@ -2531,6 +2540,11 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@storybook/addon-docs@10.3.3': + resolution: {integrity: sha512-trJQTpOtuOEuNv1Rn8X2Sopp5hSPpb0u0soEJ71BZAbxe4d2Y1d/1MYcxBdRKwncum6sCTsnxTpqQ/qvSJKlTQ==} + peerDependencies: + storybook: ^10.3.3 + '@storybook/addon-vitest@10.3.3': resolution: {integrity: sha512-9bbUAgraZhHh35WuWJn/83B0KvkcsP8dNpzbhssMeWQTfu92TR3DqRNeGTNSlyZvhbGfwiwT3TfBzzM4dX1feg==} peerDependencies: @@ -7407,6 +7421,12 @@ snapshots: transitivePeerDependencies: - supports-color + '@mdx-js/react@3.1.1(@types/react@19.2.2)(react@19.2.0)': + dependencies: + '@types/mdx': 2.0.13 + '@types/react': 19.2.2 + react: 19.2.0 + '@modelcontextprotocol/sdk@1.28.0(zod@3.25.76)': dependencies: '@hono/node-server': 1.19.11(hono@4.12.9) @@ -9141,7 +9161,24 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@storybook/addon-vitest@10.3.3(@vitest/browser-playwright@4.1.2(playwright@1.58.2)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2))(@vitest/browser@4.1.2(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))))(@vitest/runner@4.1.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vitest@4.1.2(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)))': + '@storybook/addon-docs@10.3.3(@types/react@19.2.2)(esbuild@0.27.4)(rollup@4.60.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))': + dependencies: + '@mdx-js/react': 3.1.1(@types/react@19.2.2)(react@19.2.0) + '@storybook/csf-plugin': 10.3.3(esbuild@0.27.4)(rollup@4.60.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) + '@storybook/icons': 2.0.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@storybook/react-dom-shim': 10.3.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + storybook: 10.3.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + ts-dedent: 2.2.0 + transitivePeerDependencies: + - '@types/react' + - esbuild + - rollup + - vite + - webpack + + '@storybook/addon-vitest@10.3.3(@vitest/browser-playwright@4.1.2(playwright@1.58.2)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2))(@vitest/browser@4.1.2(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))))(@vitest/runner@4.1.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vitest@4.1.2(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)))': dependencies: '@storybook/global': 5.0.0 '@storybook/icons': 2.0.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -9149,7 +9186,7 @@ snapshots: optionalDependencies: '@vitest/browser': 4.1.2(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))) '@vitest/browser-playwright': 4.1.2(playwright@1.58.2)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) - '@vitest/runner': 4.1.1 + '@vitest/runner': 4.1.2 vitest: 4.1.2(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) transitivePeerDependencies: - react From 969788d03d7db5cc5e1686abd004d29641a24435 Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Sat, 28 Mar 2026 01:47:06 +0700 Subject: [PATCH 009/199] feat(docs): Live previews, design anatomy, icons, link Add live component previews in docs. Source exports for dev (no build needed). Design anatomy table, with icons demo, as-a-link pattern. SVG opacity as attributes. Remove tailwindcss from styles, consumer imports first. --- apps/docs/src/app/docs/[[...slug]]/page.tsx | 26 +++- apps/docs/src/app/globals.css | 3 +- apps/docs/src/app/page.tsx | 1 - apps/docs/src/components/MdxComponents.tsx | 89 ++++++++++++ apps/docs/src/components/Preview.tsx | 17 +++ .../src/content/docs/components/button.mdx | 135 +++++++++++++++++- apps/storybook/.storybook/main.ts | 12 -- apps/storybook/.storybook/styles.css | 2 +- packages/react/eslint.config.ts | 6 + packages/react/package.json | 12 +- .../react/src/components/button/Button.tsx | 4 +- .../react/src/components/icons/Spinner.tsx | 4 +- packages/react/tsconfig.json | 5 +- packages/styles/src/index.css | 2 - prettier.config.mjs | 2 +- 15 files changed, 282 insertions(+), 38 deletions(-) create mode 100644 apps/docs/src/components/MdxComponents.tsx create mode 100644 apps/docs/src/components/Preview.tsx diff --git a/apps/docs/src/app/docs/[[...slug]]/page.tsx b/apps/docs/src/app/docs/[[...slug]]/page.tsx index 456f4d9..09c9fda 100644 --- a/apps/docs/src/app/docs/[[...slug]]/page.tsx +++ b/apps/docs/src/app/docs/[[...slug]]/page.tsx @@ -2,6 +2,17 @@ import defaultMdxComponents from "fumadocs-ui/mdx"; import { DocsBody, DocsDescription, DocsPage, DocsTitle } from "fumadocs-ui/page"; import { notFound } from "next/navigation"; +import { + ButtonAsLink, + ButtonCustomClass, + ButtonDemo, + ButtonDisabled, + ButtonPending, + ButtonRadii, + ButtonSizes, + ButtonWithIcons, +} from "@/components/MdxComponents"; +import { Preview } from "@/components/Preview"; import { source } from "@/lib/source"; interface PageProps { @@ -12,6 +23,19 @@ interface SlugParamsProps { slug?: string[]; } +const mdxComponents = { + ...defaultMdxComponents, + ButtonAsLink, + ButtonCustomClass, + ButtonDemo, + ButtonDisabled, + ButtonPending, + ButtonRadii, + ButtonSizes, + ButtonWithIcons, + Preview, +}; + async function Page(props: Readonly): Promise { const params = await props.params; const page = source.getPage(params.slug); @@ -25,7 +49,7 @@ async function Page(props: Readonly): Promise { {page.data.description} - + ); diff --git a/apps/docs/src/app/globals.css b/apps/docs/src/app/globals.css index 6c6f4f5..3526167 100644 --- a/apps/docs/src/app/globals.css +++ b/apps/docs/src/app/globals.css @@ -1,3 +1,4 @@ +@import "tailwindcss" source("../../"); +@import "fumadocs-ui/css/black.css"; @import "fumadocs-ui/style.css"; @import "@fried-ui/styles"; -@source "../../../packages/react/src/**/*.{ts,tsx}"; diff --git a/apps/docs/src/app/page.tsx b/apps/docs/src/app/page.tsx index 2092f9f..d0702ca 100644 --- a/apps/docs/src/app/page.tsx +++ b/apps/docs/src/app/page.tsx @@ -5,7 +5,6 @@ function Home(): React.JSX.Element {

fried-ui

-

Beautiful, accessible React components

Click me; +} + +function ButtonSizes(): React.JSX.Element { + return ( +
+ + + + +
+ ); +} + +function ButtonRadii(): React.JSX.Element { + return ( +
+ + + + + + +
+ ); +} + +function ButtonDisabled(): React.JSX.Element { + return ; +} + +function ButtonPending(): React.JSX.Element { + return ; +} + +function ButtonCustomClass(): React.JSX.Element { + return ; +} + +function ButtonWithIcons(): React.JSX.Element { + return ( +
+ + + + + +
+ ); +} + +function ButtonAsLink(): React.JSX.Element { + return ( + + + + ); +} + +export { + ButtonAsLink, + ButtonCustomClass, + ButtonDemo, + ButtonDisabled, + ButtonPending, + ButtonRadii, + ButtonSizes, + ButtonWithIcons, +}; diff --git a/apps/docs/src/components/Preview.tsx b/apps/docs/src/components/Preview.tsx new file mode 100644 index 0000000..7b15254 --- /dev/null +++ b/apps/docs/src/components/Preview.tsx @@ -0,0 +1,17 @@ +import type { ReactNode } from "react"; + +interface PreviewProps { + children: ReactNode; +} + +function Preview(props: Readonly): React.JSX.Element { + const { children } = props; + + return ( +
+ {children} +
+ ); +} + +export { Preview }; diff --git a/apps/docs/src/content/docs/components/button.mdx b/apps/docs/src/content/docs/components/button.mdx index 61141e4..298a1fb 100644 --- a/apps/docs/src/content/docs/components/button.mdx +++ b/apps/docs/src/content/docs/components/button.mdx @@ -3,6 +3,49 @@ title: Button description: An accessible button component built on React Aria with focus management, keyboard navigation, and screen reader support. --- + + ## Import ```tsx @@ -11,14 +54,35 @@ import { Button } from "@fried-ui/react/button"; ## Usage + + + + ```tsx - + ``` +## Design Anatomy + +Every spacing value is derived from the **golden ratio (φ = 1.618)** using font-size as the base unit `x`: + +| Property | Formula | sm (14px) | md (16px) | lg (20px) | xl (24px) | +| -------------- | --------- | --------- | ----------- | --------- | ---------- | +| Font | x | `text-sm` | `text-base` | `text-xl` | `text-2xl` | +| Padding Inline | x | 14px | 16px | 20px | 24px | +| Padding Block | x × 0.485 | 6px | 8px | 10px | 12px | +| Gap | x / φ | 8.65px | 9.89px | 12.36px | 14.83px | + +Radius and font-size are **independent** — size controls structure (spacing via φ), radius controls style (branding). + ## Sizes The `size` prop controls padding, gap, and font-size using golden ratio proportional scaling. + + + + ```tsx @@ -30,17 +94,21 @@ The `size` prop controls padding, gap, and font-size using golden ratio proporti The `radius` prop controls border-radius independently from size. + + + + ```tsx - - - - ``` ## Disabled + + + + ```tsx ``` @@ -49,10 +117,67 @@ The `radius` prop controls border-radius independently from size. Shows a loading spinner and disables press/hover events. + + + + ```tsx ``` +## With Icons + +Icons inside buttons use the `size-match-font` utility to lock their size to the font. Gap between icon and text is calculated via φ — so icons scale proportionally across all sizes. + + + + + +```tsx + +``` + +## As a Link + +Wrap a `Button` with an anchor or Next.js `Link` to render a button that navigates. + + + + + +```tsx +import Link from "next/link"; + + + +; +``` + +## Custom Class + +Use `className` to add layout or typography utilities. Component visual styles (background, color) are controlled by props. + + + + + +```tsx + +``` + +## Accessibility + +Built on [React Aria Button](https://react-spectrum.adobe.com/react-aria/Button.html) — accessible out of the box: + +- **Keyboard** — `Enter` and `Space` trigger press events +- **Focus management** — visible focus ring on keyboard navigation +- **Screen reader** — announces button role, label, disabled, and pending states +- **Touch** — optimized press handling with cancel on drag +- **Reduced motion** — respects `prefers-reduced-motion` + ## API Reference ### Button Props diff --git a/apps/storybook/.storybook/main.ts b/apps/storybook/.storybook/main.ts index 10a0b3d..261d743 100644 --- a/apps/storybook/.storybook/main.ts +++ b/apps/storybook/.storybook/main.ts @@ -1,10 +1,6 @@ -import path from "node:path"; - import type { StorybookConfig } from "@storybook/react-vite"; import tailwindcss from "@tailwindcss/vite"; -const reactPkg = path.resolve(import.meta.dirname, "../../../packages/react"); - const config: StorybookConfig = { stories: ["../../../packages/react/src/**/*.stories.@(ts|tsx)"], framework: "@storybook/react-vite", @@ -12,14 +8,6 @@ const config: StorybookConfig = { viteFinal(config) { config.plugins = [...(config.plugins || []), tailwindcss()]; - config.resolve = { - ...config.resolve, - alias: { - ...config.resolve?.alias, - src: path.join(reactPkg, "src"), - }, - }; - return config; }, diff --git a/apps/storybook/.storybook/styles.css b/apps/storybook/.storybook/styles.css index 4aa500b..2e1db47 100644 --- a/apps/storybook/.storybook/styles.css +++ b/apps/storybook/.storybook/styles.css @@ -1,2 +1,2 @@ +@import "tailwindcss"; @import "@fried-ui/styles"; -@source "../../../packages/react/src/**/*.{ts,tsx}"; diff --git a/packages/react/eslint.config.ts b/packages/react/eslint.config.ts index 02225ca..6482565 100644 --- a/packages/react/eslint.config.ts +++ b/packages/react/eslint.config.ts @@ -2,6 +2,12 @@ import { createReactConfig } from "@repo/quality/eslint/react-internal"; export default [ ...createReactConfig(import.meta.dirname), + { + files: ["src/**/*.{ts,tsx}"], + rules: { + "nextfriday/no-relative-imports": "off", + }, + }, { files: ["src/components/**/*.{ts,tsx}"], rules: { diff --git a/packages/react/package.json b/packages/react/package.json index 4bc6a38..6fc01aa 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -24,18 +24,18 @@ "type": "module", "exports": { ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.mjs" + "import": "./src/index.ts" }, "./button": { - "types": "./dist/components/button/index.d.ts", - "import": "./dist/components/button/index.mjs" + "import": "./src/components/button/index.ts" }, "./utils/cn": { - "types": "./dist/utils/cn/index.d.ts", - "import": "./dist/utils/cn/index.mjs" + "import": "./src/utils/cn/index.ts" } }, + "main": "./dist/index.mjs", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", "files": [ "dist" ], diff --git a/packages/react/src/components/button/Button.tsx b/packages/react/src/components/button/Button.tsx index a37eaa6..b564a18 100644 --- a/packages/react/src/components/button/Button.tsx +++ b/packages/react/src/components/button/Button.tsx @@ -4,8 +4,8 @@ import type { ReactNode } from "react"; import { Button as RACButton, type ButtonProps as RACButtonProps, composeRenderProps } from "react-aria-components"; -import { Spinner } from "src/components/icons"; -import { cn } from "src/utils/cn"; +import { Spinner } from "../icons"; +import { cn } from "../../utils/cn"; export type ButtonProps = { variant?: ButtonVariant; diff --git a/packages/react/src/components/icons/Spinner.tsx b/packages/react/src/components/icons/Spinner.tsx index 130115a..b972b6e 100644 --- a/packages/react/src/components/icons/Spinner.tsx +++ b/packages/react/src/components/icons/Spinner.tsx @@ -7,8 +7,8 @@ function Spinner(props: Readonly>) { return ( - - + + ); } diff --git a/packages/react/tsconfig.json b/packages/react/tsconfig.json index f3e8410..e8ec68d 100644 --- a/packages/react/tsconfig.json +++ b/packages/react/tsconfig.json @@ -3,10 +3,7 @@ "compilerOptions": { "rootDir": "src", "outDir": "dist", - "strictNullChecks": true, - "paths": { - "src/*": ["./src/*"] - } + "strictNullChecks": true }, "include": ["src"], "exclude": ["node_modules", "dist", "src/**/*.stories.tsx"] diff --git a/packages/styles/src/index.css b/packages/styles/src/index.css index c5edfbf..7b5ccf7 100644 --- a/packages/styles/src/index.css +++ b/packages/styles/src/index.css @@ -1,5 +1,3 @@ -@import "tailwindcss"; - @import "./tokens/index.css"; @import "./utilities/index.css"; @import "./components/index.css"; diff --git a/prettier.config.mjs b/prettier.config.mjs index ff4ab3c..76c3bd4 100644 --- a/prettier.config.mjs +++ b/prettier.config.mjs @@ -10,7 +10,7 @@ const config = { arrowParens: "always", endOfLine: "lf", plugins: ["prettier-plugin-tailwindcss"], - tailwindStylesheet: "packages/styles/src/index.css", + tailwindStylesheet: "apps/docs/src/app/globals.css", tailwindAttributes: ["className", "classNames"], tailwindFunctions: ["tv", "clsx", "cn"], }; From 6788ce483ec6cc8f9824052ee513f71e1c3820b8 Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Sat, 28 Mar 2026 02:25:21 +0700 Subject: [PATCH 010/199] feat(docs): Copy for LLM, fix dark mode, @layer base Add Copy for LLM button in right sidebar with MDX API. Fix dark mode: use .dark class + @layer base like HeroUI. Remove @media prefers-color-scheme. All tokens use fried-ui design system colors. --- apps/docs/source.config.ts | 5 + apps/docs/src/app/api/mdx/route.ts | 30 ++++ apps/docs/src/app/docs/[[...slug]]/page.tsx | 6 +- apps/docs/src/components/CopyMarkdown.tsx | 38 +++++ apps/docs/src/components/Preview.tsx | 2 +- .../src/content/docs/components/button.mdx | 40 ++--- packages/styles/src/tokens/colors.css | 159 +++++++++--------- 7 files changed, 181 insertions(+), 99 deletions(-) create mode 100644 apps/docs/src/app/api/mdx/route.ts create mode 100644 apps/docs/src/components/CopyMarkdown.tsx diff --git a/apps/docs/source.config.ts b/apps/docs/source.config.ts index 05dfc2a..c60c9fe 100644 --- a/apps/docs/source.config.ts +++ b/apps/docs/source.config.ts @@ -2,4 +2,9 @@ import { defineDocs } from "fumadocs-mdx/config"; export const docs = defineDocs({ dir: "src/content/docs", + docs: { + postprocess: { + includeProcessedMarkdown: true, + }, + }, }); diff --git a/apps/docs/src/app/api/mdx/route.ts b/apps/docs/src/app/api/mdx/route.ts new file mode 100644 index 0000000..4ce619a --- /dev/null +++ b/apps/docs/src/app/api/mdx/route.ts @@ -0,0 +1,30 @@ +import { type NextRequest, NextResponse } from "next/server"; + +import { source } from "@/lib/source"; + +async function GET(request: NextRequest): Promise { + const slug = request.nextUrl.searchParams.get("slug"); + + if (!slug) { + return NextResponse.json({ error: "slug required" }, { status: 400 }); + } + + const slugParts = slug.split("/").filter(Boolean); + const page = source.getPage(slugParts); + + if (!page) { + return NextResponse.json({ error: "not found" }, { status: 404 }); + } + + const text = await page.data.getText("processed"); + + const response = new NextResponse(text, { + headers: { + "Content-Type": "text/markdown; charset=utf-8", + }, + }); + + return response; +} + +export { GET }; diff --git a/apps/docs/src/app/docs/[[...slug]]/page.tsx b/apps/docs/src/app/docs/[[...slug]]/page.tsx index 09c9fda..968ea40 100644 --- a/apps/docs/src/app/docs/[[...slug]]/page.tsx +++ b/apps/docs/src/app/docs/[[...slug]]/page.tsx @@ -2,6 +2,7 @@ import defaultMdxComponents from "fumadocs-ui/mdx"; import { DocsBody, DocsDescription, DocsPage, DocsTitle } from "fumadocs-ui/page"; import { notFound } from "next/navigation"; +import { CopyMarkdown } from "@/components/CopyMarkdown"; import { ButtonAsLink, ButtonCustomClass, @@ -42,9 +43,12 @@ async function Page(props: Readonly): Promise { if (!page) notFound(); const MdxContent = page.data.body; + const slugPath = params.slug?.join("/") ?? ""; + const markdownUrl = `/api/mdx?slug=${slugPath}`; + const tocConfig = { header: }; return ( - + {page.data.title} {page.data.description} diff --git a/apps/docs/src/components/CopyMarkdown.tsx b/apps/docs/src/components/CopyMarkdown.tsx new file mode 100644 index 0000000..a688b3e --- /dev/null +++ b/apps/docs/src/components/CopyMarkdown.tsx @@ -0,0 +1,38 @@ +"use client"; + +import { useState } from "react"; + +interface CopyMarkdownProps { + url: string; +} + +function CopyMarkdown(props: Readonly): React.JSX.Element { + const { url } = props; + const [copied, setCopied] = useState(false); + + async function handleCopy(): Promise { + const res = await fetch(url); + const text = await res.text(); + + await navigator.clipboard.writeText(text); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } + + return ( + + ); +} + +export { CopyMarkdown }; diff --git a/apps/docs/src/components/Preview.tsx b/apps/docs/src/components/Preview.tsx index 7b15254..129a815 100644 --- a/apps/docs/src/components/Preview.tsx +++ b/apps/docs/src/components/Preview.tsx @@ -8,7 +8,7 @@ function Preview(props: Readonly): React.JSX.Element { const { children } = props; return ( -
+
{children}
); diff --git a/apps/docs/src/content/docs/components/button.mdx b/apps/docs/src/content/docs/components/button.mdx index 298a1fb..a0b9abc 100644 --- a/apps/docs/src/content/docs/components/button.mdx +++ b/apps/docs/src/content/docs/components/button.mdx @@ -59,22 +59,9 @@ import { Button } from "@fried-ui/react/button"; ```tsx - + ``` -## Design Anatomy - -Every spacing value is derived from the **golden ratio (φ = 1.618)** using font-size as the base unit `x`: - -| Property | Formula | sm (14px) | md (16px) | lg (20px) | xl (24px) | -| -------------- | --------- | --------- | ----------- | --------- | ---------- | -| Font | x | `text-sm` | `text-base` | `text-xl` | `text-2xl` | -| Padding Inline | x | 14px | 16px | 20px | 24px | -| Padding Block | x × 0.485 | 6px | 8px | 10px | 12px | -| Gap | x / φ | 8.65px | 9.89px | 12.36px | 14.83px | - -Radius and font-size are **independent** — size controls structure (spacing via φ), radius controls style (branding). - ## Sizes The `size` prop controls padding, gap, and font-size using golden ratio proportional scaling. @@ -168,15 +155,30 @@ Use `className` to add layout or typography utilities. Component visual styles ( ``` +## Design Anatomy + +Every spacing value is derived from the **golden ratio (φ = 1.618)** using font-size as the base unit `x`: + +| Property | Formula | sm (14px) | md (16px) | lg (20px) | xl (24px) | +| -------------- | --------- | --------- | ----------- | --------- | ---------- | +| Font | x | `text-sm` | `text-base` | `text-xl` | `text-2xl` | +| Padding Inline | x | 14px | 16px | 20px | 24px | +| Padding Block | x × 0.485 | 6px | 8px | 10px | 12px | +| Gap | x / φ | 8.65px | 9.89px | 12.36px | 14.83px | + +Radius and font-size are **independent** — size controls structure (spacing via φ), radius controls style (branding). + ## Accessibility Built on [React Aria Button](https://react-spectrum.adobe.com/react-aria/Button.html) — accessible out of the box: -- **Keyboard** — `Enter` and `Space` trigger press events -- **Focus management** — visible focus ring on keyboard navigation -- **Screen reader** — announces button role, label, disabled, and pending states -- **Touch** — optimized press handling with cancel on drag -- **Reduced motion** — respects `prefers-reduced-motion` +| Feature | Description | +| ---------------- | ---------------------------------------------------------- | +| Keyboard | `Enter` and `Space` trigger press events | +| Focus management | Visible focus ring on keyboard navigation | +| Screen reader | Announces button role, label, disabled, and pending states | +| Touch | Optimized press handling with cancel on drag | +| Reduced motion | Respects `prefers-reduced-motion` automatically | ## API Reference diff --git a/packages/styles/src/tokens/colors.css b/packages/styles/src/tokens/colors.css index 10ab194..3bd61a7 100644 --- a/packages/styles/src/tokens/colors.css +++ b/packages/styles/src/tokens/colors.css @@ -1,82 +1,85 @@ -@custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *)); - -@theme { - --color-primary: var(--color-branding-500); - --color-primary-hover: var(--color-branding-600); - --color-primary-active: var(--color-branding-700); - --color-primary-foreground: var(--color-neutral-950); - - --color-secondary: var(--color-neutral-100); - --color-secondary-hover: var(--color-neutral-200); - --color-secondary-active: var(--color-neutral-300); - --color-secondary-foreground: var(--color-neutral-900); - - --color-accent: var(--color-blue-600); - --color-accent-hover: var(--color-blue-700); - --color-accent-active: var(--color-blue-800); - --color-accent-foreground: var(--color-white); - - --color-success: var(--color-green-600); - --color-success-hover: var(--color-green-700); - --color-success-active: var(--color-green-800); - --color-success-foreground: var(--color-white); - - --color-warning: var(--color-amber-500); - --color-warning-hover: var(--color-amber-600); - --color-warning-active: var(--color-amber-700); - --color-warning-foreground: var(--color-neutral-950); - - --color-danger: var(--color-red-600); - --color-danger-hover: var(--color-red-700); - --color-danger-active: var(--color-red-800); - --color-danger-foreground: var(--color-white); - - --color-info: var(--color-sky-500); - --color-info-hover: var(--color-sky-600); - --color-info-active: var(--color-sky-700); - --color-info-foreground: var(--color-white); - - --color-background: var(--color-white); - --color-foreground: var(--color-neutral-950); - - --color-surface: var(--color-neutral-50); - --color-surface-hover: var(--color-neutral-100); - --color-surface-foreground: var(--color-neutral-900); - - --color-muted: var(--color-neutral-100); - --color-muted-hover: var(--color-neutral-200); - --color-muted-foreground: var(--color-neutral-500); - - --color-separator: var(--color-neutral-100); - --color-skeleton: var(--color-neutral-200); - - --color-disabled: var(--color-neutral-100); - --color-disabled-foreground: var(--color-neutral-400); - - --color-heading: var(--color-neutral-950); - --color-code: var(--color-neutral-800); - --color-code-background: var(--color-neutral-100); - --color-mark: var(--color-amber-200); - --color-mark-foreground: var(--color-neutral-950); - - --color-border: var(--color-neutral-200); - --color-border-hover: var(--color-neutral-300); - - --color-field: var(--color-white); - --color-field-hover: var(--color-neutral-50); - --color-field-foreground: var(--color-neutral-950); - --color-field-border: var(--color-neutral-300); - --color-field-border-hover: var(--color-neutral-400); - --color-field-placeholder: var(--color-neutral-400); - - --color-link: var(--color-neutral-950); - --color-link-hover: var(--color-neutral-600); - --color-ring: var(--color-neutral-950); - --color-overlay: oklch(0% 0 0 / 50%); -} +@custom-variant dark (&:is(.dark, [data-theme="dark"]) *); + +@layer base { + :root, + .light, + [data-theme="light"] { + --color-primary: var(--color-branding-500); + --color-primary-hover: var(--color-branding-600); + --color-primary-active: var(--color-branding-700); + --color-primary-foreground: var(--color-neutral-950); + + --color-secondary: var(--color-neutral-100); + --color-secondary-hover: var(--color-neutral-200); + --color-secondary-active: var(--color-neutral-300); + --color-secondary-foreground: var(--color-neutral-900); + + --color-accent: var(--color-blue-600); + --color-accent-hover: var(--color-blue-700); + --color-accent-active: var(--color-blue-800); + --color-accent-foreground: var(--color-white); + + --color-success: var(--color-green-600); + --color-success-hover: var(--color-green-700); + --color-success-active: var(--color-green-800); + --color-success-foreground: var(--color-white); + + --color-warning: var(--color-amber-500); + --color-warning-hover: var(--color-amber-600); + --color-warning-active: var(--color-amber-700); + --color-warning-foreground: var(--color-neutral-950); + + --color-danger: var(--color-red-600); + --color-danger-hover: var(--color-red-700); + --color-danger-active: var(--color-red-800); + --color-danger-foreground: var(--color-white); + + --color-info: var(--color-sky-500); + --color-info-hover: var(--color-sky-600); + --color-info-active: var(--color-sky-700); + --color-info-foreground: var(--color-white); + + --color-background: var(--color-white); + --color-foreground: var(--color-neutral-950); + + --color-surface: var(--color-neutral-50); + --color-surface-hover: var(--color-neutral-100); + --color-surface-foreground: var(--color-neutral-900); + + --color-muted: var(--color-neutral-100); + --color-muted-hover: var(--color-neutral-200); + --color-muted-foreground: var(--color-neutral-500); + + --color-separator: var(--color-neutral-100); + --color-skeleton: var(--color-neutral-200); + + --color-disabled: var(--color-neutral-100); + --color-disabled-foreground: var(--color-neutral-400); + + --color-heading: var(--color-neutral-950); + --color-code: var(--color-neutral-800); + --color-code-background: var(--color-neutral-100); + --color-mark: var(--color-amber-200); + --color-mark-foreground: var(--color-neutral-950); + + --color-border: var(--color-neutral-200); + --color-border-hover: var(--color-neutral-300); + + --color-field: var(--color-white); + --color-field-hover: var(--color-neutral-50); + --color-field-foreground: var(--color-neutral-950); + --color-field-border: var(--color-neutral-300); + --color-field-border-hover: var(--color-neutral-400); + --color-field-placeholder: var(--color-neutral-400); + + --color-link: var(--color-neutral-950); + --color-link-hover: var(--color-neutral-600); + --color-ring: var(--color-neutral-950); + --color-overlay: oklch(0% 0 0 / 50%); + } -@media (prefers-color-scheme: dark) { - :root:not([data-theme="light"]) { + .dark, + [data-theme="dark"] { --color-primary: var(--color-branding-500); --color-primary-hover: var(--color-branding-600); --color-primary-active: var(--color-branding-700); From 76168f40d8d0a669981af75b9330412d6b6e7522 Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Sat, 28 Mar 2026 03:16:33 +0700 Subject: [PATCH 011/199] docs(docs): Add LLM docs, symmetric rules/agents, CSS classes --- apps/docs/public/llm/button.md | 216 ++++++++++++++++++ apps/docs/public/llms.txt | 2 + apps/docs/src/app/api/mdx/route.ts | 22 +- .../src/content/docs/components/button.mdx | 108 ++++++--- 4 files changed, 314 insertions(+), 34 deletions(-) create mode 100644 apps/docs/public/llm/button.md diff --git a/apps/docs/public/llm/button.md b/apps/docs/public/llm/button.md new file mode 100644 index 0000000..c14df92 --- /dev/null +++ b/apps/docs/public/llm/button.md @@ -0,0 +1,216 @@ +# Button + +Accessible button component. Built on React Aria Button. + +## Install + +```bash +pnpm add @fried-ui/react +``` + +## Import + +```tsx +import { Button } from "@fried-ui/react/button"; +``` + +## Props + +| Prop | Type | Default | Description | +| ------------------ | -------------------------------------------------- | ----------- | --------------------------------------- | +| `variant` | `"primary"` | `"primary"` | Visual variant | +| `size` | `"sm" \| "md" \| "lg" \| "xl"` | `"md"` | Size — controls padding, gap, font-size | +| `radius` | `"none" \| "sm" \| "md" \| "lg" \| "xl" \| "full"` | `"md"` | Border radius (independent from size) | +| `isDisabled` | `boolean` | `false` | Disabled state | +| `isPending` | `boolean` | `false` | Loading spinner, disables interaction | +| `onPress` | `(e: PressEvent) => void` | - | Press handler (preferred over onClick) | +| `onPressStart` | `(e: PressEvent) => void` | - | Press start handler | +| `onPressEnd` | `(e: PressEvent) => void` | - | Press end handler | +| `onPressChange` | `(isPressed: boolean) => void` | - | Press state change handler | +| `onHoverStart` | `(e: HoverEvent) => void` | - | Hover start handler | +| `onHoverEnd` | `(e: HoverEvent) => void` | - | Hover end handler | +| `onHoverChange` | `(isHovering: boolean) => void` | - | Hover state change handler | +| `onFocus` | `(e: FocusEvent) => void` | - | Focus handler | +| `onBlur` | `(e: FocusEvent) => void` | - | Blur handler | +| `onFocusChange` | `(isFocused: boolean) => void` | - | Focus state change handler | +| `onKeyDown` | `(e: KeyboardEvent) => void` | - | Key down handler | +| `onKeyUp` | `(e: KeyboardEvent) => void` | - | Key up handler | +| `type` | `"button" \| "submit" \| "reset"` | `"button"` | HTML button type | +| `form` | `string` | - | Associated form ID | +| `formAction` | `string` | - | Form submission URL | +| `autoFocus` | `boolean` | `false` | Auto focus on mount | +| `aria-label` | `string` | - | Accessible label | +| `aria-labelledby` | `string` | - | ID of labelling element | +| `aria-describedby` | `string` | - | ID of describing element | +| `className` | `string` | - | Additional CSS classes | +| `children` | `ReactNode \| (renderProps) => ReactNode` | - | Button content | + +## Data Attributes + +| Attribute | Description | +| ---------------------- | -------------------- | +| `[data-hovered]` | Mouse hover | +| `[data-pressed]` | Active/pressed | +| `[data-focused]` | Focused (any method) | +| `[data-focus-visible]` | Keyboard focus only | +| `[data-disabled]` | Disabled | +| `[data-pending]` | Loading/pending | + +## CSS Classes + +| Class | Description | +| ------------------------- | ------------------------- | +| `fri-button` | Base styles | +| `fri-button--primary` | Primary variant | +| `fri-button--sm` | Small size | +| `fri-button--md` | Medium size | +| `fri-button--lg` | Large size | +| `fri-button--xl` | Extra large size | +| `fri-button--radius-none` | No border radius | +| `fri-button--radius-sm` | Small border radius | +| `fri-button--radius-md` | Medium border radius | +| `fri-button--radius-lg` | Large border radius | +| `fri-button--radius-xl` | Extra large border radius | +| `fri-button--radius-full` | Full (pill) border radius | +| `fri-button--pending` | Pending/loading state | +| `fri-button__spinner` | Internal spinner element | + +## CSS Variables + +| Variable | Default | Description | +| ------------------------- | ---------------------------- | ------------------ | +| `--fri-button-bg` | `transparent` | Background | +| `--fri-button-bg-hover` | `var(--fri-button-bg)` | Hover background | +| `--fri-button-bg-pressed` | `var(--fri-button-bg-hover)` | Pressed background | +| `--fri-button-fg` | `currentColor` | Text/icon color | + +## Examples + +### Basic + +Default button with medium size and medium radius. + +```tsx + +``` + +### Sizes + +Golden ratio proportional scaling across all sizes. + +```tsx + + + + +``` + +### Radius + +Border radius independent from size. + +```tsx + + +``` + +### With Icon + +Icons use `size-match-font` to match the font size. Gap scales via golden ratio. + +```tsx + +``` + +### Disabled + +Prevents interaction and applies disabled styling. + +```tsx + +``` + +### Pending + +Text becomes transparent, spinner appears centered. All interaction is blocked. + +```tsx + +``` + +### As a Link + +Wrap with anchor or framework Link component. + +```tsx + + + +``` + +### Custom Class + +Use `className` for layout or typography utilities. + +```tsx + +``` + +### Form Submit + +Associate with a form element via `type` and `form` props. + +```tsx + +``` + +### Press Handler + +Use `onPress` instead of `onClick` — handles keyboard, touch, and mouse uniformly. + +```tsx + +``` + +## Anatomy + +``` +┌─ Button ──────────────────────────┐ +│ [Icon] Label [Spinner overlay] │ +└───────────────────────────────────┘ +``` + +- `children`: Icon + label as direct children +- Spinner: Rendered internally when `isPending={true}`, absolutely positioned over content +- Icon sizing: Use `className="size-match-font"` on SVG icons + +## Spacing Math + +| | sm (14px) | md (16px) | lg (20px) | xl (24px) | +| -------------- | --------- | --------- | --------- | --------- | +| Font | 14px | 16px | 20px | 24px | +| Padding Inline | 14px | 16px | 20px | 24px | +| Padding Block | 6px | 8px | 10px | 12px | +| Gap | 8.65px | 9.89px | 12.36px | 14.83px | + +Formulas: `padding-inline = x`, `padding-block = x × 0.485`, `gap = x / φ`. + +## Accessibility + +- Keyboard: `Enter` and `Space` trigger press +- Focus ring on keyboard navigation (not mouse) +- Screen reader: announces role, label, disabled, pending states +- Touch: optimized press with cancel on drag +- Respects `prefers-reduced-motion` + +## Constraints + +- Only `"primary"` variant currently available +- `radius` is independent from `size` — they do not affect each other +- `isPending` makes text transparent and shows spinner; content stays in DOM for layout +- Uses BEM classes (`fri-button`, `fri-button--primary`, etc.) — no inline Tailwind in component diff --git a/apps/docs/public/llms.txt b/apps/docs/public/llms.txt index 83bbff6..a57005b 100644 --- a/apps/docs/public/llms.txt +++ b/apps/docs/public/llms.txt @@ -4,4 +4,6 @@ ## Docs +- [Button](/llm/button.md): Accessible button with golden ratio spacing, React Aria keyboard/focus/screen reader support + ## Optional diff --git a/apps/docs/src/app/api/mdx/route.ts b/apps/docs/src/app/api/mdx/route.ts index 4ce619a..55d34aa 100644 --- a/apps/docs/src/app/api/mdx/route.ts +++ b/apps/docs/src/app/api/mdx/route.ts @@ -1,6 +1,7 @@ -import { type NextRequest, NextResponse } from "next/server"; +import { readFile } from "node:fs/promises"; +import { join } from "node:path"; -import { source } from "@/lib/source"; +import { type NextRequest, NextResponse } from "next/server"; async function GET(request: NextRequest): Promise { const slug = request.nextUrl.searchParams.get("slug"); @@ -9,14 +10,21 @@ async function GET(request: NextRequest): Promise { return NextResponse.json({ error: "slug required" }, { status: 400 }); } - const slugParts = slug.split("/").filter(Boolean); - const page = source.getPage(slugParts); + const lastSegment = slug.split("/").filter(Boolean).pop(); - if (!page) { - return NextResponse.json({ error: "not found" }, { status: 404 }); + if (!lastSegment) { + return NextResponse.json({ error: "invalid slug" }, { status: 400 }); } - const text = await page.data.getText("processed"); + const filePath = join(process.cwd(), "public", "llm", `${lastSegment}.md`); + + let text: string; + + try { + text = await readFile(filePath, "utf-8"); + } catch { + return NextResponse.json({ error: "not found" }, { status: 404 }); + } const response = new NextResponse(text, { headers: { diff --git a/apps/docs/src/content/docs/components/button.mdx b/apps/docs/src/content/docs/components/button.mdx index a0b9abc..09ecd72 100644 --- a/apps/docs/src/content/docs/components/button.mdx +++ b/apps/docs/src/content/docs/components/button.mdx @@ -7,7 +7,7 @@ description: An accessible button component built on React Aria with focus manag @@ -17,7 +17,7 @@ description: An accessible button component built on React Aria with focus manag @@ -27,7 +27,7 @@ description: An accessible button component built on React Aria with focus manag @@ -37,7 +37,7 @@ description: An accessible button component built on React Aria with focus manag @@ -62,6 +62,26 @@ import { Button } from "@fried-ui/react/button"; ``` +## Features + +- Golden ratio (φ) proportional spacing across all sizes +- Accessible via React Aria — keyboard, focus, screen reader built-in +- Supports loading (pending) and disabled states +- Zero JS runtime styling — pure CSS with BEM +- Independent `radius` prop — style separated from structure +- `prefers-reduced-motion` respected automatically + +## Anatomy + +Button accepts `children` directly. Icons use `size-match-font` to lock size to the font. Gap between icon and label is controlled by golden ratio. + +```tsx + +``` + ## Sizes The `size` prop controls padding, gap, and font-size using golden ratio proportional scaling. @@ -87,31 +107,13 @@ The `radius` prop controls border-radius independently from size. ```tsx + + + + ``` -## Disabled - - - - - -```tsx - -``` - -## Pending - -Shows a loading spinner and disables press/hover events. - - - - - -```tsx - -``` - ## With Icons Icons inside buttons use the `size-match-font` utility to lock their size to the font. Gap between icon and text is calculated via φ — so icons scale proportionally across all sizes. @@ -143,6 +145,30 @@ import Link from "next/link"; ; ``` +## Disabled + +Prevents interaction and applies disabled styling. + + + + + +```tsx + +``` + +## Pending + +Shows a loading spinner and disables press/hover events. + + + + + +```tsx + +``` + ## Custom Class Use `className` to add layout or typography utilities. Component visual styles (background, color) are controlled by props. @@ -213,7 +239,7 @@ Built on [React Aria Button](https://react-spectrum.adobe.com/react-aria/Button. | `className` | `string` | - | Additional CSS classes | | `children` | `ReactNode \| (renderProps) => ReactNode` | - | Button content | -### Render Props (CSS Selectors) +### Render Props | Render Prop | CSS Selector | Description | | ---------------- | ---------------------- | --------------------------- | @@ -223,3 +249,31 @@ Built on [React Aria Button](https://react-spectrum.adobe.com/react-aria/Button. | `isFocusVisible` | `[data-focus-visible]` | Keyboard focused only | | `isDisabled` | `[data-disabled]` | Disabled state | | `isPending` | `[data-pending]` | Pending/loading state | + +### CSS Classes + +| Class | Description | +| ------------------------- | ------------------------- | +| `fri-button` | Base styles | +| `fri-button--primary` | Primary variant | +| `fri-button--sm` | Small size | +| `fri-button--md` | Medium size | +| `fri-button--lg` | Large size | +| `fri-button--xl` | Extra large size | +| `fri-button--radius-none` | No border radius | +| `fri-button--radius-sm` | Small border radius | +| `fri-button--radius-md` | Medium border radius | +| `fri-button--radius-lg` | Large border radius | +| `fri-button--radius-xl` | Extra large border radius | +| `fri-button--radius-full` | Full (pill) border radius | +| `fri-button--pending` | Pending/loading state | +| `fri-button__spinner` | Internal spinner element | + +### CSS Variables + +| CSS Variable | Description | Default | +| ------------------------- | ------------------------- | ---------------------------- | +| `--fri-button-bg` | Background color | `transparent` | +| `--fri-button-bg-hover` | Hover background | `var(--fri-button-bg)` | +| `--fri-button-bg-pressed` | Active/pressed background | `var(--fri-button-bg-hover)` | +| `--fri-button-fg` | Text and icon color | `currentColor` | From 02a7846c81e30078b72eb169a1b96c90c6803203 Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Sat, 28 Mar 2026 03:33:16 +0700 Subject: [PATCH 012/199] refactor(styles): Use rem, audit symmetry --- apps/docs/public/llm/button.md | 12 +++++------ apps/docs/src/components/CopyMarkdown.tsx | 2 +- .../src/content/docs/components/button.mdx | 20 +++++++++---------- packages/styles/src/components/button.css | 8 ++++---- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/apps/docs/public/llm/button.md b/apps/docs/public/llm/button.md index c14df92..e52fc55 100644 --- a/apps/docs/public/llm/button.md +++ b/apps/docs/public/llm/button.md @@ -191,12 +191,12 @@ Use `onPress` instead of `onClick` — handles keyboard, touch, and mouse unifor ## Spacing Math -| | sm (14px) | md (16px) | lg (20px) | xl (24px) | -| -------------- | --------- | --------- | --------- | --------- | -| Font | 14px | 16px | 20px | 24px | -| Padding Inline | 14px | 16px | 20px | 24px | -| Padding Block | 6px | 8px | 10px | 12px | -| Gap | 8.65px | 9.89px | 12.36px | 14.83px | +| | sm | md | lg | xl | +| -------------- | -------- | -------- | -------- | -------- | +| Font | 0.875rem | 1rem | 1.25rem | 1.5rem | +| Padding Inline | 0.875rem | 1rem | 1.25rem | 1.5rem | +| Padding Block | 0.375rem | 0.5rem | 0.625rem | 0.75rem | +| Gap | 0.541rem | 0.618rem | 0.773rem | 0.927rem | Formulas: `padding-inline = x`, `padding-block = x × 0.485`, `gap = x / φ`. diff --git a/apps/docs/src/components/CopyMarkdown.tsx b/apps/docs/src/components/CopyMarkdown.tsx index a688b3e..7d90c11 100644 --- a/apps/docs/src/components/CopyMarkdown.tsx +++ b/apps/docs/src/components/CopyMarkdown.tsx @@ -21,7 +21,7 @@ function CopyMarkdown(props: Readonly): React.JSX.Element { return (
+ + + + + + + + +
+ ); +} + function ButtonSizes(): React.JSX.Element { return (
@@ -85,5 +100,6 @@ export { ButtonPending, ButtonRadii, ButtonSizes, + ButtonVariants, ButtonWithIcons, }; diff --git a/apps/docs/src/content/docs/components/button.mdx b/apps/docs/src/content/docs/components/button.mdx index bb0cf8e..f1ef586 100644 --- a/apps/docs/src/content/docs/components/button.mdx +++ b/apps/docs/src/content/docs/components/button.mdx @@ -7,7 +7,7 @@ description: An accessible button component built on React Aria with focus manag @@ -17,7 +17,7 @@ description: An accessible button component built on React Aria with focus manag @@ -27,7 +27,7 @@ description: An accessible button component built on React Aria with focus manag @@ -37,7 +37,7 @@ description: An accessible button component built on React Aria with focus manag @@ -82,6 +82,25 @@ Button accepts `children` directly. Icons use `size-match-font` to lock size to ``` +## Variants + +The `variant` prop controls the visual style using semantic color tokens. + + + + + +```tsx + + + + + + + + +``` + ## Sizes The `size` prop controls padding, gap, and font-size using golden ratio proportional scaling. @@ -210,34 +229,34 @@ Built on [React Aria Button](https://react-spectrum.adobe.com/react-aria/Button. ### Button Props -| Prop | Type | Default | Description | -| ------------------ | -------------------------------------------------- | ----------- | ------------------------------------------ | -| `variant` | `"primary"` | `"primary"` | The visual variant of the button | -| `size` | `"sm" \| "md" \| "lg" \| "xl"` | `"md"` | The size of the button | -| `radius` | `"none" \| "sm" \| "md" \| "lg" \| "xl" \| "full"` | `"md"` | The border radius of the button | -| `isDisabled` | `boolean` | `false` | Whether the button is disabled | -| `isPending` | `boolean` | `false` | Whether the button shows a loading spinner | -| `onPress` | `(e: PressEvent) => void` | - | Handler called when the button is pressed | -| `onPressStart` | `(e: PressEvent) => void` | - | Handler called when press starts | -| `onPressEnd` | `(e: PressEvent) => void` | - | Handler called when press ends | -| `onPressChange` | `(isPressed: boolean) => void` | - | Handler called when press state changes | -| `onHoverStart` | `(e: HoverEvent) => void` | - | Handler called when hover starts | -| `onHoverEnd` | `(e: HoverEvent) => void` | - | Handler called when hover ends | -| `onHoverChange` | `(isHovering: boolean) => void` | - | Handler called when hover state changes | -| `onFocus` | `(e: FocusEvent) => void` | - | Handler called when focused | -| `onBlur` | `(e: FocusEvent) => void` | - | Handler called when blurred | -| `onFocusChange` | `(isFocused: boolean) => void` | - | Handler called when focus state changes | -| `onKeyDown` | `(e: KeyboardEvent) => void` | - | Handler called on key down | -| `onKeyUp` | `(e: KeyboardEvent) => void` | - | Handler called on key up | -| `type` | `"button" \| "submit" \| "reset"` | `"button"` | The button type attribute | -| `form` | `string` | - | The form element to associate with | -| `formAction` | `string` | - | The URL for form submission | -| `autoFocus` | `boolean` | `false` | Whether to auto focus on mount | -| `aria-label` | `string` | - | Accessible label | -| `aria-labelledby` | `string` | - | ID of the labelling element | -| `aria-describedby` | `string` | - | ID of the describing element | -| `className` | `string` | - | Additional CSS classes | -| `children` | `ReactNode \| (renderProps) => ReactNode` | - | Button content | +| Prop | Type | Default | Description | +| ------------------ | -------------------------------------------------------------------------------------------------- | ----------- | ------------------------------------------ | +| `variant` | `"primary" \| "secondary" \| "ghost" \| "outline" \| "success" \| "warning" \| "danger" \| "info"` | `"primary"` | The visual variant of the button | +| `size` | `"sm" \| "md" \| "lg" \| "xl"` | `"md"` | The size of the button | +| `radius` | `"none" \| "sm" \| "md" \| "lg" \| "xl" \| "full"` | `"md"` | The border radius of the button | +| `isDisabled` | `boolean` | `false` | Whether the button is disabled | +| `isPending` | `boolean` | `false` | Whether the button shows a loading spinner | +| `onPress` | `(e: PressEvent) => void` | - | Handler called when the button is pressed | +| `onPressStart` | `(e: PressEvent) => void` | - | Handler called when press starts | +| `onPressEnd` | `(e: PressEvent) => void` | - | Handler called when press ends | +| `onPressChange` | `(isPressed: boolean) => void` | - | Handler called when press state changes | +| `onHoverStart` | `(e: HoverEvent) => void` | - | Handler called when hover starts | +| `onHoverEnd` | `(e: HoverEvent) => void` | - | Handler called when hover ends | +| `onHoverChange` | `(isHovering: boolean) => void` | - | Handler called when hover state changes | +| `onFocus` | `(e: FocusEvent) => void` | - | Handler called when focused | +| `onBlur` | `(e: FocusEvent) => void` | - | Handler called when blurred | +| `onFocusChange` | `(isFocused: boolean) => void` | - | Handler called when focus state changes | +| `onKeyDown` | `(e: KeyboardEvent) => void` | - | Handler called on key down | +| `onKeyUp` | `(e: KeyboardEvent) => void` | - | Handler called on key up | +| `type` | `"button" \| "submit" \| "reset"` | `"button"` | The button type attribute | +| `form` | `string` | - | The form element to associate with | +| `formAction` | `string` | - | The URL for form submission | +| `autoFocus` | `boolean` | `false` | Whether to auto focus on mount | +| `aria-label` | `string` | - | Accessible label | +| `aria-labelledby` | `string` | - | ID of the labelling element | +| `aria-describedby` | `string` | - | ID of the describing element | +| `className` | `string` | - | Additional CSS classes | +| `children` | `ReactNode \| (renderProps) => ReactNode` | - | Button content | ### Render Props @@ -256,6 +275,13 @@ Built on [React Aria Button](https://react-spectrum.adobe.com/react-aria/Button. | ------------------------- | ------------------------- | | `fri-button` | Base styles | | `fri-button--primary` | Primary variant | +| `fri-button--secondary` | Secondary variant | +| `fri-button--ghost` | Ghost variant | +| `fri-button--outline` | Outline variant | +| `fri-button--success` | Success variant | +| `fri-button--warning` | Warning variant | +| `fri-button--danger` | Danger variant | +| `fri-button--info` | Info variant | | `fri-button--sm` | Small size | | `fri-button--md` | Medium size | | `fri-button--lg` | Large size | diff --git a/packages/react/src/components/button/Button.tsx b/packages/react/src/components/button/Button.tsx index b564a18..39f919b 100644 --- a/packages/react/src/components/button/Button.tsx +++ b/packages/react/src/components/button/Button.tsx @@ -15,7 +15,7 @@ export type ButtonProps = { children?: ReactNode | ((renderProps: { isPending: boolean }) => ReactNode); } & Omit; -type ButtonVariant = "primary"; +type ButtonVariant = "primary" | "secondary" | "ghost" | "outline" | "success" | "warning" | "danger" | "info"; type ButtonSize = "sm" | "md" | "lg" | "xl"; type ButtonRadius = "none" | "sm" | "md" | "lg" | "xl" | "full"; diff --git a/packages/react/src/components/button/button.stories.tsx b/packages/react/src/components/button/button.stories.tsx index 70f1e15..d460473 100644 --- a/packages/react/src/components/button/button.stories.tsx +++ b/packages/react/src/components/button/button.stories.tsx @@ -34,11 +34,11 @@ const meta = { }, variant: { control: "select", - options: ["primary"], + options: ["primary", "secondary", "ghost", "outline", "success", "warning", "danger", "info"], description: "The visual variant of the button", table: { type: { - summary: '"primary"', + summary: '"primary" | "secondary" | "ghost" | "outline" | "success" | "warning" | "danger" | "info"', }, defaultValue: { summary: "primary", @@ -151,6 +151,21 @@ const Radii: Story = { ), }; +const Variants: Story = { + render: () => ( +
+ + + + + + + + +
+ ), +}; + const Disabled: Story = { args: { children: "Disabled", @@ -165,6 +180,6 @@ const Pending: Story = { }, }; -export { Default, Sizes, Radii, Disabled, Pending }; +export { Default, Variants, Sizes, Radii, Disabled, Pending }; export default meta; diff --git a/packages/styles/src/components/button.css b/packages/styles/src/components/button.css index 4833dca..659d4a9 100644 --- a/packages/styles/src/components/button.css +++ b/packages/styles/src/components/button.css @@ -45,6 +45,57 @@ --fri-button-fg: var(--color-primary-foreground); } +.fri-button--secondary { + --fri-button-bg: var(--color-secondary); + --fri-button-bg-hover: var(--color-secondary-hover); + --fri-button-bg-pressed: var(--color-secondary-active); + --fri-button-fg: var(--color-secondary-foreground); +} + +.fri-button--ghost { + --fri-button-bg: transparent; + --fri-button-bg-hover: var(--color-muted); + --fri-button-bg-pressed: var(--color-muted-hover); + --fri-button-fg: var(--color-foreground); +} + +.fri-button--outline { + --fri-button-bg: transparent; + --fri-button-bg-hover: var(--color-surface-hover); + --fri-button-bg-pressed: var(--color-surface-active); + --fri-button-fg: var(--color-foreground); + + @apply border-border; +} + +.fri-button--success { + --fri-button-bg: var(--color-success); + --fri-button-bg-hover: var(--color-success-hover); + --fri-button-bg-pressed: var(--color-success-active); + --fri-button-fg: var(--color-success-foreground); +} + +.fri-button--warning { + --fri-button-bg: var(--color-warning); + --fri-button-bg-hover: var(--color-warning-hover); + --fri-button-bg-pressed: var(--color-warning-active); + --fri-button-fg: var(--color-warning-foreground); +} + +.fri-button--danger { + --fri-button-bg: var(--color-danger); + --fri-button-bg-hover: var(--color-danger-hover); + --fri-button-bg-pressed: var(--color-danger-active); + --fri-button-fg: var(--color-danger-foreground); +} + +.fri-button--info { + --fri-button-bg: var(--color-info); + --fri-button-bg-hover: var(--color-info-hover); + --fri-button-bg-pressed: var(--color-info-active); + --fri-button-fg: var(--color-info-foreground); +} + .fri-button--sm { @apply gap-[0.541rem] px-3.5 py-1.5 text-sm; } diff --git a/packages/styles/src/tokens/colors.css b/packages/styles/src/tokens/colors.css index 3bd61a7..25877bb 100644 --- a/packages/styles/src/tokens/colors.css +++ b/packages/styles/src/tokens/colors.css @@ -1,5 +1,77 @@ @custom-variant dark (&:is(.dark, [data-theme="dark"]) *); +@theme { + --color-primary: var(--color-branding-500); + --color-primary-hover: var(--color-branding-600); + --color-primary-active: var(--color-branding-700); + --color-primary-foreground: var(--color-neutral-950); + + --color-secondary: var(--color-neutral-100); + --color-secondary-hover: var(--color-neutral-200); + --color-secondary-active: var(--color-neutral-300); + --color-secondary-foreground: var(--color-neutral-900); + + --color-success: var(--color-green-600); + --color-success-hover: var(--color-green-700); + --color-success-active: var(--color-green-800); + --color-success-foreground: var(--color-white); + + --color-warning: var(--color-amber-500); + --color-warning-hover: var(--color-amber-600); + --color-warning-active: var(--color-amber-700); + --color-warning-foreground: var(--color-neutral-950); + + --color-danger: var(--color-red-600); + --color-danger-hover: var(--color-red-700); + --color-danger-active: var(--color-red-800); + --color-danger-foreground: var(--color-white); + + --color-info: var(--color-sky-500); + --color-info-hover: var(--color-sky-600); + --color-info-active: var(--color-sky-700); + --color-info-foreground: var(--color-white); + + --color-background: var(--color-white); + --color-foreground: var(--color-neutral-950); + + --color-surface: var(--color-neutral-50); + --color-surface-hover: var(--color-neutral-100); + --color-surface-active: var(--color-neutral-200); + --color-surface-foreground: var(--color-neutral-900); + + --color-muted: var(--color-neutral-100); + --color-muted-hover: var(--color-neutral-200); + --color-muted-active: var(--color-neutral-300); + --color-muted-foreground: var(--color-neutral-500); + + --color-separator: var(--color-neutral-100); + --color-skeleton: var(--color-neutral-200); + + --color-disabled: var(--color-neutral-100); + --color-disabled-foreground: var(--color-neutral-400); + + --color-heading: var(--color-neutral-950); + --color-code: var(--color-neutral-800); + --color-code-background: var(--color-neutral-100); + --color-mark: var(--color-amber-200); + --color-mark-foreground: var(--color-neutral-950); + + --color-border: var(--color-neutral-200); + --color-border-hover: var(--color-neutral-300); + + --color-field: var(--color-white); + --color-field-hover: var(--color-neutral-50); + --color-field-foreground: var(--color-neutral-950); + --color-field-border: var(--color-neutral-300); + --color-field-border-hover: var(--color-neutral-400); + --color-field-placeholder: var(--color-neutral-400); + + --color-link: var(--color-neutral-950); + --color-link-hover: var(--color-neutral-600); + --color-ring: var(--color-neutral-950); + --color-overlay: oklch(0% 0 0 / 50%); +} + @layer base { :root, .light, @@ -14,11 +86,6 @@ --color-secondary-active: var(--color-neutral-300); --color-secondary-foreground: var(--color-neutral-900); - --color-accent: var(--color-blue-600); - --color-accent-hover: var(--color-blue-700); - --color-accent-active: var(--color-blue-800); - --color-accent-foreground: var(--color-white); - --color-success: var(--color-green-600); --color-success-hover: var(--color-green-700); --color-success-active: var(--color-green-800); @@ -44,10 +111,12 @@ --color-surface: var(--color-neutral-50); --color-surface-hover: var(--color-neutral-100); + --color-surface-active: var(--color-neutral-200); --color-surface-foreground: var(--color-neutral-900); --color-muted: var(--color-neutral-100); --color-muted-hover: var(--color-neutral-200); + --color-muted-active: var(--color-neutral-300); --color-muted-foreground: var(--color-neutral-500); --color-separator: var(--color-neutral-100); @@ -84,55 +153,67 @@ --color-primary-hover: var(--color-branding-600); --color-primary-active: var(--color-branding-700); --color-primary-foreground: var(--color-neutral-950); + --color-secondary: var(--color-neutral-800); --color-secondary-hover: var(--color-neutral-900); --color-secondary-active: var(--color-neutral-950); --color-secondary-foreground: var(--color-neutral-100); - --color-accent: var(--color-blue-500); - --color-accent-hover: var(--color-blue-600); - --color-accent-active: var(--color-blue-700); - --color-accent-foreground: var(--color-white); + --color-success: var(--color-green-500); --color-success-hover: var(--color-green-600); --color-success-active: var(--color-green-700); --color-success-foreground: var(--color-white); + --color-warning: var(--color-amber-400); --color-warning-hover: var(--color-amber-500); --color-warning-active: var(--color-amber-600); --color-warning-foreground: var(--color-neutral-950); + --color-danger: var(--color-red-500); --color-danger-hover: var(--color-red-600); --color-danger-active: var(--color-red-700); --color-danger-foreground: var(--color-white); + --color-info: var(--color-sky-400); --color-info-hover: var(--color-sky-500); --color-info-active: var(--color-sky-600); --color-info-foreground: var(--color-white); + --color-background: var(--color-neutral-950); --color-foreground: var(--color-neutral-50); + --color-surface: var(--color-neutral-900); --color-surface-hover: var(--color-neutral-800); + --color-surface-active: var(--color-neutral-700); --color-surface-foreground: var(--color-neutral-50); + --color-muted: var(--color-neutral-800); --color-muted-hover: var(--color-neutral-700); + --color-muted-active: var(--color-neutral-600); --color-muted-foreground: var(--color-neutral-400); + --color-separator: var(--color-neutral-800); --color-skeleton: var(--color-neutral-700); + --color-disabled: var(--color-neutral-800); --color-disabled-foreground: var(--color-neutral-500); + --color-heading: var(--color-neutral-50); --color-code: var(--color-neutral-200); --color-code-background: var(--color-neutral-800); --color-mark: var(--color-amber-800); --color-mark-foreground: var(--color-amber-100); - --color-border: var(--color-neutral-800); - --color-border-hover: var(--color-neutral-700); + + --color-border: var(--color-neutral-700); + --color-border-hover: var(--color-neutral-600); + --color-field: var(--color-neutral-900); --color-field-hover: var(--color-neutral-800); --color-field-foreground: var(--color-neutral-50); --color-field-border: var(--color-neutral-700); --color-field-border-hover: var(--color-neutral-600); --color-field-placeholder: var(--color-neutral-500); + --color-link: var(--color-neutral-50); --color-link-hover: var(--color-neutral-400); --color-ring: var(--color-neutral-50); From 28de722f27c918f2eddc2670cb5f1527065dcecc Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Sat, 28 Mar 2026 05:02:18 +0700 Subject: [PATCH 014/199] refactor(styles): Use blue for info tokens --- packages/styles/src/tokens/colors.css | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/styles/src/tokens/colors.css b/packages/styles/src/tokens/colors.css index 25877bb..9f2bbd4 100644 --- a/packages/styles/src/tokens/colors.css +++ b/packages/styles/src/tokens/colors.css @@ -26,9 +26,9 @@ --color-danger-active: var(--color-red-800); --color-danger-foreground: var(--color-white); - --color-info: var(--color-sky-500); - --color-info-hover: var(--color-sky-600); - --color-info-active: var(--color-sky-700); + --color-info: var(--color-blue-600); + --color-info-hover: var(--color-blue-700); + --color-info-active: var(--color-blue-800); --color-info-foreground: var(--color-white); --color-background: var(--color-white); @@ -101,9 +101,9 @@ --color-danger-active: var(--color-red-800); --color-danger-foreground: var(--color-white); - --color-info: var(--color-sky-500); - --color-info-hover: var(--color-sky-600); - --color-info-active: var(--color-sky-700); + --color-info: var(--color-blue-600); + --color-info-hover: var(--color-blue-700); + --color-info-active: var(--color-blue-800); --color-info-foreground: var(--color-white); --color-background: var(--color-white); @@ -174,9 +174,9 @@ --color-danger-active: var(--color-red-700); --color-danger-foreground: var(--color-white); - --color-info: var(--color-sky-400); - --color-info-hover: var(--color-sky-500); - --color-info-active: var(--color-sky-600); + --color-info: var(--color-blue-500); + --color-info-hover: var(--color-blue-600); + --color-info-active: var(--color-blue-700); --color-info-foreground: var(--color-white); --color-background: var(--color-neutral-950); From 8d6c0ccc5002f11d4e8727597e8be3b28d021158 Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Sat, 28 Mar 2026 05:26:31 +0700 Subject: [PATCH 015/199] feat(styles): Add fried-* palette, replace Tailwind built-in --- packages/styles/src/tokens/colors.css | 98 +++++++++++++------------- packages/styles/src/tokens/palette.css | 70 +++++++++++++++--- 2 files changed, 108 insertions(+), 60 deletions(-) diff --git a/packages/styles/src/tokens/colors.css b/packages/styles/src/tokens/colors.css index 9f2bbd4..9f88737 100644 --- a/packages/styles/src/tokens/colors.css +++ b/packages/styles/src/tokens/colors.css @@ -1,9 +1,9 @@ @custom-variant dark (&:is(.dark, [data-theme="dark"]) *); @theme { - --color-primary: var(--color-branding-500); - --color-primary-hover: var(--color-branding-600); - --color-primary-active: var(--color-branding-700); + --color-primary: var(--color-fried-lime-500); + --color-primary-hover: var(--color-fried-lime-600); + --color-primary-active: var(--color-fried-lime-700); --color-primary-foreground: var(--color-neutral-950); --color-secondary: var(--color-neutral-100); @@ -11,24 +11,24 @@ --color-secondary-active: var(--color-neutral-300); --color-secondary-foreground: var(--color-neutral-900); - --color-success: var(--color-green-600); - --color-success-hover: var(--color-green-700); - --color-success-active: var(--color-green-800); + --color-success: var(--color-fried-green-600); + --color-success-hover: var(--color-fried-green-700); + --color-success-active: var(--color-fried-green-800); --color-success-foreground: var(--color-white); - --color-warning: var(--color-amber-500); - --color-warning-hover: var(--color-amber-600); - --color-warning-active: var(--color-amber-700); + --color-warning: var(--color-fried-yellow-500); + --color-warning-hover: var(--color-fried-yellow-600); + --color-warning-active: var(--color-fried-yellow-700); --color-warning-foreground: var(--color-neutral-950); - --color-danger: var(--color-red-600); - --color-danger-hover: var(--color-red-700); - --color-danger-active: var(--color-red-800); + --color-danger: var(--color-fried-red-600); + --color-danger-hover: var(--color-fried-red-700); + --color-danger-active: var(--color-fried-red-800); --color-danger-foreground: var(--color-white); - --color-info: var(--color-blue-600); - --color-info-hover: var(--color-blue-700); - --color-info-active: var(--color-blue-800); + --color-info: var(--color-fried-blue-600); + --color-info-hover: var(--color-fried-blue-700); + --color-info-active: var(--color-fried-blue-800); --color-info-foreground: var(--color-white); --color-background: var(--color-white); @@ -53,7 +53,7 @@ --color-heading: var(--color-neutral-950); --color-code: var(--color-neutral-800); --color-code-background: var(--color-neutral-100); - --color-mark: var(--color-amber-200); + --color-mark: var(--color-fried-yellow-200); --color-mark-foreground: var(--color-neutral-950); --color-border: var(--color-neutral-200); @@ -76,9 +76,9 @@ :root, .light, [data-theme="light"] { - --color-primary: var(--color-branding-500); - --color-primary-hover: var(--color-branding-600); - --color-primary-active: var(--color-branding-700); + --color-primary: var(--color-fried-lime-500); + --color-primary-hover: var(--color-fried-lime-600); + --color-primary-active: var(--color-fried-lime-700); --color-primary-foreground: var(--color-neutral-950); --color-secondary: var(--color-neutral-100); @@ -86,24 +86,24 @@ --color-secondary-active: var(--color-neutral-300); --color-secondary-foreground: var(--color-neutral-900); - --color-success: var(--color-green-600); - --color-success-hover: var(--color-green-700); - --color-success-active: var(--color-green-800); + --color-success: var(--color-fried-green-600); + --color-success-hover: var(--color-fried-green-700); + --color-success-active: var(--color-fried-green-800); --color-success-foreground: var(--color-white); - --color-warning: var(--color-amber-500); - --color-warning-hover: var(--color-amber-600); - --color-warning-active: var(--color-amber-700); + --color-warning: var(--color-fried-yellow-500); + --color-warning-hover: var(--color-fried-yellow-600); + --color-warning-active: var(--color-fried-yellow-700); --color-warning-foreground: var(--color-neutral-950); - --color-danger: var(--color-red-600); - --color-danger-hover: var(--color-red-700); - --color-danger-active: var(--color-red-800); + --color-danger: var(--color-fried-red-600); + --color-danger-hover: var(--color-fried-red-700); + --color-danger-active: var(--color-fried-red-800); --color-danger-foreground: var(--color-white); - --color-info: var(--color-blue-600); - --color-info-hover: var(--color-blue-700); - --color-info-active: var(--color-blue-800); + --color-info: var(--color-fried-blue-600); + --color-info-hover: var(--color-fried-blue-700); + --color-info-active: var(--color-fried-blue-800); --color-info-foreground: var(--color-white); --color-background: var(--color-white); @@ -128,7 +128,7 @@ --color-heading: var(--color-neutral-950); --color-code: var(--color-neutral-800); --color-code-background: var(--color-neutral-100); - --color-mark: var(--color-amber-200); + --color-mark: var(--color-fried-yellow-200); --color-mark-foreground: var(--color-neutral-950); --color-border: var(--color-neutral-200); @@ -149,9 +149,9 @@ .dark, [data-theme="dark"] { - --color-primary: var(--color-branding-500); - --color-primary-hover: var(--color-branding-600); - --color-primary-active: var(--color-branding-700); + --color-primary: var(--color-fried-lime-500); + --color-primary-hover: var(--color-fried-lime-600); + --color-primary-active: var(--color-fried-lime-700); --color-primary-foreground: var(--color-neutral-950); --color-secondary: var(--color-neutral-800); @@ -159,24 +159,24 @@ --color-secondary-active: var(--color-neutral-950); --color-secondary-foreground: var(--color-neutral-100); - --color-success: var(--color-green-500); - --color-success-hover: var(--color-green-600); - --color-success-active: var(--color-green-700); + --color-success: var(--color-fried-green-500); + --color-success-hover: var(--color-fried-green-600); + --color-success-active: var(--color-fried-green-700); --color-success-foreground: var(--color-white); - --color-warning: var(--color-amber-400); - --color-warning-hover: var(--color-amber-500); - --color-warning-active: var(--color-amber-600); + --color-warning: var(--color-fried-yellow-400); + --color-warning-hover: var(--color-fried-yellow-500); + --color-warning-active: var(--color-fried-yellow-600); --color-warning-foreground: var(--color-neutral-950); - --color-danger: var(--color-red-500); - --color-danger-hover: var(--color-red-600); - --color-danger-active: var(--color-red-700); + --color-danger: var(--color-fried-red-500); + --color-danger-hover: var(--color-fried-red-600); + --color-danger-active: var(--color-fried-red-700); --color-danger-foreground: var(--color-white); - --color-info: var(--color-blue-500); - --color-info-hover: var(--color-blue-600); - --color-info-active: var(--color-blue-700); + --color-info: var(--color-fried-blue-500); + --color-info-hover: var(--color-fried-blue-600); + --color-info-active: var(--color-fried-blue-700); --color-info-foreground: var(--color-white); --color-background: var(--color-neutral-950); @@ -201,8 +201,8 @@ --color-heading: var(--color-neutral-50); --color-code: var(--color-neutral-200); --color-code-background: var(--color-neutral-800); - --color-mark: var(--color-amber-800); - --color-mark-foreground: var(--color-amber-100); + --color-mark: var(--color-fried-yellow-800); + --color-mark-foreground: var(--color-fried-yellow-100); --color-border: var(--color-neutral-700); --color-border-hover: var(--color-neutral-600); diff --git a/packages/styles/src/tokens/palette.css b/packages/styles/src/tokens/palette.css index bf2f902..0a17099 100644 --- a/packages/styles/src/tokens/palette.css +++ b/packages/styles/src/tokens/palette.css @@ -1,13 +1,61 @@ @theme { - --color-branding-50: oklch(0.99 0.027 120.44); - --color-branding-100: oklch(0.99 0.027 120.44); - --color-branding-200: oklch(0.967 0.09 120.76); - --color-branding-300: oklch(0.957 0.127 120.4); - --color-branding-400: oklch(0.947 0.169 120.48); - --color-branding-500: oklch(0.93 0.203 120.55); - --color-branding-600: oklch(0.775 0.169 120.74); - --color-branding-700: oklch(0.612 0.133 120.6); - --color-branding-800: oklch(0.45 0.098 120.6); - --color-branding-900: oklch(0.299 0.065 120.65); - --color-branding-950: oklch(0.213 0.047 120.75); + --color-fried-lime-50: oklch(0.99 0.027 120.44); + --color-fried-lime-100: oklch(0.99 0.027 120.44); + --color-fried-lime-200: oklch(0.967 0.09 120.76); + --color-fried-lime-300: oklch(0.957 0.127 120.4); + --color-fried-lime-400: oklch(0.947 0.169 120.48); + --color-fried-lime-500: oklch(0.93 0.203 120.55); + --color-fried-lime-600: oklch(0.775 0.169 120.74); + --color-fried-lime-700: oklch(0.612 0.133 120.6); + --color-fried-lime-800: oklch(0.45 0.098 120.6); + --color-fried-lime-900: oklch(0.299 0.065 120.65); + --color-fried-lime-950: oklch(0.213 0.047 120.75); + + --color-fried-green-50: oklch(0.975 0.023 150); + --color-fried-green-100: oklch(0.96 0.023 150); + --color-fried-green-200: oklch(0.907 0.077 150); + --color-fried-green-300: oklch(0.867 0.108 150); + --color-fried-green-400: oklch(0.827 0.144 150); + --color-fried-green-500: oklch(0.78 0.173 150); + --color-fried-green-600: oklch(0.625 0.144 150); + --color-fried-green-700: oklch(0.462 0.113 150); + --color-fried-green-800: oklch(0.3 0.083 150); + --color-fried-green-900: oklch(0.149 0.055 150); + --color-fried-green-950: oklch(0.1 0.04 150); + + --color-fried-yellow-50: oklch(0.985 0.024 85); + --color-fried-yellow-100: oklch(0.98 0.024 85); + --color-fried-yellow-200: oklch(0.947 0.081 85); + --color-fried-yellow-300: oklch(0.927 0.114 85); + --color-fried-yellow-400: oklch(0.907 0.152 85); + --color-fried-yellow-500: oklch(0.88 0.183 85); + --color-fried-yellow-600: oklch(0.725 0.152 85); + --color-fried-yellow-700: oklch(0.562 0.12 85); + --color-fried-yellow-800: oklch(0.4 0.088 85); + --color-fried-yellow-900: oklch(0.249 0.059 85); + --color-fried-yellow-950: oklch(0.163 0.042 85); + + --color-fried-red-50: oklch(0.96 0.026 25); + --color-fried-red-100: oklch(0.93 0.026 25); + --color-fried-red-200: oklch(0.847 0.085 25); + --color-fried-red-300: oklch(0.777 0.121 25); + --color-fried-red-400: oklch(0.707 0.161 25); + --color-fried-red-500: oklch(0.63 0.193 25); + --color-fried-red-600: oklch(0.475 0.161 25); + --color-fried-red-700: oklch(0.312 0.126 25); + --color-fried-red-800: oklch(0.15 0.093 25); + --color-fried-red-900: oklch(0.1 0.062 25); + --color-fried-red-950: oklch(0.1 0.045 25); + + --color-fried-blue-50: oklch(0.96 0.023 260); + --color-fried-blue-100: oklch(0.93 0.023 260); + --color-fried-blue-200: oklch(0.847 0.077 260); + --color-fried-blue-300: oklch(0.777 0.108 260); + --color-fried-blue-400: oklch(0.707 0.144 260); + --color-fried-blue-500: oklch(0.63 0.173 260); + --color-fried-blue-600: oklch(0.475 0.144 260); + --color-fried-blue-700: oklch(0.312 0.113 260); + --color-fried-blue-800: oklch(0.15 0.083 260); + --color-fried-blue-900: oklch(0.1 0.055 260); + --color-fried-blue-950: oklch(0.1 0.04 260); } From 1452562ac4e0d23a5f383eb429082f0baf5b9ea7 Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Sat, 28 Mar 2026 05:40:30 +0700 Subject: [PATCH 016/199] feat(react): Add forwardRef, render props, ref tests --- apps/docs/public/llm/button.md | 30 ++++++++++++++++++- .../src/content/docs/components/button.mdx | 20 ++++++++++++- .../react/src/components/button/Button.tsx | 17 +++++++---- .../src/components/button/button.stories.tsx | 6 +++- .../src/components/button/button.test.tsx | 13 ++++++++ 5 files changed, 78 insertions(+), 8 deletions(-) diff --git a/apps/docs/public/llm/button.md b/apps/docs/public/llm/button.md index 77a8145..ebe57eb 100644 --- a/apps/docs/public/llm/button.md +++ b/apps/docs/public/llm/button.md @@ -43,7 +43,8 @@ import { Button } from "@fried-ui/react/button"; | `aria-labelledby` | `string` | - | ID of labelling element | | `aria-describedby` | `string` | - | ID of describing element | | `className` | `string` | - | Additional CSS classes | -| `children` | `ReactNode \| (renderProps) => ReactNode` | - | Button content | +| `ref` | `Ref` | - | Forwarded ref to button element | +| `children` | `ReactNode \| (renderProps: ButtonRenderProps) => ReactNode` | - | Button content | ## Data Attributes @@ -199,6 +200,32 @@ Use `onPress` instead of `onClick` — handles keyboard, touch, and mouse unifor ``` +### Render Props + +Access internal state to dynamically change content using the children function. + +```tsx + +``` + +Available render props: `isHovered`, `isPressed`, `isFocused`, `isFocusVisible`, `isDisabled`, `isPending`. + +### Ref Forwarding + +Supports ref forwarding for integration with Tooltip, Popover, Floating UI, etc. + +```tsx +const ref = useRef(null); +; +``` + ## Anatomy ``` @@ -210,6 +237,7 @@ Use `onPress` instead of `onClick` — handles keyboard, touch, and mouse unifor - `children`: Icon + label as direct children - Spinner: Rendered internally when `isPending={true}`, absolutely positioned over content - Icon sizing: Use `className="size-match-font"` on SVG icons +- Ref forwarding: Supports `ref` prop to the underlying ` ``` +## Render Props + +Access internal state to dynamically change content using the children function. + +```tsx + +``` + +Available render props: `isHovered`, `isPressed`, `isFocused`, `isFocusVisible`, `isDisabled`, `isPending`. + ## Design Anatomy Every spacing value is derived from the **golden ratio (φ = 1.618)** using font-size as the base unit `x`: @@ -256,7 +273,8 @@ Built on [React Aria Button](https://react-spectrum.adobe.com/react-aria/Button. | `aria-labelledby` | `string` | - | ID of the labelling element | | `aria-describedby` | `string` | - | ID of the describing element | | `className` | `string` | - | Additional CSS classes | -| `children` | `ReactNode \| (renderProps) => ReactNode` | - | Button content | +| `ref` | `Ref` | - | Forwarded ref to the button element | +| `children` | `ReactNode \| (renderProps: ButtonRenderProps) => ReactNode` | - | Button content | ### Render Props diff --git a/packages/react/src/components/button/Button.tsx b/packages/react/src/components/button/Button.tsx index 39f919b..754ec5b 100644 --- a/packages/react/src/components/button/Button.tsx +++ b/packages/react/src/components/button/Button.tsx @@ -1,8 +1,13 @@ "use client"; -import type { ReactNode } from "react"; +import { type ForwardedRef, forwardRef, type ReactNode } from "react"; -import { Button as RACButton, type ButtonProps as RACButtonProps, composeRenderProps } from "react-aria-components"; +import { + Button as RACButton, + type ButtonProps as RACButtonProps, + type ButtonRenderProps, + composeRenderProps, +} from "react-aria-components"; import { Spinner } from "../icons"; import { cn } from "../../utils/cn"; @@ -12,14 +17,14 @@ export type ButtonProps = { size?: ButtonSize; radius?: ButtonRadius; className?: string; - children?: ReactNode | ((renderProps: { isPending: boolean }) => ReactNode); + children?: ReactNode | ((renderProps: ButtonRenderProps) => ReactNode); } & Omit; type ButtonVariant = "primary" | "secondary" | "ghost" | "outline" | "success" | "warning" | "danger" | "info"; type ButtonSize = "sm" | "md" | "lg" | "xl"; type ButtonRadius = "none" | "sm" | "md" | "lg" | "xl" | "full"; -function Button(props: Readonly) { +function ButtonInner(props: Readonly, ref: ForwardedRef) { const { radius = "md", size = "md", variant = "primary", children, className, ...rest } = props; const pendingClass = rest.isPending ? "fri-button--pending" : undefined; @@ -34,7 +39,7 @@ function Button(props: Readonly) { ); return ( - + {composeRenderProps(children, (child, { isPending }) => ( <> {child} @@ -46,4 +51,6 @@ function Button(props: Readonly) { ); } +const Button = forwardRef(ButtonInner); + export { Button }; diff --git a/packages/react/src/components/button/button.stories.tsx b/packages/react/src/components/button/button.stories.tsx index d460473..24d591c 100644 --- a/packages/react/src/components/button/button.stories.tsx +++ b/packages/react/src/components/button/button.stories.tsx @@ -180,6 +180,10 @@ const Pending: Story = { }, }; -export { Default, Variants, Sizes, Radii, Disabled, Pending }; +const RenderProps: Story = { + render: () => , +}; + +export { Default, Variants, Sizes, Radii, Disabled, Pending, RenderProps }; export default meta; diff --git a/packages/react/src/components/button/button.test.tsx b/packages/react/src/components/button/button.test.tsx index 5307435..ad72065 100644 --- a/packages/react/src/components/button/button.test.tsx +++ b/packages/react/src/components/button/button.test.tsx @@ -1,3 +1,5 @@ +import { createRef } from "react"; + import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { describe, expect, it, vi } from "vitest"; @@ -82,4 +84,15 @@ describe("Button", () => { render(); expect(screen.getByTestId("custom-btn")).toBeInTheDocument(); }); + + it("forwards ref to the button element", () => { + const ref = createRef(); + render(); + expect(ref.current).toBeInstanceOf(HTMLButtonElement); + }); + + it("supports render props children", () => { + render(); + expect(screen.getByRole("button", { name: "Idle" })).toBeInTheDocument(); + }); }); From 2bdfcb5950b1f552dab1e1b63ddeacd4d0164c1b Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Sat, 28 Mar 2026 05:43:00 +0700 Subject: [PATCH 017/199] fix(deps): Override path-to-regexp vulnerability --- package.json | 1 + pnpm-lock.yaml | 21 +++++++++++---------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 3728aee..dea9d07 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "js-yaml@<3.13.1": ">=3.13.1", "minimatch@>=9.0.0 <9.0.6": ">=9.0.6", "minimatch@<3.1.3": ">=3.1.3", + "path-to-regexp@>=8.0.0 <8.4.0": ">=8.4.0", "picomatch@>=4.0.0 <4.0.4": ">=4.0.4", "picomatch@<2.3.2": ">=2.3.2" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d5e37b0..836d0ab 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,13 +5,14 @@ settings: excludeLinksFromLockfile: false overrides: - minimatch@<3.1.3: '>=3.1.3' - minimatch@>=9.0.0 <9.0.6: '>=9.0.6' flatted@<3.4.2: '>=3.4.2' - picomatch@<2.3.2: '>=2.3.2' - picomatch@>=4.0.0 <4.0.4: '>=4.0.4' - js-yaml@<3.13.1: '>=3.13.1' js-yaml@>=4.0.0 <4.1.1: '>=4.1.1' + js-yaml@<3.13.1: '>=3.13.1' + minimatch@>=9.0.0 <9.0.6: '>=9.0.6' + minimatch@<3.1.3: '>=3.1.3' + picomatch@>=4.0.0 <4.0.4: '>=4.0.4' + picomatch@<2.3.2: '>=2.3.2' + path-to-regexp@>=8.0.0 <8.4.0: '>=8.4.0' importers: @@ -5283,8 +5284,8 @@ packages: resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} engines: {node: 18 || 20 || >=22} - path-to-regexp@8.3.0: - resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + path-to-regexp@8.4.0: + resolution: {integrity: sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg==} path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} @@ -10905,7 +10906,7 @@ snapshots: mdast-util-to-markdown: 2.1.2 negotiator: 1.0.0 npm-to-yarn: 3.0.1 - path-to-regexp: 8.3.0 + path-to-regexp: 8.4.0 remark: 15.0.1 remark-gfm: 4.0.1 remark-rehype: 11.1.2 @@ -12526,7 +12527,7 @@ snapshots: lru-cache: 11.2.7 minipass: 7.1.3 - path-to-regexp@8.3.0: {} + path-to-regexp@8.4.0: {} path-type@4.0.0: {} @@ -13080,7 +13081,7 @@ snapshots: depd: 2.0.0 is-promise: 4.0.0 parseurl: 1.3.3 - path-to-regexp: 8.3.0 + path-to-regexp: 8.4.0 transitivePeerDependencies: - supports-color From 378a2b929de5a9509cfe923750d17f72e17bbf4c Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Sat, 28 Mar 2026 05:43:58 +0700 Subject: [PATCH 018/199] fix(react): Add return types for lint --- packages/react/src/components/button/button.stories.tsx | 2 +- packages/react/src/components/button/button.test.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react/src/components/button/button.stories.tsx b/packages/react/src/components/button/button.stories.tsx index 24d591c..0d96e0f 100644 --- a/packages/react/src/components/button/button.stories.tsx +++ b/packages/react/src/components/button/button.stories.tsx @@ -181,7 +181,7 @@ const Pending: Story = { }; const RenderProps: Story = { - render: () => , + render: (): React.JSX.Element => , }; export { Default, Variants, Sizes, Radii, Disabled, Pending, RenderProps }; diff --git a/packages/react/src/components/button/button.test.tsx b/packages/react/src/components/button/button.test.tsx index ad72065..7698374 100644 --- a/packages/react/src/components/button/button.test.tsx +++ b/packages/react/src/components/button/button.test.tsx @@ -92,7 +92,7 @@ describe("Button", () => { }); it("supports render props children", () => { - render(); + render(); expect(screen.getByRole("button", { name: "Idle" })).toBeInTheDocument(); }); }); From 97471874026d3893f5eedcad7297342738906093 Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Sat, 28 Mar 2026 06:42:58 +0700 Subject: [PATCH 019/199] feat(react): Add isIconOnly prop, dedupe color tokens --- apps/docs/public/llm/button.md | 12 +++ apps/docs/src/app/docs/[[...slug]]/page.tsx | 2 + apps/docs/src/app/globals.css | 36 +++++++++ apps/docs/src/components/MdxComponents.tsx | 41 ++++++++++ apps/docs/src/components/Preview.tsx | 2 +- .../src/content/docs/components/button.mdx | 16 ++++ .../react/src/components/button/Button.tsx | 8 +- .../src/components/button/button.stories.tsx | 55 +++++++++++++- .../src/components/button/button.test.tsx | 10 +++ packages/styles/src/components/button.css | 20 +++++ packages/styles/src/tokens/colors.css | 76 +------------------ 11 files changed, 197 insertions(+), 81 deletions(-) diff --git a/apps/docs/public/llm/button.md b/apps/docs/public/llm/button.md index ebe57eb..ab5ff6b 100644 --- a/apps/docs/public/llm/button.md +++ b/apps/docs/public/llm/button.md @@ -21,6 +21,7 @@ import { Button } from "@fried-ui/react/button"; | `variant` | `"primary" \| "secondary" \| "ghost" \| "outline" \| "success" \| "warning" \| "danger" \| "info"` | `"primary"` | Visual variant | | `size` | `"sm" \| "md" \| "lg" \| "xl"` | `"md"` | Size — controls padding, gap, font-size | | `radius` | `"none" \| "sm" \| "md" \| "lg" \| "xl" \| "full"` | `"md"` | Border radius (independent from size) | +| `isIconOnly` | `boolean` | `false` | Icon-only square button | | `isDisabled` | `boolean` | `false` | Disabled state | | `isPending` | `boolean` | `false` | Loading spinner, disables interaction | | `onPress` | `(e: PressEvent) => void` | - | Press handler (preferred over onClick) | @@ -80,6 +81,7 @@ import { Button } from "@fried-ui/react/button"; | `fri-button--radius-lg` | Large border radius | | `fri-button--radius-xl` | Extra large border radius | | `fri-button--radius-full` | Full (pill) border radius | +| `fri-button--icon-only` | Icon-only square button | | `fri-button--pending` | Pending/loading state | | `fri-button__spinner` | Internal spinner element | @@ -148,6 +150,16 @@ Icons use `size-match-font` to match the font size. Gap scales via golden ratio. ``` +### Icon Only + +Square button for icon-only actions. Always provide `aria-label`. + +```tsx + +``` + ### Disabled Prevents interaction and applies disabled styling. diff --git a/apps/docs/src/app/docs/[[...slug]]/page.tsx b/apps/docs/src/app/docs/[[...slug]]/page.tsx index 338b582..18ea1c4 100644 --- a/apps/docs/src/app/docs/[[...slug]]/page.tsx +++ b/apps/docs/src/app/docs/[[...slug]]/page.tsx @@ -13,6 +13,7 @@ import { ButtonSizes, ButtonVariants, ButtonWithIcons, + ButtonIconOnly, } from "@/components/MdxComponents"; import { Preview } from "@/components/Preview"; import { source } from "@/lib/source"; @@ -31,6 +32,7 @@ const mdxComponents = { ButtonCustomClass, ButtonDemo, ButtonDisabled, + ButtonIconOnly, ButtonPending, ButtonRadii, ButtonSizes, diff --git a/apps/docs/src/app/globals.css b/apps/docs/src/app/globals.css index 3526167..0d6dc70 100644 --- a/apps/docs/src/app/globals.css +++ b/apps/docs/src/app/globals.css @@ -2,3 +2,39 @@ @import "fumadocs-ui/css/black.css"; @import "fumadocs-ui/style.css"; @import "@fried-ui/styles"; + +:root, +:host { + --color-fd-background: #fff; + --color-fd-foreground: #000; + --color-fd-card: #fff; + --color-fd-card-foreground: #000; + --color-fd-primary: #000; + + --radius-sm: 0; + --radius-md: 0; + --radius-lg: 0; + --radius-xl: 0; + --radius-2xl: 0; +} + +.dark { + --color-fd-background: #000; + --color-fd-foreground: #fff; + --color-fd-card: #000; + --color-fd-card-foreground: #fff; + --color-fd-border: #6663; + --color-fd-primary: #fff; + + /* --color-fd-muted: #212121; + --color-fd-muted-foreground: #b3b3b3cc; + --color-fd-popover: #1e1e1e; + --color-fd-popover-foreground: #dedede; + --color-fd-primary-foreground: #171717; + --color-fd-secondary: #212121; + --color-fd-secondary-foreground: #ebebeb; + --color-fd-accent: #6868684d; + --color-fd-accent-foreground: #e6e6e6; + --color-fd-ring: #8c8c8c; + --color-fd-overlay: #0003; */ +} diff --git a/apps/docs/src/components/MdxComponents.tsx b/apps/docs/src/components/MdxComponents.tsx index 3ca0ddb..5fcae3d 100644 --- a/apps/docs/src/components/MdxComponents.tsx +++ b/apps/docs/src/components/MdxComponents.tsx @@ -45,6 +45,46 @@ function ButtonRadii(): React.JSX.Element { ); } +function ButtonIconOnly(): React.JSX.Element { + return ( +
+ + + + +
+ ); +} + function ButtonDisabled(): React.JSX.Element { return ; } @@ -97,6 +137,7 @@ export { ButtonCustomClass, ButtonDemo, ButtonDisabled, + ButtonIconOnly, ButtonPending, ButtonRadii, ButtonSizes, diff --git a/apps/docs/src/components/Preview.tsx b/apps/docs/src/components/Preview.tsx index 129a815..7b15254 100644 --- a/apps/docs/src/components/Preview.tsx +++ b/apps/docs/src/components/Preview.tsx @@ -8,7 +8,7 @@ function Preview(props: Readonly): React.JSX.Element { const { children } = props; return ( -
+
{children}
); diff --git a/apps/docs/src/content/docs/components/button.mdx b/apps/docs/src/content/docs/components/button.mdx index cb3ee97..d9b899a 100644 --- a/apps/docs/src/content/docs/components/button.mdx +++ b/apps/docs/src/content/docs/components/button.mdx @@ -148,6 +148,20 @@ Icons inside buttons use the `size-match-font` utility to lock their size to the ``` +## Icon Only + +Square button for icon-only actions. Always provide `aria-label` for accessibility. + + + + + +```tsx + +``` + ## As a Link Wrap a `Button` with an anchor or Next.js `Link` to render a button that navigates. @@ -251,6 +265,7 @@ Built on [React Aria Button](https://react-spectrum.adobe.com/react-aria/Button. | `variant` | `"primary" \| "secondary" \| "ghost" \| "outline" \| "success" \| "warning" \| "danger" \| "info"` | `"primary"` | The visual variant of the button | | `size` | `"sm" \| "md" \| "lg" \| "xl"` | `"md"` | The size of the button | | `radius` | `"none" \| "sm" \| "md" \| "lg" \| "xl" \| "full"` | `"md"` | The border radius of the button | +| `isIconOnly` | `boolean` | `false` | Whether the button is icon-only (square) | | `isDisabled` | `boolean` | `false` | Whether the button is disabled | | `isPending` | `boolean` | `false` | Whether the button shows a loading spinner | | `onPress` | `(e: PressEvent) => void` | - | Handler called when the button is pressed | @@ -310,6 +325,7 @@ Built on [React Aria Button](https://react-spectrum.adobe.com/react-aria/Button. | `fri-button--radius-lg` | Large border radius | | `fri-button--radius-xl` | Extra large border radius | | `fri-button--radius-full` | Full (pill) border radius | +| `fri-button--icon-only` | Icon-only square button | | `fri-button--pending` | Pending/loading state | | `fri-button__spinner` | Internal spinner element | diff --git a/packages/react/src/components/button/Button.tsx b/packages/react/src/components/button/Button.tsx index 754ec5b..3eff969 100644 --- a/packages/react/src/components/button/Button.tsx +++ b/packages/react/src/components/button/Button.tsx @@ -16,6 +16,7 @@ export type ButtonProps = { variant?: ButtonVariant; size?: ButtonSize; radius?: ButtonRadius; + isIconOnly?: boolean; className?: string; children?: ReactNode | ((renderProps: ButtonRenderProps) => ReactNode); } & Omit; @@ -25,16 +26,15 @@ type ButtonSize = "sm" | "md" | "lg" | "xl"; type ButtonRadius = "none" | "sm" | "md" | "lg" | "xl" | "full"; function ButtonInner(props: Readonly, ref: ForwardedRef) { - const { radius = "md", size = "md", variant = "primary", children, className, ...rest } = props; - - const pendingClass = rest.isPending ? "fri-button--pending" : undefined; + const { isIconOnly, radius = "md", size = "md", variant = "primary", children, className, ...rest } = props; const buttonClassName = cn( "fri-button", `fri-button--${variant}`, `fri-button--${size}`, `fri-button--radius-${radius}`, - pendingClass, + isIconOnly && "fri-button--icon-only", + rest.isPending && "fri-button--pending", className, ); diff --git a/packages/react/src/components/button/button.stories.tsx b/packages/react/src/components/button/button.stories.tsx index 0d96e0f..7fc3438 100644 --- a/packages/react/src/components/button/button.stories.tsx +++ b/packages/react/src/components/button/button.stories.tsx @@ -74,6 +74,19 @@ const meta = { category: "Style Variants", }, }, + isIconOnly: { + control: "boolean", + description: "Whether the button is icon-only (square)", + table: { + type: { + summary: "boolean", + }, + defaultValue: { + summary: "false", + }, + category: "Style Variants", + }, + }, isDisabled: { control: "boolean", description: "Whether the button is disabled", @@ -180,10 +193,50 @@ const Pending: Story = { }, }; +const IconOnly: Story = { + render: (): React.JSX.Element => ( +
+ + + + +
+ ), +}; + const RenderProps: Story = { render: (): React.JSX.Element => , }; -export { Default, Variants, Sizes, Radii, Disabled, Pending, RenderProps }; +export { Default, Variants, Sizes, Radii, IconOnly, Disabled, Pending, RenderProps }; export default meta; diff --git a/packages/react/src/components/button/button.test.tsx b/packages/react/src/components/button/button.test.tsx index 7698374..785328c 100644 --- a/packages/react/src/components/button/button.test.tsx +++ b/packages/react/src/components/button/button.test.tsx @@ -91,6 +91,16 @@ describe("Button", () => { expect(ref.current).toBeInstanceOf(HTMLButtonElement); }); + it("applies icon-only class", () => { + render( + , + ); + const el = screen.getByRole("button"); + expect(el.className).toContain("fri-button--icon-only"); + }); + it("supports render props children", () => { render(); expect(screen.getByRole("button", { name: "Idle" })).toBeInTheDocument(); diff --git a/packages/styles/src/components/button.css b/packages/styles/src/components/button.css index 659d4a9..0605756 100644 --- a/packages/styles/src/components/button.css +++ b/packages/styles/src/components/button.css @@ -112,6 +112,26 @@ @apply gap-[0.927rem] px-6 py-3 text-2xl; } +.fri-button--icon-only { + @apply aspect-square justify-center gap-0 p-0; +} + +.fri-button--icon-only.fri-button--sm { + @apply w-[1.625rem]; +} + +.fri-button--icon-only.fri-button--md { + @apply w-8; +} + +.fri-button--icon-only.fri-button--lg { + @apply w-10; +} + +.fri-button--icon-only.fri-button--xl { + @apply w-12; +} + .fri-button--radius-none { @apply rounded-none; } diff --git a/packages/styles/src/tokens/colors.css b/packages/styles/src/tokens/colors.css index 9f88737..d1dbd67 100644 --- a/packages/styles/src/tokens/colors.css +++ b/packages/styles/src/tokens/colors.css @@ -73,80 +73,6 @@ } @layer base { - :root, - .light, - [data-theme="light"] { - --color-primary: var(--color-fried-lime-500); - --color-primary-hover: var(--color-fried-lime-600); - --color-primary-active: var(--color-fried-lime-700); - --color-primary-foreground: var(--color-neutral-950); - - --color-secondary: var(--color-neutral-100); - --color-secondary-hover: var(--color-neutral-200); - --color-secondary-active: var(--color-neutral-300); - --color-secondary-foreground: var(--color-neutral-900); - - --color-success: var(--color-fried-green-600); - --color-success-hover: var(--color-fried-green-700); - --color-success-active: var(--color-fried-green-800); - --color-success-foreground: var(--color-white); - - --color-warning: var(--color-fried-yellow-500); - --color-warning-hover: var(--color-fried-yellow-600); - --color-warning-active: var(--color-fried-yellow-700); - --color-warning-foreground: var(--color-neutral-950); - - --color-danger: var(--color-fried-red-600); - --color-danger-hover: var(--color-fried-red-700); - --color-danger-active: var(--color-fried-red-800); - --color-danger-foreground: var(--color-white); - - --color-info: var(--color-fried-blue-600); - --color-info-hover: var(--color-fried-blue-700); - --color-info-active: var(--color-fried-blue-800); - --color-info-foreground: var(--color-white); - - --color-background: var(--color-white); - --color-foreground: var(--color-neutral-950); - - --color-surface: var(--color-neutral-50); - --color-surface-hover: var(--color-neutral-100); - --color-surface-active: var(--color-neutral-200); - --color-surface-foreground: var(--color-neutral-900); - - --color-muted: var(--color-neutral-100); - --color-muted-hover: var(--color-neutral-200); - --color-muted-active: var(--color-neutral-300); - --color-muted-foreground: var(--color-neutral-500); - - --color-separator: var(--color-neutral-100); - --color-skeleton: var(--color-neutral-200); - - --color-disabled: var(--color-neutral-100); - --color-disabled-foreground: var(--color-neutral-400); - - --color-heading: var(--color-neutral-950); - --color-code: var(--color-neutral-800); - --color-code-background: var(--color-neutral-100); - --color-mark: var(--color-fried-yellow-200); - --color-mark-foreground: var(--color-neutral-950); - - --color-border: var(--color-neutral-200); - --color-border-hover: var(--color-neutral-300); - - --color-field: var(--color-white); - --color-field-hover: var(--color-neutral-50); - --color-field-foreground: var(--color-neutral-950); - --color-field-border: var(--color-neutral-300); - --color-field-border-hover: var(--color-neutral-400); - --color-field-placeholder: var(--color-neutral-400); - - --color-link: var(--color-neutral-950); - --color-link-hover: var(--color-neutral-600); - --color-ring: var(--color-neutral-950); - --color-overlay: oklch(0% 0 0 / 50%); - } - .dark, [data-theme="dark"] { --color-primary: var(--color-fried-lime-500); @@ -179,7 +105,7 @@ --color-info-active: var(--color-fried-blue-700); --color-info-foreground: var(--color-white); - --color-background: var(--color-neutral-950); + --color-background: var(--color-black); --color-foreground: var(--color-neutral-50); --color-surface: var(--color-neutral-900); From 66c0fe597f059fd5ecb7853872a84992cdd50b21 Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Sat, 28 Mar 2026 06:53:34 +0700 Subject: [PATCH 020/199] refactor(docs): Use barrel import, Button badges --- README.md | 2 +- apps/docs/public/llm/button.md | 2 +- apps/docs/src/app/docs/[[...slug]]/page.tsx | 4 +- apps/docs/src/app/globals.css | 6 --- apps/docs/src/components/MdxComponents.tsx | 54 ++++++++++++++++++- .../src/content/docs/components/button.mdx | 45 +--------------- apps/docs/src/content/docs/index.mdx | 2 +- 7 files changed, 61 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index 009f84c..ca027ba 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ pnpm add @fried-ui/react @fried-ui/styles ``` ```tsx -import { Button } from "@fried-ui/react/button"; +import { Button } from "@fried-ui/react";
+ + + + + + + + + + +
+ ); +} function ButtonDemo(): React.JSX.Element { return ; @@ -134,6 +185,7 @@ function ButtonAsLink(): React.JSX.Element { export { ButtonAsLink, + ButtonBadgeLinks, ButtonCustomClass, ButtonDemo, ButtonDisabled, diff --git a/apps/docs/src/content/docs/components/button.mdx b/apps/docs/src/content/docs/components/button.mdx index d9b899a..6e36422 100644 --- a/apps/docs/src/content/docs/components/button.mdx +++ b/apps/docs/src/content/docs/components/button.mdx @@ -3,53 +3,12 @@ title: Button description: An accessible button component built on React Aria with focus management, keyboard navigation, and screen reader support. --- - + ## Import ```tsx -import { Button } from "@fried-ui/react/button"; +import { Button } from "@fried-ui/react"; ``` ## Usage diff --git a/apps/docs/src/content/docs/index.mdx b/apps/docs/src/content/docs/index.mdx index 5adb2f7..4c8fb23 100644 --- a/apps/docs/src/content/docs/index.mdx +++ b/apps/docs/src/content/docs/index.mdx @@ -19,7 +19,7 @@ Import styles in your CSS entry file: ## Usage ```tsx -import { Button } from "@fried-ui/react/button"; +import { Button } from "@fried-ui/react"; export default function App() { return ; From 890c7d2337546d8884782839e8955f6b9c97d0b5 Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Sat, 28 Mar 2026 07:09:29 +0700 Subject: [PATCH 021/199] fix(react): Lint fixes, extract CopyIcon, prop order --- apps/docs/src/app/docs/[[...slug]]/page.tsx | 2 +- apps/docs/src/components/CopyMarkdown.tsx | 16 +++++----------- apps/docs/src/components/MdxComponents.tsx | 15 +++++++++++---- packages/react/src/components/button/Button.tsx | 9 ++++++--- .../src/components/button/button.stories.tsx | 12 ++++++++---- .../react/src/components/button/button.test.tsx | 3 ++- packages/react/src/components/icons/CopyIcon.tsx | 16 ++++++++++++++++ packages/react/src/components/icons/index.ts | 1 + packages/react/src/components/index.ts | 1 + 9 files changed, 51 insertions(+), 24 deletions(-) create mode 100644 packages/react/src/components/icons/CopyIcon.tsx diff --git a/apps/docs/src/app/docs/[[...slug]]/page.tsx b/apps/docs/src/app/docs/[[...slug]]/page.tsx index 476262e..d752884 100644 --- a/apps/docs/src/app/docs/[[...slug]]/page.tsx +++ b/apps/docs/src/app/docs/[[...slug]]/page.tsx @@ -51,7 +51,7 @@ async function Page(props: Readonly): Promise { const MdxContent = page.data.body; const slugPath = params.slug?.join("/") ?? ""; const markdownUrl = `/api/mdx?slug=${slugPath}`; - const tocConfig = { header: }; + const tocConfig = { footer: }; return ( diff --git a/apps/docs/src/components/CopyMarkdown.tsx b/apps/docs/src/components/CopyMarkdown.tsx index 5c486a1..cb97b7c 100644 --- a/apps/docs/src/components/CopyMarkdown.tsx +++ b/apps/docs/src/components/CopyMarkdown.tsx @@ -2,6 +2,8 @@ import { useState } from "react"; +import { Button, CopyIcon } from "@fried-ui/react"; + interface CopyMarkdownProps { url: string; } @@ -20,18 +22,10 @@ function CopyMarkdown(props: Readonly): React.JSX.Element { } return ( - + ); } diff --git a/apps/docs/src/components/MdxComponents.tsx b/apps/docs/src/components/MdxComponents.tsx index a27bc51..8e1a005 100644 --- a/apps/docs/src/components/MdxComponents.tsx +++ b/apps/docs/src/components/MdxComponents.tsx @@ -17,6 +17,7 @@ function ButtonBadgeLinks(): React.JSX.Element { Storybook + + + - - - - - - - @@ -33,9 +39,7 @@ function ButtonBadgeLinks(): React.JSX.Element { rel="noopener noreferrer" > @@ -46,9 +50,7 @@ function ButtonBadgeLinks(): React.JSX.Element { rel="noopener noreferrer" > @@ -103,41 +105,19 @@ function ButtonIconOnly(): React.JSX.Element { return (
); @@ -159,23 +139,17 @@ function ButtonWithIcons(): React.JSX.Element { return (
diff --git a/packages/react/src/components/icons/ArrowRightIcon.tsx b/packages/react/src/components/icons/ArrowRightIcon.tsx new file mode 100644 index 0000000..2245521 --- /dev/null +++ b/packages/react/src/components/icons/ArrowRightIcon.tsx @@ -0,0 +1,15 @@ +"use client"; + +import type { SVGProps } from "react"; + +function ArrowRightIcon(props: Readonly>): React.JSX.Element { + const { className, ...rest } = props; + + return ( + + + + ); +} + +export { ArrowRightIcon }; diff --git a/packages/react/src/components/icons/ClipboardIcon.tsx b/packages/react/src/components/icons/ClipboardIcon.tsx new file mode 100644 index 0000000..1611608 --- /dev/null +++ b/packages/react/src/components/icons/ClipboardIcon.tsx @@ -0,0 +1,15 @@ +"use client"; + +import type { SVGProps } from "react"; + +function ClipboardIcon(props: Readonly>): React.JSX.Element { + const { className, ...rest } = props; + + return ( + + + + ); +} + +export { ClipboardIcon }; diff --git a/packages/react/src/components/icons/CopyIcon.tsx b/packages/react/src/components/icons/CopyIcon.tsx deleted file mode 100644 index c44f070..0000000 --- a/packages/react/src/components/icons/CopyIcon.tsx +++ /dev/null @@ -1,16 +0,0 @@ -"use client"; - -import type { SVGProps } from "react"; - -function CopyIcon(props: Readonly>): React.JSX.Element { - const { className, ...rest } = props; - - return ( - - - - - ); -} - -export { CopyIcon }; diff --git a/packages/react/src/components/icons/GitHubIcon.tsx b/packages/react/src/components/icons/GitHubIcon.tsx new file mode 100644 index 0000000..280184e --- /dev/null +++ b/packages/react/src/components/icons/GitHubIcon.tsx @@ -0,0 +1,15 @@ +"use client"; + +import type { SVGProps } from "react"; + +function GitHubIcon(props: Readonly>): React.JSX.Element { + const { className, ...rest } = props; + + return ( + + + + ); +} + +export { GitHubIcon }; diff --git a/packages/react/src/components/icons/MoreIcon.tsx b/packages/react/src/components/icons/MoreIcon.tsx new file mode 100644 index 0000000..08e6c42 --- /dev/null +++ b/packages/react/src/components/icons/MoreIcon.tsx @@ -0,0 +1,19 @@ +"use client"; + +import type { SVGProps } from "react"; + +function MoreIcon(props: Readonly>): React.JSX.Element { + const { className, ...rest } = props; + + return ( + + + + ); +} + +export { MoreIcon }; diff --git a/packages/react/src/components/icons/PlusIcon.tsx b/packages/react/src/components/icons/PlusIcon.tsx new file mode 100644 index 0000000..ee1eb08 --- /dev/null +++ b/packages/react/src/components/icons/PlusIcon.tsx @@ -0,0 +1,15 @@ +"use client"; + +import type { SVGProps } from "react"; + +function PlusIcon(props: Readonly>): React.JSX.Element { + const { className, ...rest } = props; + + return ( + + + + ); +} + +export { PlusIcon }; diff --git a/packages/react/src/components/icons/ReactAriaIcon.tsx b/packages/react/src/components/icons/ReactAriaIcon.tsx new file mode 100644 index 0000000..514ff0f --- /dev/null +++ b/packages/react/src/components/icons/ReactAriaIcon.tsx @@ -0,0 +1,15 @@ +"use client"; + +import type { SVGProps } from "react"; + +function ReactAriaIcon(props: Readonly>): React.JSX.Element { + const { className, ...rest } = props; + + return ( + + + + ); +} + +export { ReactAriaIcon }; diff --git a/packages/react/src/components/icons/SettingsIcon.tsx b/packages/react/src/components/icons/SettingsIcon.tsx new file mode 100644 index 0000000..b9e6b17 --- /dev/null +++ b/packages/react/src/components/icons/SettingsIcon.tsx @@ -0,0 +1,21 @@ +"use client"; + +import type { SVGProps } from "react"; + +function SettingsIcon(props: Readonly>): React.JSX.Element { + const { className, ...rest } = props; + + return ( + + + + + + ); +} + +export { SettingsIcon }; diff --git a/packages/react/src/components/icons/ShareIcon.tsx b/packages/react/src/components/icons/ShareIcon.tsx new file mode 100644 index 0000000..6a1b969 --- /dev/null +++ b/packages/react/src/components/icons/ShareIcon.tsx @@ -0,0 +1,19 @@ +"use client"; + +import type { SVGProps } from "react"; + +function ShareIcon(props: Readonly>): React.JSX.Element { + const { className, ...rest } = props; + + return ( + + + + ); +} + +export { ShareIcon }; diff --git a/packages/react/src/components/icons/StorybookIcon.tsx b/packages/react/src/components/icons/StorybookIcon.tsx new file mode 100644 index 0000000..390b60b --- /dev/null +++ b/packages/react/src/components/icons/StorybookIcon.tsx @@ -0,0 +1,15 @@ +"use client"; + +import type { SVGProps } from "react"; + +function StorybookIcon(props: Readonly>): React.JSX.Element { + const { className, ...rest } = props; + + return ( + + + + ); +} + +export { StorybookIcon }; diff --git a/packages/react/src/components/icons/index.ts b/packages/react/src/components/icons/index.ts index 625e6a2..c3247f2 100644 --- a/packages/react/src/components/icons/index.ts +++ b/packages/react/src/components/icons/index.ts @@ -1,2 +1,10 @@ -export { CopyIcon } from "./CopyIcon"; +export { ArrowRightIcon } from "./ArrowRightIcon"; +export { ClipboardIcon } from "./ClipboardIcon"; +export { GitHubIcon } from "./GitHubIcon"; +export { MoreIcon } from "./MoreIcon"; +export { PlusIcon } from "./PlusIcon"; +export { ReactAriaIcon } from "./ReactAriaIcon"; +export { SettingsIcon } from "./SettingsIcon"; +export { ShareIcon } from "./ShareIcon"; export { Spinner } from "./Spinner"; +export { StorybookIcon } from "./StorybookIcon"; diff --git a/packages/react/src/components/index.ts b/packages/react/src/components/index.ts index 393794f..eb9e30a 100644 --- a/packages/react/src/components/index.ts +++ b/packages/react/src/components/index.ts @@ -22,4 +22,16 @@ export type { Color, ColorFormat, ColorSpace, ColorChannel, ColorChannelRange, C export { Collection, ListBoxLoadMoreItem, RouterProvider, I18nProvider } from "react-aria-components"; export * from "./button"; -export { CopyIcon, Spinner } from "./icons"; + +export { + ArrowRightIcon, + ClipboardIcon, + GitHubIcon, + MoreIcon, + PlusIcon, + ReactAriaIcon, + SettingsIcon, + ShareIcon, + Spinner, + StorybookIcon, +} from "./icons"; From dd77920d8abed45fa7b584d61afc96c5eaa602eb Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Sat, 28 Mar 2026 07:30:44 +0700 Subject: [PATCH 023/199] refactor(docs): Extract BadgeLinks as global component --- apps/docs/src/components/BadgeLinks.tsx | 34 +++++++++++ apps/docs/src/components/MdxComponents.tsx | 69 ++++++++-------------- 2 files changed, 60 insertions(+), 43 deletions(-) create mode 100644 apps/docs/src/components/BadgeLinks.tsx diff --git a/apps/docs/src/components/BadgeLinks.tsx b/apps/docs/src/components/BadgeLinks.tsx new file mode 100644 index 0000000..db373a5 --- /dev/null +++ b/apps/docs/src/components/BadgeLinks.tsx @@ -0,0 +1,34 @@ +"use client"; + +import type { ReactNode } from "react"; + +import { Button } from "@fried-ui/react"; + +interface BadgeLinksProps { + links: BadgeLinkProps[]; +} + +export interface BadgeLinkProps { + href: string; + icon: ReactNode; + label: string; +} + +function BadgeLinks(props: Readonly): React.JSX.Element { + const { links } = props; + + return ( +
+ {links.map((link) => ( + + + + ))} +
+ ); +} + +export { BadgeLinks }; diff --git a/apps/docs/src/components/MdxComponents.tsx b/apps/docs/src/components/MdxComponents.tsx index 96248ca..3ca6888 100644 --- a/apps/docs/src/components/MdxComponents.tsx +++ b/apps/docs/src/components/MdxComponents.tsx @@ -12,50 +12,33 @@ import { StorybookIcon, } from "@fried-ui/react"; +import { BadgeLinks } from "./BadgeLinks"; + +const buttonBadgeLinksData = [ + { + href: "https://storybook.fried-ui.com/?path=/docs/components-button--docs", + icon: , + label: "Storybook", + }, + { + href: "https://react-spectrum.adobe.com/react-aria/Button.html", + icon: , + label: "React Aria", + }, + { + href: "https://github.com/fried-day/fried-ui/blob/main/packages/react/src/components/button/Button.tsx", + icon: , + label: "Source", + }, + { + href: "https://github.com/fried-day/fried-ui/blob/main/packages/styles/src/components/button.css", + icon: , + label: "Styles source", + }, +]; + function ButtonBadgeLinks(): React.JSX.Element { - return ( - - ); + return ; } function ButtonDemo(): React.JSX.Element { From 5e617cf1f9222b84c9aea4064bcbee9ccafe0ece Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Sat, 28 Mar 2026 07:32:14 +0700 Subject: [PATCH 024/199] fix(docs): Update Storybook URL to Vercel --- apps/docs/src/components/MdxComponents.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/src/components/MdxComponents.tsx b/apps/docs/src/components/MdxComponents.tsx index 3ca6888..ee5bf3a 100644 --- a/apps/docs/src/components/MdxComponents.tsx +++ b/apps/docs/src/components/MdxComponents.tsx @@ -16,7 +16,7 @@ import { BadgeLinks } from "./BadgeLinks"; const buttonBadgeLinksData = [ { - href: "https://storybook.fried-ui.com/?path=/docs/components-button--docs", + href: "https://fried-ui-storybook.vercel.app/?path=/story/components-button--default", icon: , label: "Storybook", }, From 786b09b5bccaf29481d364804ae88d808bec1609 Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Sat, 28 Mar 2026 07:47:31 +0700 Subject: [PATCH 025/199] fix(docs): Add size sm to CopyMarkdown button --- apps/docs/src/components/CopyMarkdown.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/src/components/CopyMarkdown.tsx b/apps/docs/src/components/CopyMarkdown.tsx index 035db74..f9552b1 100644 --- a/apps/docs/src/components/CopyMarkdown.tsx +++ b/apps/docs/src/components/CopyMarkdown.tsx @@ -22,7 +22,7 @@ function CopyMarkdown(props: Readonly): React.JSX.Element { } return ( - From 0bbbe4b7037a1ea47e581d5041d5cd7d50dbd8e8 Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Sat, 28 Mar 2026 11:15:30 +0700 Subject: [PATCH 026/199] refactor(styles): Rewrite rules, add token ref, dark mode tokens --- apps/docs/src/components/BadgeLinks.tsx | 2 +- .../react/src/components/button/Button.tsx | 10 +- packages/styles/src/tokens/colors.css | 232 +++++++++++++----- packages/styles/src/tokens/layout.css | 12 +- packages/styles/src/tokens/palette.css | 48 ---- 5 files changed, 186 insertions(+), 118 deletions(-) diff --git a/apps/docs/src/components/BadgeLinks.tsx b/apps/docs/src/components/BadgeLinks.tsx index db373a5..5b37ab8 100644 --- a/apps/docs/src/components/BadgeLinks.tsx +++ b/apps/docs/src/components/BadgeLinks.tsx @@ -21,7 +21,7 @@ function BadgeLinks(props: Readonly): React.JSX.Element {
{links.map((link) => ( - diff --git a/packages/react/src/components/button/Button.tsx b/packages/react/src/components/button/Button.tsx index f9a7c49..36b7263 100644 --- a/packages/react/src/components/button/Button.tsx +++ b/packages/react/src/components/button/Button.tsx @@ -13,18 +13,14 @@ import { Spinner } from "../icons"; import { cn } from "../../utils/cn"; export type ButtonProps = { - variant?: ButtonVariant; - size?: ButtonSize; - radius?: ButtonRadius; + variant?: "primary" | "secondary" | "ghost" | "outline" | "success" | "warning" | "danger" | "info"; + size?: "sm" | "md" | "lg" | "xl"; + radius?: "none" | "sm" | "md" | "lg" | "xl" | "full"; isIconOnly?: boolean; className?: string; children?: ReactNode | ((renderProps: ButtonRenderProps) => ReactNode); } & Omit; -type ButtonVariant = "primary" | "secondary" | "ghost" | "outline" | "success" | "warning" | "danger" | "info"; -type ButtonSize = "sm" | "md" | "lg" | "xl"; -type ButtonRadius = "none" | "sm" | "md" | "lg" | "xl" | "full"; - function ButtonInner(props: Readonly, ref: ForwardedRef) { const { radius = "md", size = "md", variant = "primary", children, className, isIconOnly, ...rest } = props; diff --git a/packages/styles/src/tokens/colors.css b/packages/styles/src/tokens/colors.css index d1dbd67..d03c269 100644 --- a/packages/styles/src/tokens/colors.css +++ b/packages/styles/src/tokens/colors.css @@ -1,148 +1,264 @@ @custom-variant dark (&:is(.dark, [data-theme="dark"]) *); @theme { + /* --- primary: สี brand หลัก (CTA, solid button) --- */ --color-primary: var(--color-fried-lime-500); --color-primary-hover: var(--color-fried-lime-600); --color-primary-active: var(--color-fried-lime-700); --color-primary-foreground: var(--color-neutral-950); + --color-primary-pale: var(--color-fried-lime-100); + --color-primary-pale-foreground: var(--color-fried-lime-800); + /* --- secondary: ปุ่มรอง, less emphasis --- */ --color-secondary: var(--color-neutral-100); --color-secondary-hover: var(--color-neutral-200); --color-secondary-active: var(--color-neutral-300); --color-secondary-foreground: var(--color-neutral-900); + --color-secondary-pale: var(--color-neutral-50); + --color-secondary-pale-foreground: var(--color-neutral-700); - --color-success: var(--color-fried-green-600); - --color-success-hover: var(--color-fried-green-700); - --color-success-active: var(--color-fried-green-800); + /* --- success: สำเร็จ, saved, confirmed --- */ + --color-success: var(--color-green-600); + --color-success-hover: var(--color-green-700); + --color-success-active: var(--color-green-800); --color-success-foreground: var(--color-white); + --color-success-pale: var(--color-green-50); + --color-success-pale-foreground: var(--color-green-800); - --color-warning: var(--color-fried-yellow-500); - --color-warning-hover: var(--color-fried-yellow-600); - --color-warning-active: var(--color-fried-yellow-700); + /* --- warning: เตือน, caution --- */ + --color-warning: var(--color-amber-500); + --color-warning-hover: var(--color-amber-600); + --color-warning-active: var(--color-amber-700); --color-warning-foreground: var(--color-neutral-950); + --color-warning-pale: var(--color-amber-50); + --color-warning-pale-foreground: var(--color-amber-800); - --color-danger: var(--color-fried-red-600); - --color-danger-hover: var(--color-fried-red-700); - --color-danger-active: var(--color-fried-red-800); + /* --- danger: ลบ, error, destructive --- */ + --color-danger: var(--color-rose-600); + --color-danger-hover: var(--color-rose-700); + --color-danger-active: var(--color-rose-800); --color-danger-foreground: var(--color-white); + --color-danger-pale: var(--color-rose-50); + --color-danger-pale-foreground: var(--color-rose-800); - --color-info: var(--color-fried-blue-600); - --color-info-hover: var(--color-fried-blue-700); - --color-info-active: var(--color-fried-blue-800); + /* --- info: ข้อมูล, neutral info --- */ + --color-info: var(--color-blue-600); + --color-info-hover: var(--color-blue-700); + --color-info-active: var(--color-blue-800); --color-info-foreground: var(--color-white); + --color-info-pale: var(--color-blue-50); + --color-info-pale-foreground: var(--color-blue-800); + /* --- background: ชั้น 1 — พื้นหลัง page ทั้งหน้า --- */ --color-background: var(--color-white); + /* --- background-secondary: ชั้น 2 — sidebar, header, footer --- */ + --color-background-secondary: var(--color-neutral-50); + /* --- foreground: สีข้อความหลัก body text --- */ --color-foreground: var(--color-neutral-950); - --color-surface: var(--color-neutral-50); - --color-surface-hover: var(--color-neutral-100); - --color-surface-active: var(--color-neutral-200); + /* --- surface: ชั้น 3 — card, modal, dropdown --- */ + --color-surface: var(--color-neutral-100); + --color-surface-hover: var(--color-neutral-200); + --color-surface-active: var(--color-neutral-300); --color-surface-foreground: var(--color-neutral-900); + /* --- popover: tooltip, dropdown menu, combobox — ลอยสูงสุด --- */ + --color-popover: var(--color-white); + --color-popover-foreground: var(--color-neutral-950); + + /* --- muted: bg จางมาก (ghost button, tag, chip) --- */ --color-muted: var(--color-neutral-100); --color-muted-hover: var(--color-neutral-200); --color-muted-active: var(--color-neutral-300); --color-muted-foreground: var(--color-neutral-500); - --color-separator: var(--color-neutral-100); + /* --- separator: เส้นแบ่ง
, divider --- */ + --color-separator: var(--color-neutral-200); + /* --- skeleton: loading placeholder bg --- */ --color-skeleton: var(--color-neutral-200); + /* --- disabled: ปิดการใช้งาน --- */ --color-disabled: var(--color-neutral-100); --color-disabled-foreground: var(--color-neutral-400); - --color-heading: var(--color-neutral-950); + /* --- typography hierarchy --- */ + --color-heading: var(--color-neutral-950); /* h1-h6 — เข้มสุด */ + --color-foreground-secondary: var(--color-neutral-600); /* subtitle, description */ + --color-foreground-muted: var(--color-neutral-400); /* caption, hint, timestamp */ + /* --- code: inline `code` text --- */ --color-code: var(--color-neutral-800); + /* --- code-background: inline `code` bg --- */ --color-code-background: var(--color-neutral-100); - --color-mark: var(--color-fried-yellow-200); + /* --- mark: highlight bg --- */ + --color-mark: var(--color-amber-200); + /* --- mark-foreground: highlight text --- */ --color-mark-foreground: var(--color-neutral-950); + /* --- border: ขอบ default (input, card, outline button) --- */ --color-border: var(--color-neutral-200); + /* --- border-hover: ขอบตอน hover --- */ --color-border-hover: var(--color-neutral-300); + /* --- field: input/textarea/select bg --- */ --color-field: var(--color-white); + /* --- field-hover: input bg hover --- */ --color-field-hover: var(--color-neutral-50); + /* --- field-foreground: input text ที่พิมพ์ --- */ --color-field-foreground: var(--color-neutral-950); + /* --- field-border: input border --- */ --color-field-border: var(--color-neutral-300); + /* --- field-border-hover: input border hover --- */ --color-field-border-hover: var(--color-neutral-400); + /* --- field-placeholder: placeholder text --- */ --color-field-placeholder: var(--color-neutral-400); + /* --- selection: ::selection text highlight --- */ + --color-selection: var(--color-fried-lime-200); + --color-selection-foreground: var(--color-neutral-950); + + /* --- chart: data visualization --- */ + --color-chart-1: var(--color-fried-lime-500); + --color-chart-2: var(--color-blue-500); + --color-chart-3: var(--color-rose-500); + --color-chart-4: var(--color-amber-500); + --color-chart-5: var(--color-green-500); + + /* --- link:
text --- */ --color-link: var(--color-neutral-950); + /* --- link-hover: hover --- */ --color-link-hover: var(--color-neutral-600); + /* --- ring: focus ring สี (keyboard nav) --- */ --color-ring: var(--color-neutral-950); + /* --- overlay: modal/dialog backdrop สีทับ --- */ --color-overlay: oklch(0% 0 0 / 50%); } @layer base { + /* TODO: ไล่ใส่สี dark mode — ค่าว่าง fallback ไป @theme (light) อัตโนมัติ */ .dark, [data-theme="dark"] { - --color-primary: var(--color-fried-lime-500); - --color-primary-hover: var(--color-fried-lime-600); - --color-primary-active: var(--color-fried-lime-700); - --color-primary-foreground: var(--color-neutral-950); + /* --- primary: สี brand หลัก (CTA, solid button) --- */ + --color-primary: var(--color-fried-lime-500); /* bg ปุ่ม */ + --color-primary-hover: var(--color-fried-lime-600); /* bg hover */ + --color-primary-active: var(--color-fried-lime-700); /* bg pressed */ + --color-primary-foreground: var(--color-neutral-950); /* text บนปุ่ม */ + --color-primary-pale: var(--color-fried-lime-900); /* bg จาง (soft button, badge, alert) */ + --color-primary-pale-foreground: var(--color-fried-lime-200); /* text บน pale bg */ + /* --- secondary: ปุ่มรอง, less emphasis --- */ --color-secondary: var(--color-neutral-800); - --color-secondary-hover: var(--color-neutral-900); - --color-secondary-active: var(--color-neutral-950); - --color-secondary-foreground: var(--color-neutral-100); + --color-secondary-hover: var(--color-neutral-700); /* dark: สว่างขึ้น */ + --color-secondary-active: var(--color-neutral-600); + --color-secondary-foreground: var(--color-neutral-50); + --color-secondary-pale: var(--color-neutral-900); + --color-secondary-pale-foreground: var(--color-neutral-300); - --color-success: var(--color-fried-green-500); - --color-success-hover: var(--color-fried-green-600); - --color-success-active: var(--color-fried-green-700); + /* --- success: สำเร็จ, saved, confirmed --- */ + --color-success: var(--color-green-500); + --color-success-hover: var(--color-green-600); + --color-success-active: var(--color-green-700); --color-success-foreground: var(--color-white); + --color-success-pale: var(--color-green-950); + --color-success-pale-foreground: var(--color-green-200); - --color-warning: var(--color-fried-yellow-400); - --color-warning-hover: var(--color-fried-yellow-500); - --color-warning-active: var(--color-fried-yellow-600); + /* --- warning: เตือน, caution --- */ + --color-warning: var(--color-amber-500); + --color-warning-hover: var(--color-amber-600); + --color-warning-active: var(--color-amber-700); --color-warning-foreground: var(--color-neutral-950); + --color-warning-pale: var(--color-amber-950); + --color-warning-pale-foreground: var(--color-amber-200); - --color-danger: var(--color-fried-red-500); - --color-danger-hover: var(--color-fried-red-600); - --color-danger-active: var(--color-fried-red-700); + /* --- danger: ลบ, error, destructive --- */ + --color-danger: var(--color-rose-500); + --color-danger-hover: var(--color-rose-600); + --color-danger-active: var(--color-rose-700); --color-danger-foreground: var(--color-white); + --color-danger-pale: var(--color-rose-950); + --color-danger-pale-foreground: var(--color-rose-200); - --color-info: var(--color-fried-blue-500); - --color-info-hover: var(--color-fried-blue-600); - --color-info-active: var(--color-fried-blue-700); + /* --- info: ข้อมูล, neutral info --- */ + --color-info: var(--color-blue-500); + --color-info-hover: var(--color-blue-600); + --color-info-active: var(--color-blue-700); --color-info-foreground: var(--color-white); + --color-info-pale: var(--color-blue-950); + --color-info-pale-foreground: var(--color-blue-200); - --color-background: var(--color-black); - --color-foreground: var(--color-neutral-50); + /* --- background: ชั้น 1 — พื้นหลัง page ทั้งหน้า --- */ + --color-background: var(--color-black); /* #000 */ + /* --- background-secondary: ชั้น 2 — sidebar, header, footer --- */ + --color-background-secondary: var(--color-neutral-950); /* #0a0a0a */ + /* --- foreground: สีข้อความหลัก body text --- */ + --color-foreground: var(--color-neutral-50); /* #fafafa */ - --color-surface: var(--color-neutral-900); - --color-surface-hover: var(--color-neutral-800); - --color-surface-active: var(--color-neutral-700); + /* --- surface: ชั้น 3 — card, modal, dropdown --- */ + --color-surface: var(--color-neutral-900); /* #171717 */ + --color-surface-hover: var(--color-neutral-800); /* #262626 */ + --color-surface-active: var(--color-neutral-700); /* #404040 */ --color-surface-foreground: var(--color-neutral-50); - --color-muted: var(--color-neutral-800); + /* --- popover: tooltip, dropdown menu, combobox — ลอยสูงสุด --- */ + --color-popover: var(--color-neutral-800); /* #262626 สว่างกว่า surface */ + --color-popover-foreground: var(--color-neutral-50); + + /* --- muted: bg จางมาก (ghost button, tag, chip) --- */ + --color-muted: var(--color-neutral-800); /* #262626 */ --color-muted-hover: var(--color-neutral-700); --color-muted-active: var(--color-neutral-600); - --color-muted-foreground: var(--color-neutral-400); + --color-muted-foreground: var(--color-neutral-400); /* text รอง, caption, hint */ - --color-separator: var(--color-neutral-800); - --color-skeleton: var(--color-neutral-700); + /* --- separator: เส้นแบ่ง
, divider --- */ + --color-separator: var(--color-neutral-700); + /* --- skeleton: loading placeholder bg --- */ + --color-skeleton: var(--color-neutral-600); - --color-disabled: var(--color-neutral-800); + /* --- disabled: ปิดการใช้งาน --- */ + --color-disabled: var(--color-neutral-900); --color-disabled-foreground: var(--color-neutral-500); - --color-heading: var(--color-neutral-50); + /* --- typography hierarchy --- */ + --color-heading: var(--color-neutral-50); /* h1-h6 — สว่างสุด */ + --color-foreground-secondary: var(--color-neutral-400); /* subtitle, description */ + --color-foreground-muted: var(--color-neutral-500); /* caption, hint, timestamp */ + /* --- code: inline `code` text --- */ --color-code: var(--color-neutral-200); + /* --- code-background: inline `code` bg --- */ --color-code-background: var(--color-neutral-800); - --color-mark: var(--color-fried-yellow-800); - --color-mark-foreground: var(--color-fried-yellow-100); + /* --- mark: highlight bg --- */ + --color-mark: var(--color-amber-800); + /* --- mark-foreground: highlight text --- */ + --color-mark-foreground: var(--color-amber-100); - --color-border: var(--color-neutral-700); - --color-border-hover: var(--color-neutral-600); + /* --- border: ขอบ default (input, card, outline button) --- */ + --color-border: var(--color-neutral-800); + /* --- border-hover: ขอบตอน hover --- */ + --color-border-hover: var(--color-neutral-700); - --color-field: var(--color-neutral-900); - --color-field-hover: var(--color-neutral-800); + /* --- field: input/textarea/select bg --- */ + --color-field: var(--color-neutral-800); + /* --- field-hover: input bg hover --- */ + --color-field-hover: var(--color-neutral-700); + /* --- field-foreground: input text ที่พิมพ์ --- */ --color-field-foreground: var(--color-neutral-50); - --color-field-border: var(--color-neutral-700); - --color-field-border-hover: var(--color-neutral-600); + /* --- field-border: input border --- */ + --color-field-border: var(--color-neutral-600); + /* --- field-border-hover: input border hover --- */ + --color-field-border-hover: var(--color-neutral-500); + /* --- field-placeholder: placeholder text --- */ --color-field-placeholder: var(--color-neutral-500); - --color-link: var(--color-neutral-50); - --color-link-hover: var(--color-neutral-400); + /* --- selection: ::selection text highlight --- */ + --color-selection: oklch(from var(--color-blue-500) l c h / 10%); + --color-selection-foreground: var(--color-blue-500); + + /* --- link:
text --- */ + --color-link: var(--color-blue-500); + /* --- link-hover: hover --- */ + --color-link-hover: var(--color-blue-400); + /* --- ring: focus ring สี (keyboard nav) --- */ --color-ring: var(--color-neutral-50); + /* --- overlay: modal/dialog backdrop สีทับ --- */ --color-overlay: oklch(0% 0 0 / 70%); } } diff --git a/packages/styles/src/tokens/layout.css b/packages/styles/src/tokens/layout.css index ea127a7..2d3b142 100644 --- a/packages/styles/src/tokens/layout.css +++ b/packages/styles/src/tokens/layout.css @@ -13,8 +13,12 @@ --z-dialog: 40; --z-alert: 50; - --shadow-1: 0 1px 2px oklch(0% 0 0 / 10%); - --shadow-2: 0 2px 4px oklch(0% 0 0 / 10%); - --shadow-3: 0 3px 6px oklch(0% 0 0 / 10%); - --shadow-4: 0 5px 10px oklch(0% 0 0 / 10%); + /* --- shadow: Framer-inspired, light mode (black depth) --- */ + --shadow-1: 0 1px 2px oklch(0% 0 0 / 5%); /* subtle */ + --shadow-2: 0 2px 8px oklch(0% 0 0 / 8%), 0 1px 2px oklch(0% 0 0 / 6%); /* card */ + --shadow-3: 0 8px 24px oklch(0% 0 0 / 12%), 0 2px 8px oklch(0% 0 0 / 8%); /* dropdown, popover */ + --shadow-4: 0 25px 50px oklch(0% 0 0 / 25%), 0 5px 25px oklch(0% 0 0 / 15%); /* modal, dialog */ + /* --- shadow-border: Framer-style white glow border (dark mode) --- */ + --shadow-border: + inset 0 0.5px 0 oklch(100% 0 0 / 25%), 0 0 0 1px oklch(100% 0 0 / 10%); /* inner highlight + border glow */ } diff --git a/packages/styles/src/tokens/palette.css b/packages/styles/src/tokens/palette.css index 0a17099..c9e2ba4 100644 --- a/packages/styles/src/tokens/palette.css +++ b/packages/styles/src/tokens/palette.css @@ -10,52 +10,4 @@ --color-fried-lime-800: oklch(0.45 0.098 120.6); --color-fried-lime-900: oklch(0.299 0.065 120.65); --color-fried-lime-950: oklch(0.213 0.047 120.75); - - --color-fried-green-50: oklch(0.975 0.023 150); - --color-fried-green-100: oklch(0.96 0.023 150); - --color-fried-green-200: oklch(0.907 0.077 150); - --color-fried-green-300: oklch(0.867 0.108 150); - --color-fried-green-400: oklch(0.827 0.144 150); - --color-fried-green-500: oklch(0.78 0.173 150); - --color-fried-green-600: oklch(0.625 0.144 150); - --color-fried-green-700: oklch(0.462 0.113 150); - --color-fried-green-800: oklch(0.3 0.083 150); - --color-fried-green-900: oklch(0.149 0.055 150); - --color-fried-green-950: oklch(0.1 0.04 150); - - --color-fried-yellow-50: oklch(0.985 0.024 85); - --color-fried-yellow-100: oklch(0.98 0.024 85); - --color-fried-yellow-200: oklch(0.947 0.081 85); - --color-fried-yellow-300: oklch(0.927 0.114 85); - --color-fried-yellow-400: oklch(0.907 0.152 85); - --color-fried-yellow-500: oklch(0.88 0.183 85); - --color-fried-yellow-600: oklch(0.725 0.152 85); - --color-fried-yellow-700: oklch(0.562 0.12 85); - --color-fried-yellow-800: oklch(0.4 0.088 85); - --color-fried-yellow-900: oklch(0.249 0.059 85); - --color-fried-yellow-950: oklch(0.163 0.042 85); - - --color-fried-red-50: oklch(0.96 0.026 25); - --color-fried-red-100: oklch(0.93 0.026 25); - --color-fried-red-200: oklch(0.847 0.085 25); - --color-fried-red-300: oklch(0.777 0.121 25); - --color-fried-red-400: oklch(0.707 0.161 25); - --color-fried-red-500: oklch(0.63 0.193 25); - --color-fried-red-600: oklch(0.475 0.161 25); - --color-fried-red-700: oklch(0.312 0.126 25); - --color-fried-red-800: oklch(0.15 0.093 25); - --color-fried-red-900: oklch(0.1 0.062 25); - --color-fried-red-950: oklch(0.1 0.045 25); - - --color-fried-blue-50: oklch(0.96 0.023 260); - --color-fried-blue-100: oklch(0.93 0.023 260); - --color-fried-blue-200: oklch(0.847 0.077 260); - --color-fried-blue-300: oklch(0.777 0.108 260); - --color-fried-blue-400: oklch(0.707 0.144 260); - --color-fried-blue-500: oklch(0.63 0.173 260); - --color-fried-blue-600: oklch(0.475 0.144 260); - --color-fried-blue-700: oklch(0.312 0.113 260); - --color-fried-blue-800: oklch(0.15 0.083 260); - --color-fried-blue-900: oklch(0.1 0.055 260); - --color-fried-blue-950: oklch(0.1 0.04 260); } From 21df6747d7af782dbffaa5a92348796e86d67498 Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Sat, 28 Mar 2026 11:38:19 +0700 Subject: [PATCH 027/199] fix(styles): Clean comments, pale-border, fumadocs override --- apps/docs/src/app/globals.css | 55 ++++++----- packages/styles/src/tokens/colors.css | 136 ++++++++------------------ packages/styles/src/tokens/layout.css | 13 +-- 3 files changed, 77 insertions(+), 127 deletions(-) diff --git a/apps/docs/src/app/globals.css b/apps/docs/src/app/globals.css index dba5418..26004cf 100644 --- a/apps/docs/src/app/globals.css +++ b/apps/docs/src/app/globals.css @@ -5,30 +5,39 @@ :root, :host { - --color-fd-background: #fff; - --color-fd-foreground: #000; - --color-fd-card: #fff; - --color-fd-card-foreground: #000; - --color-fd-primary: #000; + --color-fd-background: var(--color-background); + --color-fd-foreground: var(--color-foreground); + --color-fd-muted: var(--color-muted); + --color-fd-muted-foreground: var(--color-muted-foreground); + --color-fd-card: var(--color-surface); + --color-fd-card-foreground: var(--color-surface-foreground); + --color-fd-popover: var(--color-popover); + --color-fd-popover-foreground: var(--color-popover-foreground); + --color-fd-border: var(--color-border); + --color-fd-primary: var(--color-black); + --color-fd-primary-foreground: var(--color-white); + --color-fd-secondary: var(--color-secondary); + --color-fd-secondary-foreground: var(--color-secondary-foreground); + --color-fd-accent: var(--color-muted); + --color-fd-accent-foreground: var(--color-foreground); + --color-fd-ring: var(--color-ring); } .dark { - --color-fd-background: #000; - --color-fd-foreground: #fff; - --color-fd-card: #000; - --color-fd-card-foreground: #fff; - --color-fd-border: #6663; - --color-fd-primary: #fff; - - /* --color-fd-muted: #212121; - --color-fd-muted-foreground: #b3b3b3cc; - --color-fd-popover: #1e1e1e; - --color-fd-popover-foreground: #dedede; - --color-fd-primary-foreground: #171717; - --color-fd-secondary: #212121; - --color-fd-secondary-foreground: #ebebeb; - --color-fd-accent: #6868684d; - --color-fd-accent-foreground: #e6e6e6; - --color-fd-ring: #8c8c8c; - --color-fd-overlay: #0003; */ + --color-fd-background: var(--color-background); + --color-fd-foreground: var(--color-foreground); + --color-fd-muted: var(--color-muted); + --color-fd-muted-foreground: var(--color-muted-foreground); + --color-fd-card: var(--color-surface); + --color-fd-card-foreground: var(--color-surface-foreground); + --color-fd-popover: var(--color-popover); + --color-fd-popover-foreground: var(--color-popover-foreground); + --color-fd-border: var(--color-border); + --color-fd-primary: var(--color-white); + --color-fd-primary-foreground: var(--color-black); + --color-fd-secondary: var(--color-secondary); + --color-fd-secondary-foreground: var(--color-secondary-foreground); + --color-fd-accent: var(--color-muted); + --color-fd-accent-foreground: var(--color-foreground); + --color-fd-ring: var(--color-ring); } diff --git a/packages/styles/src/tokens/colors.css b/packages/styles/src/tokens/colors.css index d03c269..d47f3ca 100644 --- a/packages/styles/src/tokens/colors.css +++ b/packages/styles/src/tokens/colors.css @@ -1,264 +1,208 @@ @custom-variant dark (&:is(.dark, [data-theme="dark"]) *); @theme { - /* --- primary: สี brand หลัก (CTA, solid button) --- */ --color-primary: var(--color-fried-lime-500); --color-primary-hover: var(--color-fried-lime-600); --color-primary-active: var(--color-fried-lime-700); --color-primary-foreground: var(--color-neutral-950); --color-primary-pale: var(--color-fried-lime-100); --color-primary-pale-foreground: var(--color-fried-lime-800); + --color-primary-pale-border: var(--color-fried-lime-200); - /* --- secondary: ปุ่มรอง, less emphasis --- */ --color-secondary: var(--color-neutral-100); --color-secondary-hover: var(--color-neutral-200); --color-secondary-active: var(--color-neutral-300); --color-secondary-foreground: var(--color-neutral-900); --color-secondary-pale: var(--color-neutral-50); --color-secondary-pale-foreground: var(--color-neutral-700); + --color-secondary-pale-border: var(--color-neutral-200); - /* --- success: สำเร็จ, saved, confirmed --- */ --color-success: var(--color-green-600); --color-success-hover: var(--color-green-700); --color-success-active: var(--color-green-800); --color-success-foreground: var(--color-white); --color-success-pale: var(--color-green-50); --color-success-pale-foreground: var(--color-green-800); + --color-success-pale-border: var(--color-green-200); - /* --- warning: เตือน, caution --- */ --color-warning: var(--color-amber-500); --color-warning-hover: var(--color-amber-600); --color-warning-active: var(--color-amber-700); --color-warning-foreground: var(--color-neutral-950); --color-warning-pale: var(--color-amber-50); --color-warning-pale-foreground: var(--color-amber-800); + --color-warning-pale-border: var(--color-amber-200); - /* --- danger: ลบ, error, destructive --- */ --color-danger: var(--color-rose-600); --color-danger-hover: var(--color-rose-700); --color-danger-active: var(--color-rose-800); --color-danger-foreground: var(--color-white); --color-danger-pale: var(--color-rose-50); --color-danger-pale-foreground: var(--color-rose-800); + --color-danger-pale-border: var(--color-rose-200); - /* --- info: ข้อมูล, neutral info --- */ --color-info: var(--color-blue-600); --color-info-hover: var(--color-blue-700); --color-info-active: var(--color-blue-800); --color-info-foreground: var(--color-white); --color-info-pale: var(--color-blue-50); --color-info-pale-foreground: var(--color-blue-800); + --color-info-pale-border: var(--color-blue-200); - /* --- background: ชั้น 1 — พื้นหลัง page ทั้งหน้า --- */ --color-background: var(--color-white); - /* --- background-secondary: ชั้น 2 — sidebar, header, footer --- */ --color-background-secondary: var(--color-neutral-50); - /* --- foreground: สีข้อความหลัก body text --- */ --color-foreground: var(--color-neutral-950); - /* --- surface: ชั้น 3 — card, modal, dropdown --- */ --color-surface: var(--color-neutral-100); --color-surface-hover: var(--color-neutral-200); --color-surface-active: var(--color-neutral-300); --color-surface-foreground: var(--color-neutral-900); - /* --- popover: tooltip, dropdown menu, combobox — ลอยสูงสุด --- */ --color-popover: var(--color-white); --color-popover-foreground: var(--color-neutral-950); - /* --- muted: bg จางมาก (ghost button, tag, chip) --- */ --color-muted: var(--color-neutral-100); --color-muted-hover: var(--color-neutral-200); --color-muted-active: var(--color-neutral-300); --color-muted-foreground: var(--color-neutral-500); - /* --- separator: เส้นแบ่ง
, divider --- */ --color-separator: var(--color-neutral-200); - /* --- skeleton: loading placeholder bg --- */ --color-skeleton: var(--color-neutral-200); - /* --- disabled: ปิดการใช้งาน --- */ --color-disabled: var(--color-neutral-100); --color-disabled-foreground: var(--color-neutral-400); - /* --- typography hierarchy --- */ - --color-heading: var(--color-neutral-950); /* h1-h6 — เข้มสุด */ - --color-foreground-secondary: var(--color-neutral-600); /* subtitle, description */ - --color-foreground-muted: var(--color-neutral-400); /* caption, hint, timestamp */ - /* --- code: inline `code` text --- */ + --color-heading: var(--color-neutral-950); + --color-foreground-secondary: var(--color-neutral-600); + --color-foreground-muted: var(--color-neutral-400); --color-code: var(--color-neutral-800); - /* --- code-background: inline `code` bg --- */ --color-code-background: var(--color-neutral-100); - /* --- mark: highlight bg --- */ --color-mark: var(--color-amber-200); - /* --- mark-foreground: highlight text --- */ --color-mark-foreground: var(--color-neutral-950); - /* --- border: ขอบ default (input, card, outline button) --- */ --color-border: var(--color-neutral-200); - /* --- border-hover: ขอบตอน hover --- */ --color-border-hover: var(--color-neutral-300); - /* --- field: input/textarea/select bg --- */ --color-field: var(--color-white); - /* --- field-hover: input bg hover --- */ --color-field-hover: var(--color-neutral-50); - /* --- field-foreground: input text ที่พิมพ์ --- */ --color-field-foreground: var(--color-neutral-950); - /* --- field-border: input border --- */ --color-field-border: var(--color-neutral-300); - /* --- field-border-hover: input border hover --- */ --color-field-border-hover: var(--color-neutral-400); - /* --- field-placeholder: placeholder text --- */ --color-field-placeholder: var(--color-neutral-400); - /* --- selection: ::selection text highlight --- */ --color-selection: var(--color-fried-lime-200); --color-selection-foreground: var(--color-neutral-950); - /* --- chart: data visualization --- */ --color-chart-1: var(--color-fried-lime-500); --color-chart-2: var(--color-blue-500); --color-chart-3: var(--color-rose-500); --color-chart-4: var(--color-amber-500); --color-chart-5: var(--color-green-500); - /* --- link:
text --- */ --color-link: var(--color-neutral-950); - /* --- link-hover: hover --- */ --color-link-hover: var(--color-neutral-600); - /* --- ring: focus ring สี (keyboard nav) --- */ --color-ring: var(--color-neutral-950); - /* --- overlay: modal/dialog backdrop สีทับ --- */ --color-overlay: oklch(0% 0 0 / 50%); } @layer base { - /* TODO: ไล่ใส่สี dark mode — ค่าว่าง fallback ไป @theme (light) อัตโนมัติ */ .dark, [data-theme="dark"] { - /* --- primary: สี brand หลัก (CTA, solid button) --- */ - --color-primary: var(--color-fried-lime-500); /* bg ปุ่ม */ - --color-primary-hover: var(--color-fried-lime-600); /* bg hover */ - --color-primary-active: var(--color-fried-lime-700); /* bg pressed */ - --color-primary-foreground: var(--color-neutral-950); /* text บนปุ่ม */ - --color-primary-pale: var(--color-fried-lime-900); /* bg จาง (soft button, badge, alert) */ - --color-primary-pale-foreground: var(--color-fried-lime-200); /* text บน pale bg */ - - /* --- secondary: ปุ่มรอง, less emphasis --- */ + --color-primary: var(--color-fried-lime-500); + --color-primary-hover: var(--color-fried-lime-600); + --color-primary-active: var(--color-fried-lime-700); + --color-primary-foreground: var(--color-neutral-950); + --color-primary-pale: var(--color-fried-lime-900); + --color-primary-pale-foreground: var(--color-fried-lime-200); + --color-primary-pale-border: var(--color-fried-lime-800); + --color-secondary: var(--color-neutral-800); - --color-secondary-hover: var(--color-neutral-700); /* dark: สว่างขึ้น */ + --color-secondary-hover: var(--color-neutral-700); --color-secondary-active: var(--color-neutral-600); --color-secondary-foreground: var(--color-neutral-50); --color-secondary-pale: var(--color-neutral-900); --color-secondary-pale-foreground: var(--color-neutral-300); + --color-secondary-pale-border: var(--color-neutral-700); - /* --- success: สำเร็จ, saved, confirmed --- */ --color-success: var(--color-green-500); --color-success-hover: var(--color-green-600); --color-success-active: var(--color-green-700); --color-success-foreground: var(--color-white); - --color-success-pale: var(--color-green-950); + --color-success-pale: var(--color-green-900); --color-success-pale-foreground: var(--color-green-200); + --color-success-pale-border: var(--color-green-700); - /* --- warning: เตือน, caution --- */ --color-warning: var(--color-amber-500); --color-warning-hover: var(--color-amber-600); --color-warning-active: var(--color-amber-700); --color-warning-foreground: var(--color-neutral-950); - --color-warning-pale: var(--color-amber-950); + --color-warning-pale: var(--color-amber-900); --color-warning-pale-foreground: var(--color-amber-200); + --color-warning-pale-border: var(--color-amber-700); - /* --- danger: ลบ, error, destructive --- */ --color-danger: var(--color-rose-500); --color-danger-hover: var(--color-rose-600); --color-danger-active: var(--color-rose-700); --color-danger-foreground: var(--color-white); - --color-danger-pale: var(--color-rose-950); + --color-danger-pale: var(--color-rose-900); --color-danger-pale-foreground: var(--color-rose-200); + --color-danger-pale-border: var(--color-rose-700); - /* --- info: ข้อมูล, neutral info --- */ --color-info: var(--color-blue-500); --color-info-hover: var(--color-blue-600); --color-info-active: var(--color-blue-700); --color-info-foreground: var(--color-white); - --color-info-pale: var(--color-blue-950); + --color-info-pale: var(--color-blue-900); --color-info-pale-foreground: var(--color-blue-200); + --color-info-pale-border: var(--color-blue-700); + + --color-background: var(--color-black); + --color-background-secondary: var(--color-neutral-950); + --color-foreground: var(--color-neutral-50); - /* --- background: ชั้น 1 — พื้นหลัง page ทั้งหน้า --- */ - --color-background: var(--color-black); /* #000 */ - /* --- background-secondary: ชั้น 2 — sidebar, header, footer --- */ - --color-background-secondary: var(--color-neutral-950); /* #0a0a0a */ - /* --- foreground: สีข้อความหลัก body text --- */ - --color-foreground: var(--color-neutral-50); /* #fafafa */ - - /* --- surface: ชั้น 3 — card, modal, dropdown --- */ - --color-surface: var(--color-neutral-900); /* #171717 */ - --color-surface-hover: var(--color-neutral-800); /* #262626 */ - --color-surface-active: var(--color-neutral-700); /* #404040 */ + --color-surface: var(--color-neutral-900); + --color-surface-hover: var(--color-neutral-800); + --color-surface-active: var(--color-neutral-700); --color-surface-foreground: var(--color-neutral-50); - /* --- popover: tooltip, dropdown menu, combobox — ลอยสูงสุด --- */ - --color-popover: var(--color-neutral-800); /* #262626 สว่างกว่า surface */ + --color-popover: var(--color-neutral-800); --color-popover-foreground: var(--color-neutral-50); - /* --- muted: bg จางมาก (ghost button, tag, chip) --- */ - --color-muted: var(--color-neutral-800); /* #262626 */ + --color-muted: var(--color-neutral-800); --color-muted-hover: var(--color-neutral-700); --color-muted-active: var(--color-neutral-600); - --color-muted-foreground: var(--color-neutral-400); /* text รอง, caption, hint */ + --color-muted-foreground: var(--color-neutral-400); - /* --- separator: เส้นแบ่ง
, divider --- */ --color-separator: var(--color-neutral-700); - /* --- skeleton: loading placeholder bg --- */ --color-skeleton: var(--color-neutral-600); - /* --- disabled: ปิดการใช้งาน --- */ --color-disabled: var(--color-neutral-900); --color-disabled-foreground: var(--color-neutral-500); - /* --- typography hierarchy --- */ - --color-heading: var(--color-neutral-50); /* h1-h6 — สว่างสุด */ - --color-foreground-secondary: var(--color-neutral-400); /* subtitle, description */ - --color-foreground-muted: var(--color-neutral-500); /* caption, hint, timestamp */ - /* --- code: inline `code` text --- */ + --color-heading: var(--color-neutral-50); + --color-foreground-secondary: var(--color-neutral-400); + --color-foreground-muted: var(--color-neutral-500); --color-code: var(--color-neutral-200); - /* --- code-background: inline `code` bg --- */ --color-code-background: var(--color-neutral-800); - /* --- mark: highlight bg --- */ --color-mark: var(--color-amber-800); - /* --- mark-foreground: highlight text --- */ --color-mark-foreground: var(--color-amber-100); - /* --- border: ขอบ default (input, card, outline button) --- */ --color-border: var(--color-neutral-800); - /* --- border-hover: ขอบตอน hover --- */ --color-border-hover: var(--color-neutral-700); - /* --- field: input/textarea/select bg --- */ --color-field: var(--color-neutral-800); - /* --- field-hover: input bg hover --- */ --color-field-hover: var(--color-neutral-700); - /* --- field-foreground: input text ที่พิมพ์ --- */ --color-field-foreground: var(--color-neutral-50); - /* --- field-border: input border --- */ --color-field-border: var(--color-neutral-600); - /* --- field-border-hover: input border hover --- */ --color-field-border-hover: var(--color-neutral-500); - /* --- field-placeholder: placeholder text --- */ --color-field-placeholder: var(--color-neutral-500); - /* --- selection: ::selection text highlight --- */ --color-selection: oklch(from var(--color-blue-500) l c h / 10%); --color-selection-foreground: var(--color-blue-500); - /* --- link:
text --- */ --color-link: var(--color-blue-500); - /* --- link-hover: hover --- */ --color-link-hover: var(--color-blue-400); - /* --- ring: focus ring สี (keyboard nav) --- */ --color-ring: var(--color-neutral-50); - /* --- overlay: modal/dialog backdrop สีทับ --- */ --color-overlay: oklch(0% 0 0 / 70%); } } diff --git a/packages/styles/src/tokens/layout.css b/packages/styles/src/tokens/layout.css index 2d3b142..9fd4a62 100644 --- a/packages/styles/src/tokens/layout.css +++ b/packages/styles/src/tokens/layout.css @@ -13,12 +13,9 @@ --z-dialog: 40; --z-alert: 50; - /* --- shadow: Framer-inspired, light mode (black depth) --- */ - --shadow-1: 0 1px 2px oklch(0% 0 0 / 5%); /* subtle */ - --shadow-2: 0 2px 8px oklch(0% 0 0 / 8%), 0 1px 2px oklch(0% 0 0 / 6%); /* card */ - --shadow-3: 0 8px 24px oklch(0% 0 0 / 12%), 0 2px 8px oklch(0% 0 0 / 8%); /* dropdown, popover */ - --shadow-4: 0 25px 50px oklch(0% 0 0 / 25%), 0 5px 25px oklch(0% 0 0 / 15%); /* modal, dialog */ - /* --- shadow-border: Framer-style white glow border (dark mode) --- */ - --shadow-border: - inset 0 0.5px 0 oklch(100% 0 0 / 25%), 0 0 0 1px oklch(100% 0 0 / 10%); /* inner highlight + border glow */ + --shadow-1: 0 1px 2px oklch(0% 0 0 / 5%); + --shadow-2: 0 2px 8px oklch(0% 0 0 / 8%), 0 1px 2px oklch(0% 0 0 / 6%); + --shadow-3: 0 8px 24px oklch(0% 0 0 / 12%), 0 2px 8px oklch(0% 0 0 / 8%); + --shadow-4: 0 25px 50px oklch(0% 0 0 / 25%), 0 5px 25px oklch(0% 0 0 / 15%); + --shadow-border: inset 0 0.5px 0 oklch(100% 0 0 / 25%), 0 0 0 1px oklch(100% 0 0 / 10%); } From 142ab4d6a516db391337aa6cbf2e12b3db6a11ae Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Sat, 28 Mar 2026 12:15:52 +0700 Subject: [PATCH 028/199] feat(docs): Add overview pages, colors swatch, @theme static --- apps/docs/src/app/docs/[[...slug]]/page.tsx | 2 + apps/docs/src/components/ColorSwatch.tsx | 43 ++++ apps/docs/src/content/docs/index.mdx | 29 +-- apps/docs/src/content/docs/meta.json | 12 +- .../content/docs/overview/introduction.mdx | 29 +++ .../src/content/docs/overview/quick-start.mdx | 27 +++ apps/docs/src/content/docs/styles/colors.mdx | 200 ++++++++++++++++++ .../src/content/docs/styles/design-tokens.mdx | 125 +++++++++++ packages/styles/src/tokens/colors.css | 2 +- 9 files changed, 448 insertions(+), 21 deletions(-) create mode 100644 apps/docs/src/components/ColorSwatch.tsx create mode 100644 apps/docs/src/content/docs/overview/introduction.mdx create mode 100644 apps/docs/src/content/docs/overview/quick-start.mdx create mode 100644 apps/docs/src/content/docs/styles/colors.mdx create mode 100644 apps/docs/src/content/docs/styles/design-tokens.mdx diff --git a/apps/docs/src/app/docs/[[...slug]]/page.tsx b/apps/docs/src/app/docs/[[...slug]]/page.tsx index d752884..fb9f358 100644 --- a/apps/docs/src/app/docs/[[...slug]]/page.tsx +++ b/apps/docs/src/app/docs/[[...slug]]/page.tsx @@ -2,6 +2,7 @@ import defaultMdxComponents from "fumadocs-ui/mdx"; import { DocsBody, DocsDescription, DocsPage, DocsTitle } from "fumadocs-ui/page"; import { notFound } from "next/navigation"; +import { ColorSwatch } from "@/components/ColorSwatch"; import { CopyMarkdown } from "@/components/CopyMarkdown"; import { ButtonAsLink, @@ -40,6 +41,7 @@ const mdxComponents = { ButtonSizes, ButtonVariants, ButtonWithIcons, + ColorSwatch, Preview, }; diff --git a/apps/docs/src/components/ColorSwatch.tsx b/apps/docs/src/components/ColorSwatch.tsx new file mode 100644 index 0000000..72e68e9 --- /dev/null +++ b/apps/docs/src/components/ColorSwatch.tsx @@ -0,0 +1,43 @@ +"use client"; + +interface ColorSwatchProps { + colors: ColorItemProps[]; +} + +export interface ColorItemProps { + name: string; + token: string; +} + +function SwatchItem(props: Readonly): React.JSX.Element { + const { name, token } = props; + const label = `${name} color swatch`; + const bgStyle = { backgroundColor: `var(--color-${token})` }; + + return ( +
+
+ + {name} +
+ ); +} + +function ColorSwatch(props: Readonly): React.JSX.Element { + const { colors } = props; + + return ( +
+ {colors.map((color) => ( + + ))} +
+ ); +} + +export { ColorSwatch }; diff --git a/apps/docs/src/content/docs/index.mdx b/apps/docs/src/content/docs/index.mdx index 4c8fb23..7c98cc1 100644 --- a/apps/docs/src/content/docs/index.mdx +++ b/apps/docs/src/content/docs/index.mdx @@ -1,27 +1,18 @@ --- -title: Getting Started -description: Beautiful, accessible React components for building modern web apps at scale. +title: Welcome to Fried UI +description: Beautiful, accessible React components built on React Aria + Tailwind CSS v4. --- -## Installation +## Build better interfaces, faster -```bash -pnpm add @fried-ui/react @fried-ui/styles -``` +Fried UI is a production-ready React component library with accessible primitives, golden ratio spacing, and a Framer-inspired dark theme. -Import styles in your CSS entry file: +## Get Started -```css -@import "tailwindcss"; -@import "@fried-ui/styles"; -``` +- [Introduction](/docs/overview/introduction) — What is fried-ui and why use it +- [Quick Start](/docs/overview/quick-start) — Install and render your first component +- [Design Tokens](/docs/overview/design-tokens) — Color, spacing, elevation, and motion tokens -## Usage +## Components -```tsx -import { Button } from "@fried-ui/react"; - -export default function App() { - return ; -} -``` +- [Button](/docs/components/button) — 8 variants, icon-only, render props, ref forwarding diff --git a/apps/docs/src/content/docs/meta.json b/apps/docs/src/content/docs/meta.json index a788c9b..1051813 100644 --- a/apps/docs/src/content/docs/meta.json +++ b/apps/docs/src/content/docs/meta.json @@ -1,4 +1,14 @@ { "title": "Documentation", - "pages": ["index", "---Components---", "components/button"] + "pages": [ + "---Overview---", + "index", + "overview/introduction", + "overview/quick-start", + "---Styles---", + "styles/design-tokens", + "styles/colors", + "---Components---", + "components/button" + ] } diff --git a/apps/docs/src/content/docs/overview/introduction.mdx b/apps/docs/src/content/docs/overview/introduction.mdx new file mode 100644 index 0000000..bb84f4f --- /dev/null +++ b/apps/docs/src/content/docs/overview/introduction.mdx @@ -0,0 +1,29 @@ +--- +title: Introduction +description: Beautiful, accessible React components built on React Aria + Tailwind CSS v4. +--- + +## What is fried-ui? + +fried-ui is a React component library built on top of [React Aria Components](https://react-spectrum.adobe.com/react-aria/) and [Tailwind CSS v4](https://tailwindcss.com/). It provides accessible, composable UI primitives with a design system powered by the golden ratio. + +## Key Features + +- **Accessible by default** — Built on React Aria, every component has keyboard navigation, focus management, and screen reader support out of the box +- **Golden ratio spacing** — All sizing uses φ (1.618) for mathematically harmonious proportions +- **Pure CSS tokens** — Design tokens live in `@fried-ui/styles` as plain CSS, usable with any framework +- **Dark mode** — Framer-inspired dark theme with 4-layer elevation system +- **LLM-friendly** — Single `variant` prop, inline union types for IDE autocomplete, structured docs for AI consumption + +## Architecture + +```text +@fried-ui/react → React 19 components (behavior + accessibility) +@fried-ui/styles → Pure CSS: design tokens + component BEM styles +``` + +`@fried-ui/styles` has no React dependency — it can be used with Vue, Svelte, or plain HTML. + +## Components + +- [Button](/docs/components/button) — Accessible button with 8 variants, golden ratio spacing, and icon-only mode diff --git a/apps/docs/src/content/docs/overview/quick-start.mdx b/apps/docs/src/content/docs/overview/quick-start.mdx new file mode 100644 index 0000000..bd4405a --- /dev/null +++ b/apps/docs/src/content/docs/overview/quick-start.mdx @@ -0,0 +1,27 @@ +--- +title: Quick Start +description: Install fried-ui and render your first component in under a minute. +--- + +## Installation + +```bash +pnpm add @fried-ui/react @fried-ui/styles +``` + +Import styles in your CSS entry file: + +```css +@import "tailwindcss"; +@import "@fried-ui/styles"; +``` + +## Usage + +```tsx +import { Button } from "@fried-ui/react"; + +export default function App() { + return ; +} +``` diff --git a/apps/docs/src/content/docs/styles/colors.mdx b/apps/docs/src/content/docs/styles/colors.mdx new file mode 100644 index 0000000..9ea7ff2 --- /dev/null +++ b/apps/docs/src/content/docs/styles/colors.mdx @@ -0,0 +1,200 @@ +--- +title: Colors +description: Semantic color tokens for light and dark mode — 7 tokens per group, 4-layer elevation, typography hierarchy. +--- + +## Primary Colors + +Your main brand color used for primary actions and CTAs. + + + +## Secondary Colors + +For secondary actions and less emphasis. + + + +## Status Colors + +For alerts, validation, and status messages. + +### Success + + + +### Warning + + + +### Danger + + + +### Info + + + +## Background & Surface + +4 elevation layers from lowest to highest. + + + +## Muted Colors + +For ghost buttons, tags, chips, and subtle backgrounds. + + + +## Typography + +Text color hierarchy from strongest to weakest. + + + +## Border & Separator + + + +## Form Field Colors + +For consistent form field styling across input components. + + + +## Other Colors + + + +## Chart Colors + +5 colors for data visualization. + + + +## How to Use + +```tsx + +
+ Alert +
+``` + +## Dark Mode + +All colors switch automatically via `.dark` class or `data-theme="dark"`. Toggle the theme on this page to see the difference. diff --git a/apps/docs/src/content/docs/styles/design-tokens.mdx b/apps/docs/src/content/docs/styles/design-tokens.mdx new file mode 100644 index 0000000..61d2413 --- /dev/null +++ b/apps/docs/src/content/docs/styles/design-tokens.mdx @@ -0,0 +1,125 @@ +--- +title: Design Tokens +description: Color, spacing, elevation, and motion tokens that power every fried-ui component. +--- + +## Overview + +fried-ui uses CSS custom properties as design tokens. All tokens are defined in `@fried-ui/styles` and automatically available as Tailwind CSS v4 utilities. + +```css +@import "tailwindcss"; +@import "@fried-ui/styles"; +``` + +## Semantic Colors + +Every semantic color has 7 tokens: + +| Token | Purpose | +| ------------------------ | ------------------------------------ | +| `{name}` | Solid background (button, badge) | +| `{name}-hover` | Background on hover | +| `{name}-active` | Background on pressed | +| `{name}-foreground` | Text on solid background | +| `{name}-pale` | Soft background (alert, soft button) | +| `{name}-pale-foreground` | Text on pale background | +| `{name}-pale-border` | Border on pale background | + +Available groups: `primary`, `secondary`, `success`, `warning`, `danger`, `info` + +```tsx +
Solid
+
Soft
+``` + +## Layout Elevation + +4 layers from lowest to highest: + +| Layer | Token | Light | Dark | Usage | +| ----- | ---------------------- | ----------- | ----------- | ----------------------- | +| 1 | `background` | white | black | Page background | +| 2 | `background-secondary` | neutral-50 | neutral-950 | Sidebar, header, footer | +| 3 | `surface` | neutral-100 | neutral-900 | Card, modal | +| 4 | `popover` | white | neutral-800 | Tooltip, dropdown | + +```tsx +
Page
+ +
Card
+
Dropdown
+``` + +## Typography Hierarchy + +| Token | Purpose | +| ---------------------- | ------------------------ | +| `heading` | h1-h6 headings | +| `foreground` | Body text | +| `foreground-secondary` | Subtitle, description | +| `foreground-muted` | Caption, hint, timestamp | + +```tsx +

Title

+

Body text

+

Description

+Updated 2 hours ago +``` + +## Spacing (Golden Ratio) + +All spacing derives from font-size × φ (1.618): + +| Property | Formula | sm | md | lg | xl | +| -------------- | --------- | -------- | -------- | -------- | -------- | +| Font | x | 0.875rem | 1rem | 1.25rem | 1.5rem | +| Padding Inline | x | 0.875rem | 1rem | 1.25rem | 1.5rem | +| Padding Block | x × 0.485 | 0.375rem | 0.5rem | 0.625rem | 0.75rem | +| Gap | x / φ | 0.541rem | 0.618rem | 0.773rem | 0.927rem | + +## Shadow (Framer-inspired) + +| Token | Usage | +| --------------- | ------------------------ | +| `shadow-1` | Subtle hover lift | +| `shadow-2` | Card, panel | +| `shadow-3` | Dropdown, popover | +| `shadow-4` | Modal, dialog | +| `shadow-border` | Dark mode card edge glow | + +```tsx +
Card with shadow
+``` + +## Border & Field + +| Token | Purpose | +| ------------------- | -------------------------------------------- | +| `border` | Default border (card, input, outline button) | +| `border-hover` | Border on hover | +| `field` | Input background | +| `field-border` | Input border | +| `field-placeholder` | Placeholder text | + +## Dark Mode + +Dark mode activates via `.dark` class or `data-theme="dark"`: + +```html + +``` + +All tokens automatically switch. Light values are defined in `@theme`, dark overrides in `@layer base .dark`. + +## Chart Colors + +5 colors for data visualization: + +| Token | Source | +| --------- | ------------------ | +| `chart-1` | fried-lime (brand) | +| `chart-2` | blue | +| `chart-3` | rose | +| `chart-4` | amber | +| `chart-5` | green | diff --git a/packages/styles/src/tokens/colors.css b/packages/styles/src/tokens/colors.css index d47f3ca..2357244 100644 --- a/packages/styles/src/tokens/colors.css +++ b/packages/styles/src/tokens/colors.css @@ -1,6 +1,6 @@ @custom-variant dark (&:is(.dark, [data-theme="dark"]) *); -@theme { +@theme static { --color-primary: var(--color-fried-lime-500); --color-primary-hover: var(--color-fried-lime-600); --color-primary-active: var(--color-fried-lime-700); From ae32b3b8f8479bdd3c4a44f57017c31a5162be2d Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Sat, 28 Mar 2026 12:20:53 +0700 Subject: [PATCH 029/199] feat(docs): Add overview pages, colors swatch, @theme static --- apps/docs/src/components/ColorSwatch.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/src/components/ColorSwatch.tsx b/apps/docs/src/components/ColorSwatch.tsx index 72e68e9..411c696 100644 --- a/apps/docs/src/components/ColorSwatch.tsx +++ b/apps/docs/src/components/ColorSwatch.tsx @@ -17,7 +17,7 @@ function SwatchItem(props: Readonly): React.JSX.Element { return (
Date: Sat, 28 Mar 2026 12:31:03 +0700 Subject: [PATCH 030/199] chore(ci): Remove SonarCloud workflow --- .github/workflows/sonar.yml | 29 ----------------------------- 1 file changed, 29 deletions(-) delete mode 100644 .github/workflows/sonar.yml diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml deleted file mode 100644 index f254b3c..0000000 --- a/.github/workflows/sonar.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: SonarCloud - -on: - pull_request: - branches: [main] - push: - branches: [main] - -permissions: - contents: read - pull-requests: read - -jobs: - sonar: - name: SonarCloud - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - uses: ./.github/actions/setup - - - name: Run tests with coverage - run: pnpm --filter=@fried-ui/react test - - - name: SonarCloud Scan - uses: SonarSource/sonarqube-scan-action@2f77a1ec69fb1d595b06f35ab27e97605bdef703 # v5 - env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} From fc497c02723c0027b581efa7b80b32c6f6f8d21c Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Sat, 28 Mar 2026 12:38:06 +0700 Subject: [PATCH 031/199] ci(pr): Add PR title validation workflow --- .github/workflows/pr-title.yml | 56 ++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 .github/workflows/pr-title.yml diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml new file mode 100644 index 0000000..47405fd --- /dev/null +++ b/.github/workflows/pr-title.yml @@ -0,0 +1,56 @@ +name: Pull Request Title Validation + +on: + pull_request: + types: [opened, edited, reopened, synchronize] + +jobs: + main: + name: Validate Pull Request Title + runs-on: ubuntu-latest + if: ${{ github.event.pull_request.draft == false }} + permissions: + contents: read + pull-requests: write + env: + IS_TRUSTED_BOT: ${{ github.actor == 'github-actions[bot]' || github.actor == 'changeset-bot' || github.event.pull_request.user.type == 'Bot' }} + + steps: + - name: Fast-pass for trusted bots + if: ${{ env.IS_TRUSTED_BOT == 'true' }} + run: echo "Trusted bot PR → skip PR title validation but report success." + + - uses: amannn/action-semantic-pull-request@v5 + if: ${{ env.IS_TRUSTED_BOT != 'true' }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + types: | + build + chore + ci + docs + feat + fix + perf + refactor + revert + style + test + requireScope: true + disallowScopes: | + [A-Z]+ + subjectPattern: ^(?![A-Z]).{1,50}$ + subjectPatternError: | + The subject "{subject}" found in the pull request title "{title}" + didn't match the configured pattern. Please ensure that the subject: + - doesn't start with an uppercase character + - is not empty + - is maximum 50 characters long + ignoreLabels: | + bot + ignore-semantic-pull-request + headerPattern: '^(\w*)(?:\(([\w$.\-*/ ]*)\))?: (.*)' + headerPatternCorrespondence: type, scope, subject + validateSingleCommit: true + validateSingleCommitMatchesPrTitle: true From 6a3279cbad2cac22e3d929256222bb4c1907aad2 Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Sat, 28 Mar 2026 12:41:22 +0700 Subject: [PATCH 032/199] fix(ci): Pin action SHA for security --- .github/workflows/pr-title.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml index 47405fd..d5a20ab 100644 --- a/.github/workflows/pr-title.yml +++ b/.github/workflows/pr-title.yml @@ -20,7 +20,7 @@ jobs: if: ${{ env.IS_TRUSTED_BOT == 'true' }} run: echo "Trusted bot PR → skip PR title validation but report success." - - uses: amannn/action-semantic-pull-request@v5 + - uses: amannn/action-semantic-pull-request@e32d7e603df1aa1ba07e981f2a23455dee596825 # v5 if: ${{ env.IS_TRUSTED_BOT != 'true' }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From d9ae6891b8ed992c5cb5b7d76bc91f914c020e1c Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Sat, 28 Mar 2026 12:55:19 +0700 Subject: [PATCH 033/199] feat(docs): Add custom navbar header --- apps/docs/src/app/docs/layout.tsx | 8 ++- apps/docs/src/app/layout.tsx | 7 ++- apps/docs/src/components/ColorSwatch.tsx | 4 +- apps/docs/src/components/Navbar.tsx | 70 ++++++++++++++++++++++++ 4 files changed, 85 insertions(+), 4 deletions(-) create mode 100644 apps/docs/src/components/Navbar.tsx diff --git a/apps/docs/src/app/docs/layout.tsx b/apps/docs/src/app/docs/layout.tsx index b925012..47c6f82 100644 --- a/apps/docs/src/app/docs/layout.tsx +++ b/apps/docs/src/app/docs/layout.tsx @@ -8,10 +8,16 @@ interface LayoutProps { children: ReactNode; } +const navConfig = { enabled: false }; + function Layout(props: Readonly): React.JSX.Element { const { children } = props; - return {children}; + return ( + + {children} + + ); } export default Layout; diff --git a/apps/docs/src/app/layout.tsx b/apps/docs/src/app/layout.tsx index f5317e5..1cda41b 100644 --- a/apps/docs/src/app/layout.tsx +++ b/apps/docs/src/app/layout.tsx @@ -6,6 +6,8 @@ import type { ReactNode } from "react"; import { RootProvider } from "fumadocs-ui/provider/next"; import localFont from "next/font/local"; +import { Navbar } from "@/components/Navbar"; + const geistSans = localFont({ src: "./fonts/GeistVF.woff", variable: "--font-geist-sans", @@ -32,7 +34,10 @@ function RootLayout(props: Readonly): React.JSX.Element { return ( - {children} + + + {children} + ); diff --git a/apps/docs/src/components/ColorSwatch.tsx b/apps/docs/src/components/ColorSwatch.tsx index 411c696..a939f5b 100644 --- a/apps/docs/src/components/ColorSwatch.tsx +++ b/apps/docs/src/components/ColorSwatch.tsx @@ -17,7 +17,7 @@ function SwatchItem(props: Readonly): React.JSX.Element { return (
): React.JSX.Element { const { colors } = props; return ( -
+
{colors.map((color) => ( ))} diff --git a/apps/docs/src/components/Navbar.tsx b/apps/docs/src/components/Navbar.tsx new file mode 100644 index 0000000..9fdffa2 --- /dev/null +++ b/apps/docs/src/components/Navbar.tsx @@ -0,0 +1,70 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +const NAV_ITEMS = [ + { label: "Getting Started", href: "/docs/overview/introduction" }, + { label: "Styles", href: "/docs/styles/colors" }, + { label: "Components", href: "/docs/components/button" }, +]; + +interface NavItemProps { + href: string; + label: string; + pathname: string; +} + +function NavItem(props: Readonly): React.JSX.Element { + const { href, label, pathname } = props; + const isActive = pathname.startsWith(href.split("/").slice(0, 4).join("/")); + const className = `rounded-md px-3 py-1.5 text-sm font-medium transition-colors ${isActive ? "text-foreground" : "text-foreground-muted hover:text-foreground"}`; + + return ( + + {label} + + ); +} + +function Navbar(): React.JSX.Element { + const pathname = usePathname(); + + return ( +
+ +
+ ); +} + +export { Navbar }; From 7866f8ff5c08d4cd2cbfd05e2d1eaba1826de5ba Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Sat, 28 Mar 2026 13:47:32 +0700 Subject: [PATCH 034/199] feat(docs): Notebook layout, full width, fumadocs nav --- apps/docs/src/app/docs/layout.tsx | 35 +++++++++++++-- apps/docs/src/app/globals.css | 16 +++++-- apps/docs/src/app/layout.tsx | 7 +-- apps/docs/src/components/Navbar.tsx | 70 ----------------------------- 4 files changed, 45 insertions(+), 83 deletions(-) delete mode 100644 apps/docs/src/components/Navbar.tsx diff --git a/apps/docs/src/app/docs/layout.tsx b/apps/docs/src/app/docs/layout.tsx index 47c6f82..c996f02 100644 --- a/apps/docs/src/app/docs/layout.tsx +++ b/apps/docs/src/app/docs/layout.tsx @@ -1,6 +1,6 @@ import type { ReactNode } from "react"; -import { DocsLayout } from "fumadocs-ui/layouts/docs"; +import { DocsLayout } from "fumadocs-ui/layouts/notebook"; import { source } from "@/lib/source"; @@ -8,13 +8,42 @@ interface LayoutProps { children: ReactNode; } -const navConfig = { enabled: false }; +const navConfig = { + title: "fried-ui", + url: "/docs", +}; + +const linksConfig = [ + { + text: "Storybook", + url: "https://fried-ui-storybook.vercel.app", + external: true, + }, + { + text: "GitHub", + url: "https://github.com/fried-day/fried-ui", + external: true, + }, +]; + +const sidebarConfig = { + collapsible: false, + defaultOpenLevel: 0, +}; + +const containerConfig = { className: "max-w-none" }; function Layout(props: Readonly): React.JSX.Element { const { children } = props; return ( - + {children} ); diff --git a/apps/docs/src/app/globals.css b/apps/docs/src/app/globals.css index 26004cf..78b681e 100644 --- a/apps/docs/src/app/globals.css +++ b/apps/docs/src/app/globals.css @@ -9,8 +9,8 @@ --color-fd-foreground: var(--color-foreground); --color-fd-muted: var(--color-muted); --color-fd-muted-foreground: var(--color-muted-foreground); - --color-fd-card: var(--color-surface); - --color-fd-card-foreground: var(--color-surface-foreground); + --color-fd-card: var(--color-background); + --color-fd-card-foreground: var(--color-foreground); --color-fd-popover: var(--color-popover); --color-fd-popover-foreground: var(--color-popover-foreground); --color-fd-border: var(--color-border); @@ -28,8 +28,8 @@ --color-fd-foreground: var(--color-foreground); --color-fd-muted: var(--color-muted); --color-fd-muted-foreground: var(--color-muted-foreground); - --color-fd-card: var(--color-surface); - --color-fd-card-foreground: var(--color-surface-foreground); + --color-fd-card: var(--color-background); + --color-fd-card-foreground: var(--color-foreground); --color-fd-popover: var(--color-popover); --color-fd-popover-foreground: var(--color-popover-foreground); --color-fd-border: var(--color-border); @@ -41,3 +41,11 @@ --color-fd-accent-foreground: var(--color-foreground); --color-fd-ring: var(--color-ring); } + +#nd-subnav > [data-header-body] { + border: 0 !important; +} + +#nd-sidebar { + border: 0 !important; +} diff --git a/apps/docs/src/app/layout.tsx b/apps/docs/src/app/layout.tsx index 1cda41b..f5317e5 100644 --- a/apps/docs/src/app/layout.tsx +++ b/apps/docs/src/app/layout.tsx @@ -6,8 +6,6 @@ import type { ReactNode } from "react"; import { RootProvider } from "fumadocs-ui/provider/next"; import localFont from "next/font/local"; -import { Navbar } from "@/components/Navbar"; - const geistSans = localFont({ src: "./fonts/GeistVF.woff", variable: "--font-geist-sans", @@ -34,10 +32,7 @@ function RootLayout(props: Readonly): React.JSX.Element { return ( - - - {children} - + {children} ); diff --git a/apps/docs/src/components/Navbar.tsx b/apps/docs/src/components/Navbar.tsx deleted file mode 100644 index 9fdffa2..0000000 --- a/apps/docs/src/components/Navbar.tsx +++ /dev/null @@ -1,70 +0,0 @@ -"use client"; - -import Link from "next/link"; -import { usePathname } from "next/navigation"; - -const NAV_ITEMS = [ - { label: "Getting Started", href: "/docs/overview/introduction" }, - { label: "Styles", href: "/docs/styles/colors" }, - { label: "Components", href: "/docs/components/button" }, -]; - -interface NavItemProps { - href: string; - label: string; - pathname: string; -} - -function NavItem(props: Readonly): React.JSX.Element { - const { href, label, pathname } = props; - const isActive = pathname.startsWith(href.split("/").slice(0, 4).join("/")); - const className = `rounded-md px-3 py-1.5 text-sm font-medium transition-colors ${isActive ? "text-foreground" : "text-foreground-muted hover:text-foreground"}`; - - return ( - - {label} - - ); -} - -function Navbar(): React.JSX.Element { - const pathname = usePathname(); - - return ( -
-
- - fried-ui - - - - - -
-
- ); -} - -export { Navbar }; From a21beb664032b13b2fed726f896b8ef04a3b93b7 Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Sat, 28 Mar 2026 14:19:04 +0700 Subject: [PATCH 035/199] feat(docs): Background image, metadata, GitHub icon, cleanup --- apps/docs/public/file-text.svg | 3 --- apps/docs/public/globe.svg | 10 -------- apps/docs/public/next.svg | 1 - .../public/pacT1BIPB708GOXrZYWPSztS53o.avif | Bin 0 -> 450517 bytes apps/docs/public/turborepo-dark.svg | 19 --------------- apps/docs/public/turborepo-light.svg | 19 --------------- apps/docs/public/vercel.svg | 10 -------- apps/docs/public/window.svg | 3 --- apps/docs/src/app/docs/layout.tsx | 22 ++++++------------ apps/docs/src/app/globals.css | 21 +++++++++++++++++ apps/docs/src/app/layout.tsx | 16 ++++++++++--- 11 files changed, 41 insertions(+), 83 deletions(-) delete mode 100644 apps/docs/public/file-text.svg delete mode 100644 apps/docs/public/globe.svg delete mode 100644 apps/docs/public/next.svg create mode 100644 apps/docs/public/pacT1BIPB708GOXrZYWPSztS53o.avif delete mode 100644 apps/docs/public/turborepo-dark.svg delete mode 100644 apps/docs/public/turborepo-light.svg delete mode 100644 apps/docs/public/vercel.svg delete mode 100644 apps/docs/public/window.svg diff --git a/apps/docs/public/file-text.svg b/apps/docs/public/file-text.svg deleted file mode 100644 index 9cfb3c9..0000000 --- a/apps/docs/public/file-text.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/apps/docs/public/globe.svg b/apps/docs/public/globe.svg deleted file mode 100644 index 4230a3d..0000000 --- a/apps/docs/public/globe.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/apps/docs/public/next.svg b/apps/docs/public/next.svg deleted file mode 100644 index 5174b28..0000000 --- a/apps/docs/public/next.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/apps/docs/public/pacT1BIPB708GOXrZYWPSztS53o.avif b/apps/docs/public/pacT1BIPB708GOXrZYWPSztS53o.avif new file mode 100644 index 0000000000000000000000000000000000000000..07adda2d69aefcb9965bff1c80bb93018bc5df72 GIT binary patch literal 450517 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

*d-RxD5Np& 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

Sp=xy_i=luS5ii0I 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 literal 0 HcmV?d00001 diff --git a/apps/docs/public/turborepo-dark.svg b/apps/docs/public/turborepo-dark.svg deleted file mode 100644 index dae38fe..0000000 --- a/apps/docs/public/turborepo-dark.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/apps/docs/public/turborepo-light.svg b/apps/docs/public/turborepo-light.svg deleted file mode 100644 index ddea915..0000000 --- a/apps/docs/public/turborepo-light.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/apps/docs/public/vercel.svg b/apps/docs/public/vercel.svg deleted file mode 100644 index 0164ddc..0000000 --- a/apps/docs/public/vercel.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/apps/docs/public/window.svg b/apps/docs/public/window.svg deleted file mode 100644 index bbc7800..0000000 --- a/apps/docs/public/window.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/apps/docs/src/app/docs/layout.tsx b/apps/docs/src/app/docs/layout.tsx index c996f02..4ece19d 100644 --- a/apps/docs/src/app/docs/layout.tsx +++ b/apps/docs/src/app/docs/layout.tsx @@ -2,6 +2,8 @@ import type { ReactNode } from "react"; import { DocsLayout } from "fumadocs-ui/layouts/notebook"; +import { GitHubIcon } from "@fried-ui/react"; + import { source } from "@/lib/source"; interface LayoutProps { @@ -9,17 +11,15 @@ interface LayoutProps { } const navConfig = { - title: "fried-ui", + title: "Fried UI", url: "/docs", }; +const githubIcon = ; + const linksConfig = [ { - text: "Storybook", - url: "https://fried-ui-storybook.vercel.app", - external: true, - }, - { + icon: githubIcon, text: "GitHub", url: "https://github.com/fried-day/fried-ui", external: true, @@ -31,19 +31,11 @@ const sidebarConfig = { defaultOpenLevel: 0, }; -const containerConfig = { className: "max-w-none" }; - function Layout(props: Readonly): React.JSX.Element { const { children } = props; return ( - + {children} ); diff --git a/apps/docs/src/app/globals.css b/apps/docs/src/app/globals.css index 78b681e..eaf5009 100644 --- a/apps/docs/src/app/globals.css +++ b/apps/docs/src/app/globals.css @@ -42,6 +42,27 @@ --color-fd-ring: var(--color-ring); } +body::before { + content: ""; + position: fixed; + inset: 0; + z-index: -2; + background-image: url("/pacT1BIPB708GOXrZYWPSztS53o.avif"); + background-size: cover; + background-position: center top; + background-repeat: no-repeat; + pointer-events: none; +} + +body::after { + content: ""; + position: fixed; + inset: 0; + z-index: -1; + background-color: rgba(0, 0, 0, 0.8); + backdrop-filter: blur(3.75rem); +} + #nd-subnav > [data-header-body] { border: 0 !important; } diff --git a/apps/docs/src/app/layout.tsx b/apps/docs/src/app/layout.tsx index f5317e5..9a688f0 100644 --- a/apps/docs/src/app/layout.tsx +++ b/apps/docs/src/app/layout.tsx @@ -16,9 +16,19 @@ const geistMono = localFont({ variable: "--font-geist-mono", }); -export const METADATA: Metadata = { - title: "fried-ui docs", - description: "Documentation for fried-ui component library", +export const metadata: Metadata = { + title: { + default: "fried-ui", + template: "%s — fried-ui", + }, + description: "Beautiful, accessible React components built on React Aria + Tailwind CSS v4.", + metadataBase: new URL("https://fried-ui.vercel.app"), + openGraph: { + title: "fried-ui", + description: "Beautiful, accessible React components built on React Aria + Tailwind CSS v4.", + siteName: "fried-ui", + type: "website", + }, }; interface RootLayoutProps { From 747ebc063d3d8d5a4f9a3a4e5ca6aefefd91944e Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Sat, 28 Mar 2026 21:17:47 +0700 Subject: [PATCH 036/199] feat(button): Add isFullWidth and isDisabled BEM --- apps/docs/public/llm/button.md | 21 +++++-- apps/docs/src/app/docs/[[...slug]]/page.tsx | 2 + apps/docs/src/app/docs/layout.tsx | 15 ++++- apps/docs/src/components/MdxComponents.tsx | 17 +++++ .../src/content/docs/components/button.mdx | 15 +++++ .../react/src/components/button/Button.tsx | 16 ++++- .../src/components/button/button.stories.tsx | 62 +++++++++++-------- .../src/components/button/button.test.tsx | 12 ++++ packages/styles/src/components/button.css | 7 ++- 9 files changed, 130 insertions(+), 37 deletions(-) diff --git a/apps/docs/public/llm/button.md b/apps/docs/public/llm/button.md index a3a23d0..90e4370 100644 --- a/apps/docs/public/llm/button.md +++ b/apps/docs/public/llm/button.md @@ -22,6 +22,7 @@ import { Button } from "@fried-ui/react"; | `size` | `"sm" \| "md" \| "lg" \| "xl"` | `"md"` | Size — controls padding, gap, font-size | | `radius` | `"none" \| "sm" \| "md" \| "lg" \| "xl" \| "full"` | `"md"` | Border radius (independent from size) | | `isIconOnly` | `boolean` | `false` | Icon-only square button | +| `isFullWidth` | `boolean` | `false` | Full width button | | `isDisabled` | `boolean` | `false` | Disabled state | | `isPending` | `boolean` | `false` | Loading spinner, disables interaction | | `onPress` | `(e: PressEvent) => void` | - | Press handler (preferred over onClick) | @@ -81,6 +82,8 @@ import { Button } from "@fried-ui/react"; | `fri-button--radius-lg` | Large border radius | | `fri-button--radius-xl` | Extra large border radius | | `fri-button--radius-full` | Full (pill) border radius | +| `fri-button--disabled` | Disabled state | +| `fri-button--full-width` | Full width button | | `fri-button--icon-only` | Icon-only square button | | `fri-button--pending` | Pending/loading state | | `fri-button__spinner` | Internal spinner element | @@ -139,6 +142,16 @@ Border radius independent from size. ``` +### Icon Only + +Square button for icon-only actions. Always provide `aria-label`. + +```tsx + +``` + ### With Icon Icons use `size-match-font` to match the font size. Gap scales via golden ratio. @@ -150,14 +163,12 @@ Icons use `size-match-font` to match the font size. Gap scales via golden ratio. ``` -### Icon Only +### Full Width -Square button for icon-only actions. Always provide `aria-label`. +Stretches the button to fill its container width. ```tsx - + ``` ### Disabled diff --git a/apps/docs/src/app/docs/[[...slug]]/page.tsx b/apps/docs/src/app/docs/[[...slug]]/page.tsx index fb9f358..b65fb47 100644 --- a/apps/docs/src/app/docs/[[...slug]]/page.tsx +++ b/apps/docs/src/app/docs/[[...slug]]/page.tsx @@ -10,6 +10,7 @@ import { ButtonCustomClass, ButtonDemo, ButtonDisabled, + ButtonFullWidth, ButtonIconOnly, ButtonPending, ButtonRadii, @@ -35,6 +36,7 @@ const mdxComponents = { ButtonCustomClass, ButtonDemo, ButtonDisabled, + ButtonFullWidth, ButtonIconOnly, ButtonPending, ButtonRadii, diff --git a/apps/docs/src/app/docs/layout.tsx b/apps/docs/src/app/docs/layout.tsx index 4ece19d..72aa959 100644 --- a/apps/docs/src/app/docs/layout.tsx +++ b/apps/docs/src/app/docs/layout.tsx @@ -11,7 +11,7 @@ interface LayoutProps { } const navConfig = { - title: "Fried UI", + title: "Fried-ui", url: "/docs", }; @@ -19,6 +19,7 @@ const githubIcon = ; const linksConfig = [ { + type: "icon" as const, icon: githubIcon, text: "GitHub", url: "https://github.com/fried-day/fried-ui", @@ -31,11 +32,21 @@ const sidebarConfig = { defaultOpenLevel: 0, }; +const themeSwitchConfig = { + mode: "light-dark-system" as const, +}; + function Layout(props: Readonly): React.JSX.Element { const { children } = props; return ( - + {children} ); diff --git a/apps/docs/src/components/MdxComponents.tsx b/apps/docs/src/components/MdxComponents.tsx index ee5bf3a..0eb7b08 100644 --- a/apps/docs/src/components/MdxComponents.tsx +++ b/apps/docs/src/components/MdxComponents.tsx @@ -106,6 +106,22 @@ function ButtonIconOnly(): React.JSX.Element { ); } +function ButtonFullWidth(): React.JSX.Element { + return ( +
+ + + + + +
+ ); +} + function ButtonDisabled(): React.JSX.Element { return ; } @@ -153,6 +169,7 @@ export { ButtonCustomClass, ButtonDemo, ButtonDisabled, + ButtonFullWidth, ButtonIconOnly, ButtonPending, ButtonRadii, diff --git a/apps/docs/src/content/docs/components/button.mdx b/apps/docs/src/content/docs/components/button.mdx index 6e36422..327eea3 100644 --- a/apps/docs/src/content/docs/components/button.mdx +++ b/apps/docs/src/content/docs/components/button.mdx @@ -137,6 +137,18 @@ import Link from "next/link"; ; ``` +## Full Width + +Stretches the button to fill its container width. + + + + + +```tsx + +``` + ## Disabled Prevents interaction and applies disabled styling. @@ -225,6 +237,7 @@ Built on [React Aria Button](https://react-spectrum.adobe.com/react-aria/Button. | `size` | `"sm" \| "md" \| "lg" \| "xl"` | `"md"` | The size of the button | | `radius` | `"none" \| "sm" \| "md" \| "lg" \| "xl" \| "full"` | `"md"` | The border radius of the button | | `isIconOnly` | `boolean` | `false` | Whether the button is icon-only (square) | +| `isFullWidth` | `boolean` | `false` | Whether the button takes full width | | `isDisabled` | `boolean` | `false` | Whether the button is disabled | | `isPending` | `boolean` | `false` | Whether the button shows a loading spinner | | `onPress` | `(e: PressEvent) => void` | - | Handler called when the button is pressed | @@ -284,6 +297,8 @@ Built on [React Aria Button](https://react-spectrum.adobe.com/react-aria/Button. | `fri-button--radius-lg` | Large border radius | | `fri-button--radius-xl` | Extra large border radius | | `fri-button--radius-full` | Full (pill) border radius | +| `fri-button--disabled` | Disabled state | +| `fri-button--full-width` | Full width button | | `fri-button--icon-only` | Icon-only square button | | `fri-button--pending` | Pending/loading state | | `fri-button__spinner` | Internal spinner element | diff --git a/packages/react/src/components/button/Button.tsx b/packages/react/src/components/button/Button.tsx index 36b7263..9e3adf0 100644 --- a/packages/react/src/components/button/Button.tsx +++ b/packages/react/src/components/button/Button.tsx @@ -17,13 +17,25 @@ export type ButtonProps = { size?: "sm" | "md" | "lg" | "xl"; radius?: "none" | "sm" | "md" | "lg" | "xl" | "full"; isIconOnly?: boolean; + isFullWidth?: boolean; className?: string; children?: ReactNode | ((renderProps: ButtonRenderProps) => ReactNode); } & Omit; function ButtonInner(props: Readonly, ref: ForwardedRef) { - const { radius = "md", size = "md", variant = "primary", children, className, isIconOnly, ...rest } = props; + const { + radius = "md", + size = "md", + variant = "primary", + children, + className, + isFullWidth, + isIconOnly, + ...rest + } = props; + const disabledModifier = rest.isDisabled ? "fri-button--disabled" : undefined; + const fullWidthModifier = isFullWidth ? "fri-button--full-width" : undefined; const iconOnlyModifier = isIconOnly ? "fri-button--icon-only" : undefined; const pendingModifier = rest.isPending ? "fri-button--pending" : undefined; @@ -32,6 +44,8 @@ function ButtonInner(props: Readonly, ref: ForwardedRef (
+
+ ), +}; + +const FullWidth: Story = { + render: (): React.JSX.Element => ( +
+ + + + +
), @@ -241,6 +249,6 @@ const RenderProps: Story = { render: (): React.JSX.Element => , }; -export { Default, Variants, Sizes, Radii, IconOnly, Disabled, Pending, RenderProps }; +export { Default, Variants, Sizes, Radii, IconOnly, FullWidth, Disabled, Pending, RenderProps }; export default meta; diff --git a/packages/react/src/components/button/button.test.tsx b/packages/react/src/components/button/button.test.tsx index 0a155b8..bcbe4a0 100644 --- a/packages/react/src/components/button/button.test.tsx +++ b/packages/react/src/components/button/button.test.tsx @@ -102,6 +102,18 @@ describe("Button", () => { expect(el.className).toContain("fri-button--icon-only"); }); + it("applies disabled class", () => { + render(); + const el = screen.getByRole("button"); + expect(el.className).toContain("fri-button--disabled"); + }); + + it("applies full-width class", () => { + render(); + const el = screen.getByRole("button"); + expect(el.className).toContain("fri-button--full-width"); + }); + it("supports render props children", () => { render(); expect(screen.getByRole("button", { name: "Idle" })).toBeInTheDocument(); diff --git a/packages/styles/src/components/button.css b/packages/styles/src/components/button.css index 0605756..6a00c83 100644 --- a/packages/styles/src/components/button.css +++ b/packages/styles/src/components/button.css @@ -16,8 +16,7 @@ @apply focus-ring; } - &:disabled, - &[aria-disabled="true"] { + &.fri-button--disabled { @apply status-disabled; } @@ -112,6 +111,10 @@ @apply gap-[0.927rem] px-6 py-3 text-2xl; } +.fri-button--full-width { + @apply w-full; +} + .fri-button--icon-only { @apply aspect-square justify-center gap-0 p-0; } From bb3faeec887ffe9ef5858747311dcdb3f98b4f13 Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Sat, 28 Mar 2026 21:24:10 +0700 Subject: [PATCH 037/199] fix(button): Add return types, update docs rule --- packages/react/src/components/button/button.stories.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/react/src/components/button/button.stories.tsx b/packages/react/src/components/button/button.stories.tsx index 91744a5..decf7fe 100644 --- a/packages/react/src/components/button/button.stories.tsx +++ b/packages/react/src/components/button/button.stories.tsx @@ -155,7 +155,7 @@ type Story = StoryObj; const Default: Story = {}; const Sizes: Story = { - render: () => ( + render: (): React.JSX.Element => (
@@ -166,7 +166,7 @@ const Sizes: Story = { }; const Radii: Story = { - render: () => ( + render: (): React.JSX.Element => (
@@ -179,7 +179,7 @@ const Radii: Story = { }; const Variants: Story = { - render: () => ( + render: (): React.JSX.Element => (
From d3950672f95f9d543c12a0e9483adf88c6cf412c Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Sun, 29 Mar 2026 04:06:19 +0700 Subject: [PATCH 038/199] refactor(react): Architecture overhaul, Badge, templates --- .vscode/settings.json | 2 +- apps/docs/public/llm/badge.md | 123 +++++++ apps/docs/public/llms.txt | 1 + apps/docs/src/app/docs/[[...slug]]/page.tsx | 12 +- apps/docs/src/app/docs/layout.tsx | 4 +- apps/docs/src/app/globals.css | 4 +- apps/docs/src/app/layout.tsx | 4 +- apps/docs/src/app/page.tsx | 4 +- apps/docs/src/components/BadgeLinks.tsx | 4 +- apps/docs/src/components/ColorSwatch.tsx | 8 +- apps/docs/src/components/CopyMarkdown.tsx | 4 +- apps/docs/src/components/MdxComponents.tsx | 104 ++++-- apps/docs/src/components/Preview.tsx | 4 +- .../src/content/docs/components/badge.mdx | 91 +++++ apps/docs/src/content/docs/meta.json | 1 + apps/storybook/.storybook/preview.ts | 10 +- apps/storybook/.storybook/styles.css | 4 +- packages/react/package.json | 9 +- packages/react/src/components/badge/Badge.tsx | 39 +++ .../src/components/badge/badge.stories.tsx | 231 ++++++++++++ .../react/src/components/badge/badge.test.tsx | 79 +++++ .../src/components/badge/badge.variants.ts | 4 + packages/react/src/components/badge/index.ts | 3 + .../react/src/components/button/Button.tsx | 83 ++--- .../src/components/button/button.stories.tsx | 330 +++++++++++++++--- .../src/components/button/button.test.tsx | 91 +++-- .../src/components/button/button.variants.ts | 7 + packages/react/src/components/button/index.ts | 1 + .../src/components/icons/ArrowRightIcon.tsx | 20 +- .../src/components/icons/ClipboardIcon.tsx | 12 +- .../react/src/components/icons/GitHubIcon.tsx | 12 +- .../react/src/components/icons/MoreIcon.tsx | 20 +- .../react/src/components/icons/PlusIcon.tsx | 20 +- .../src/components/icons/ReactAriaIcon.tsx | 12 +- .../src/components/icons/SettingsIcon.tsx | 20 +- .../react/src/components/icons/ShareIcon.tsx | 20 +- .../react/src/components/icons/Spinner.tsx | 12 +- .../src/components/icons/StorybookIcon.tsx | 14 +- packages/react/src/components/index.ts | 38 +- packages/react/src/index.ts | 1 - packages/react/src/utils/bem/bem.ts | 26 ++ packages/react/src/utils/bem/index.ts | 1 + packages/react/src/utils/cn/cn.ts | 10 - packages/react/src/utils/cn/index.ts | 1 - packages/react/tsup.config.ts | 2 +- packages/styles/src/components/badge.css | 75 ++++ packages/styles/src/components/button.css | 43 ++- packages/styles/src/components/index.css | 1 + packages/styles/src/tokens/colors.css | 6 +- packages/styles/src/tokens/layout.css | 2 +- packages/styles/src/utilities/focus.css | 5 +- packages/styles/src/utilities/sizing.css | 3 +- packages/styles/src/utilities/status.css | 4 +- packages/styles/tailwind.css | 2 + pnpm-lock.yaml | 3 - 55 files changed, 1340 insertions(+), 306 deletions(-) create mode 100644 apps/docs/public/llm/badge.md create mode 100644 apps/docs/src/content/docs/components/badge.mdx create mode 100644 packages/react/src/components/badge/Badge.tsx create mode 100644 packages/react/src/components/badge/badge.stories.tsx create mode 100644 packages/react/src/components/badge/badge.test.tsx create mode 100644 packages/react/src/components/badge/badge.variants.ts create mode 100644 packages/react/src/components/badge/index.ts create mode 100644 packages/react/src/components/button/button.variants.ts create mode 100644 packages/react/src/utils/bem/bem.ts create mode 100644 packages/react/src/utils/bem/index.ts delete mode 100644 packages/react/src/utils/cn/cn.ts delete mode 100644 packages/react/src/utils/cn/index.ts create mode 100644 packages/styles/src/components/badge.css create mode 100644 packages/styles/tailwind.css diff --git a/.vscode/settings.json b/.vscode/settings.json index 67d9f70..db66e8d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,7 +8,7 @@ "editor.quickSuggestions": { "strings": "on" }, "css.lint.unknownAtRules": "ignore", "tailwindCSS.includeLanguages": { "css": "css" }, - "tailwindCSS.experimental.configFile": "packages/styles/src/index.css", + "tailwindCSS.experimental.configFile": "packages/styles/tailwind.css", "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" }, "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true, diff --git a/apps/docs/public/llm/badge.md b/apps/docs/public/llm/badge.md new file mode 100644 index 0000000..2de3c1d --- /dev/null +++ b/apps/docs/public/llm/badge.md @@ -0,0 +1,123 @@ +# Badge + +Display-only label for status indicators, counts, and categories. + +## Install + +```bash +pnpm add @fried-ui/react +``` + +## Import + +```tsx +import { Badge } from "@fried-ui/react"; +``` + +## Props + +| Prop | Type | Default | Description | +| ----------- | -------------------------------------------------------------------------------------------------- | ----------- | ----------------------------- | +| `variant` | `"primary" \| "secondary" \| "ghost" \| "outline" \| "success" \| "warning" \| "danger" \| "info"` | `"primary"` | Visual variant | +| `size` | `"sm" \| "md" \| "lg"` | `"md"` | Size — controls padding, font | +| `className` | `string` | - | Additional CSS classes | +| `ref` | `Ref` | - | Forwarded ref to span element | +| `children` | `ReactNode` | - | Badge content | + +## CSS Classes + +| Class | Description | +| ---------------------- | ----------------- | +| `fri-badge` | Base styles | +| `fri-badge--primary` | Primary variant | +| `fri-badge--secondary` | Secondary variant | +| `fri-badge--ghost` | Ghost variant | +| `fri-badge--outline` | Outline variant | +| `fri-badge--success` | Success variant | +| `fri-badge--warning` | Warning variant | +| `fri-badge--danger` | Danger variant | +| `fri-badge--info` | Info variant | +| `fri-badge--sm` | Small size | +| `fri-badge--md` | Medium size | +| `fri-badge--lg` | Large size | + +## CSS Variables + +| Variable | Default | Description | +| ---------------- | -------------- | ---------------- | +| `--fri-badge-bg` | `transparent` | Background color | +| `--fri-badge-fg` | `currentColor` | Text color | + +## Examples + +### Variants + +Semantic and structural variants for different intents. + +```tsx +Primary +Secondary +Ghost +Outline +Success +Warning +Danger +Info +``` + +### Basic + +Default badge with medium size and pill shape. + +```tsx +New +``` + +### Sizes + +Three sizes for different contexts. + +```tsx +Small +Medium +Large +``` + +### Custom Class + +Use `className` for layout or typography utilities. + +```tsx +VIP +``` + +### Ref Forwarding + +Supports ref forwarding for integration with Tooltip, Popover, etc. + +```tsx +const ref = useRef(null); +Status; +``` + +## Anatomy + +``` +┌─ Badge (span) ─────┐ +│ children │ +└─────────────────────┘ +``` + +## Accessibility + +- Badge is a non-interactive `` element +- Use `aria-label` on parent if badge conveys important status +- Color is not the only indicator — always include text content + +## Constraints + +- 8 variants: primary, secondary, ghost, outline, success, warning, danger, info +- 3 sizes: sm, md, lg (no xl — badges should stay compact) +- Fixed `rounded-full` border radius (pill shape) +- Non-interactive — no hover, focus, or click states +- Uses BEM classes (`fri-badge`, `fri-badge--primary`, etc.) diff --git a/apps/docs/public/llms.txt b/apps/docs/public/llms.txt index a57005b..5266d01 100644 --- a/apps/docs/public/llms.txt +++ b/apps/docs/public/llms.txt @@ -4,6 +4,7 @@ ## Docs +- [Badge](/llm/badge.md): Display-only label for status indicators, counts, and categories - [Button](/llm/button.md): Accessible button with golden ratio spacing, React Aria keyboard/focus/screen reader support ## Optional diff --git a/apps/docs/src/app/docs/[[...slug]]/page.tsx b/apps/docs/src/app/docs/[[...slug]]/page.tsx index b65fb47..4d6d304 100644 --- a/apps/docs/src/app/docs/[[...slug]]/page.tsx +++ b/apps/docs/src/app/docs/[[...slug]]/page.tsx @@ -5,6 +5,10 @@ import { notFound } from "next/navigation"; import { ColorSwatch } from "@/components/ColorSwatch"; import { CopyMarkdown } from "@/components/CopyMarkdown"; import { + BadgeBadgeLinks, + BadgeDemo, + BadgeSizes, + BadgeVariants, ButtonAsLink, ButtonBadgeLinks, ButtonCustomClass, @@ -31,6 +35,10 @@ interface SlugParamsProps { const mdxComponents = { ...defaultMdxComponents, + BadgeBadgeLinks, + BadgeDemo, + BadgeSizes, + BadgeVariants, ButtonAsLink, ButtonBadgeLinks, ButtonCustomClass, @@ -47,7 +55,7 @@ const mdxComponents = { Preview, }; -async function Page(props: Readonly): Promise { +const Page = async (props: Readonly): Promise => { const params = await props.params; const page = source.getPage(params.slug); if (!page) notFound(); @@ -67,7 +75,7 @@ async function Page(props: Readonly): Promise { ); -} +}; export default Page; diff --git a/apps/docs/src/app/docs/layout.tsx b/apps/docs/src/app/docs/layout.tsx index 72aa959..48cd719 100644 --- a/apps/docs/src/app/docs/layout.tsx +++ b/apps/docs/src/app/docs/layout.tsx @@ -36,7 +36,7 @@ const themeSwitchConfig = { mode: "light-dark-system" as const, }; -function Layout(props: Readonly): React.JSX.Element { +const Layout = (props: Readonly): React.JSX.Element => { const { children } = props; return ( @@ -50,6 +50,6 @@ function Layout(props: Readonly): React.JSX.Element { {children} ); -} +}; export default Layout; diff --git a/apps/docs/src/app/globals.css b/apps/docs/src/app/globals.css index eaf5009..e1b747b 100644 --- a/apps/docs/src/app/globals.css +++ b/apps/docs/src/app/globals.css @@ -20,7 +20,7 @@ --color-fd-secondary-foreground: var(--color-secondary-foreground); --color-fd-accent: var(--color-muted); --color-fd-accent-foreground: var(--color-foreground); - --color-fd-ring: var(--color-ring); + --color-fd-ring: var(--color-focus-outer); } .dark { @@ -39,7 +39,7 @@ --color-fd-secondary-foreground: var(--color-secondary-foreground); --color-fd-accent: var(--color-muted); --color-fd-accent-foreground: var(--color-foreground); - --color-fd-ring: var(--color-ring); + --color-fd-ring: var(--color-focus-outer); } body::before { diff --git a/apps/docs/src/app/layout.tsx b/apps/docs/src/app/layout.tsx index 9a688f0..5b65080 100644 --- a/apps/docs/src/app/layout.tsx +++ b/apps/docs/src/app/layout.tsx @@ -35,7 +35,7 @@ interface RootLayoutProps { children: ReactNode; } -function RootLayout(props: Readonly): React.JSX.Element { +const RootLayout = (props: Readonly): React.JSX.Element => { const { children } = props; const bodyClassName = `${geistSans.variable} ${geistMono.variable} font-sans antialiased`; @@ -46,6 +46,6 @@ function RootLayout(props: Readonly): React.JSX.Element { ); -} +}; export default RootLayout; diff --git a/apps/docs/src/app/page.tsx b/apps/docs/src/app/page.tsx index d0702ca..4360288 100644 --- a/apps/docs/src/app/page.tsx +++ b/apps/docs/src/app/page.tsx @@ -1,6 +1,6 @@ import Link from "next/link"; -function Home(): React.JSX.Element { +const Home = (): React.JSX.Element => { return (
@@ -16,6 +16,6 @@ function Home(): React.JSX.Element {
); -} +}; export default Home; diff --git a/apps/docs/src/components/BadgeLinks.tsx b/apps/docs/src/components/BadgeLinks.tsx index 5b37ab8..4f079c6 100644 --- a/apps/docs/src/components/BadgeLinks.tsx +++ b/apps/docs/src/components/BadgeLinks.tsx @@ -14,7 +14,7 @@ export interface BadgeLinkProps { label: string; } -function BadgeLinks(props: Readonly): React.JSX.Element { +const BadgeLinks = (props: Readonly): React.JSX.Element => { const { links } = props; return ( @@ -29,6 +29,6 @@ function BadgeLinks(props: Readonly): React.JSX.Element { ))}
); -} +}; export { BadgeLinks }; diff --git a/apps/docs/src/components/ColorSwatch.tsx b/apps/docs/src/components/ColorSwatch.tsx index a939f5b..cd7628e 100644 --- a/apps/docs/src/components/ColorSwatch.tsx +++ b/apps/docs/src/components/ColorSwatch.tsx @@ -9,7 +9,7 @@ export interface ColorItemProps { token: string; } -function SwatchItem(props: Readonly): React.JSX.Element { +const SwatchItem = (props: Readonly): React.JSX.Element => { const { name, token } = props; const label = `${name} color swatch`; const bgStyle = { backgroundColor: `var(--color-${token})` }; @@ -26,9 +26,9 @@ function SwatchItem(props: Readonly): React.JSX.Element { {name}
); -} +}; -function ColorSwatch(props: Readonly): React.JSX.Element { +const ColorSwatch = (props: Readonly): React.JSX.Element => { const { colors } = props; return ( @@ -38,6 +38,6 @@ function ColorSwatch(props: Readonly): React.JSX.Element { ))}
); -} +}; export { ColorSwatch }; diff --git a/apps/docs/src/components/CopyMarkdown.tsx b/apps/docs/src/components/CopyMarkdown.tsx index f9552b1..594093c 100644 --- a/apps/docs/src/components/CopyMarkdown.tsx +++ b/apps/docs/src/components/CopyMarkdown.tsx @@ -8,7 +8,7 @@ interface CopyMarkdownProps { url: string; } -function CopyMarkdown(props: Readonly): React.JSX.Element { +const CopyMarkdown = (props: Readonly): React.JSX.Element => { const { url } = props; const [copied, setCopied] = useState(false); @@ -27,6 +27,6 @@ function CopyMarkdown(props: Readonly): React.JSX.Element { {copied ? "Copied!" : "Copy for LLM"} ); -} +}; export { CopyMarkdown }; diff --git a/apps/docs/src/components/MdxComponents.tsx b/apps/docs/src/components/MdxComponents.tsx index 0eb7b08..6b110ee 100644 --- a/apps/docs/src/components/MdxComponents.tsx +++ b/apps/docs/src/components/MdxComponents.tsx @@ -2,6 +2,7 @@ import { ArrowRightIcon, + Badge, Button, GitHubIcon, MoreIcon, @@ -14,6 +15,57 @@ import { import { BadgeLinks } from "./BadgeLinks"; +const badgeBadgeLinksData = [ + { + href: "https://fried-ui-storybook.vercel.app/?path=/story/components-badge--default", + icon: , + label: "Storybook", + }, + { + href: "https://github.com/fried-day/fried-ui/blob/main/packages/react/src/components/badge/Badge.tsx", + icon: , + label: "Source", + }, + { + href: "https://github.com/fried-day/fried-ui/blob/main/packages/styles/src/components/badge.css", + icon: , + label: "Styles source", + }, +]; + +const BadgeBadgeLinks = (): React.JSX.Element => { + return ; +}; + +const BadgeDemo = (): React.JSX.Element => { + return New; +}; + +const BadgeVariants = (): React.JSX.Element => { + return ( +
+ Primary + Secondary + Ghost + Outline + Success + Warning + Danger + Info +
+ ); +}; + +const BadgeSizes = (): React.JSX.Element => { + return ( +
+ Small + Medium + Large +
+ ); +}; + const buttonBadgeLinksData = [ { href: "https://fried-ui-storybook.vercel.app/?path=/story/components-button--default", @@ -37,15 +89,15 @@ const buttonBadgeLinksData = [ }, ]; -function ButtonBadgeLinks(): React.JSX.Element { +const ButtonBadgeLinks = (): React.JSX.Element => { return ; -} +}; -function ButtonDemo(): React.JSX.Element { +const ButtonDemo = (): React.JSX.Element => { return ; -} +}; -function ButtonVariants(): React.JSX.Element { +const ButtonVariants = (): React.JSX.Element => { return (
@@ -58,9 +110,9 @@ function ButtonVariants(): React.JSX.Element {
); -} +}; -function ButtonSizes(): React.JSX.Element { +const ButtonSizes = (): React.JSX.Element => { return (
@@ -69,9 +121,9 @@ function ButtonSizes(): React.JSX.Element {
); -} +}; -function ButtonRadii(): React.JSX.Element { +const ButtonRadii = (): React.JSX.Element => { return (
@@ -82,9 +134,9 @@ function ButtonRadii(): React.JSX.Element {
); -} +}; -function ButtonIconOnly(): React.JSX.Element { +const ButtonIconOnly = (): React.JSX.Element => { return (
); -} +}; -function ButtonFullWidth(): React.JSX.Element { +const ButtonFullWidth = (): React.JSX.Element => { return (
@@ -120,21 +172,21 @@ function ButtonFullWidth(): React.JSX.Element {
); -} +}; -function ButtonDisabled(): React.JSX.Element { +const ButtonDisabled = (): React.JSX.Element => { return ; -} +}; -function ButtonPending(): React.JSX.Element { +const ButtonPending = (): React.JSX.Element => { return ; -} +}; -function ButtonCustomClass(): React.JSX.Element { +const ButtonCustomClass = (): React.JSX.Element => { return ; -} +}; -function ButtonWithIcons(): React.JSX.Element { +const ButtonWithIcons = (): React.JSX.Element => { return (
); -} +}; -function ButtonAsLink(): React.JSX.Element { +const ButtonAsLink = (): React.JSX.Element => { return (
); -} +}; export { + BadgeBadgeLinks, + BadgeDemo, + BadgeSizes, + BadgeVariants, ButtonAsLink, ButtonBadgeLinks, ButtonCustomClass, diff --git a/apps/docs/src/components/Preview.tsx b/apps/docs/src/components/Preview.tsx index 7b15254..5bd3e91 100644 --- a/apps/docs/src/components/Preview.tsx +++ b/apps/docs/src/components/Preview.tsx @@ -4,7 +4,7 @@ interface PreviewProps { children: ReactNode; } -function Preview(props: Readonly): React.JSX.Element { +const Preview = (props: Readonly): React.JSX.Element => { const { children } = props; return ( @@ -12,6 +12,6 @@ function Preview(props: Readonly): React.JSX.Element { {children}

); -} +}; export { Preview }; diff --git a/apps/docs/src/content/docs/components/badge.mdx b/apps/docs/src/content/docs/components/badge.mdx new file mode 100644 index 0000000..578e84a --- /dev/null +++ b/apps/docs/src/content/docs/components/badge.mdx @@ -0,0 +1,91 @@ +--- +title: Badge +description: Display-only label for status indicators, counts, and categories. +--- + + + +## Import + +```tsx +import { Badge } from "@fried-ui/react"; +``` + +## Usage + + + + + +```tsx +New +``` + +## Variants + +The `variant` prop controls the visual style using semantic color tokens. + + + + + +```tsx +Primary +Secondary +Ghost +Outline +Success +Warning +Danger +Info +``` + +## Sizes + +The `size` prop controls padding, gap, and font-size. + + + + + +```tsx +Small +Medium +Large +``` + +## API Reference + +### Badge Props + +| Prop | Type | Default | Description | +| ----------- | -------------------------------------------------------------------------------------------------- | ----------- | ------------------------------- | +| `variant` | `"primary" \| "secondary" \| "ghost" \| "outline" \| "success" \| "warning" \| "danger" \| "info"` | `"primary"` | The visual variant of the badge | +| `size` | `"sm" \| "md" \| "lg"` | `"md"` | The size of the badge | +| `className` | `string` | - | Additional CSS classes | +| `ref` | `Ref` | - | Forwarded ref to the span | +| `children` | `ReactNode` | - | Badge content | + +### CSS Classes + +| Class | Description | +| ---------------------- | ----------------- | +| `fri-badge` | Base styles | +| `fri-badge--primary` | Primary variant | +| `fri-badge--secondary` | Secondary variant | +| `fri-badge--ghost` | Ghost variant | +| `fri-badge--outline` | Outline variant | +| `fri-badge--success` | Success variant | +| `fri-badge--warning` | Warning variant | +| `fri-badge--danger` | Danger variant | +| `fri-badge--info` | Info variant | +| `fri-badge--sm` | Small size | +| `fri-badge--md` | Medium size | +| `fri-badge--lg` | Large size | + +### CSS Variables + +| CSS Variable | Description | Default | +| ---------------- | ---------------- | -------------- | +| `--fri-badge-bg` | Background color | `transparent` | +| `--fri-badge-fg` | Text color | `currentColor` | diff --git a/apps/docs/src/content/docs/meta.json b/apps/docs/src/content/docs/meta.json index 1051813..96fd520 100644 --- a/apps/docs/src/content/docs/meta.json +++ b/apps/docs/src/content/docs/meta.json @@ -9,6 +9,7 @@ "styles/design-tokens", "styles/colors", "---Components---", + "components/badge", "components/button" ] } diff --git a/apps/storybook/.storybook/preview.ts b/apps/storybook/.storybook/preview.ts index 479141c..75eb53e 100644 --- a/apps/storybook/.storybook/preview.ts +++ b/apps/storybook/.storybook/preview.ts @@ -13,10 +13,16 @@ const preview: Preview = { }, dark: { name: "dark", - value: "var(--color-background, #0a0a0a)", + value: "var(--color-background, #000)", }, }, }, + docs: { + codePanel: true, + source: { + type: "code", + }, + }, controls: { matchers: { color: /(background|color)$/i, @@ -27,7 +33,7 @@ const preview: Preview = { decorators: [ (Story, context) => { const bg = context.globals?.backgrounds?.value; - const isDark = bg?.includes("dark") || bg?.includes("0a0a0a"); + const isDark = bg?.includes("dark") || bg?.includes("000"); const theme = isDark ? "dark" : "light"; document.documentElement.setAttribute("data-theme", theme); diff --git a/apps/storybook/.storybook/styles.css b/apps/storybook/.storybook/styles.css index 2e1db47..a8658a5 100644 --- a/apps/storybook/.storybook/styles.css +++ b/apps/storybook/.storybook/styles.css @@ -1,2 +1,4 @@ -@import "tailwindcss"; +@import "tailwindcss" source(none); @import "@fried-ui/styles"; + +@source "../../../packages/react/src"; diff --git a/packages/react/package.json b/packages/react/package.json index 6fc01aa..512efe0 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -26,11 +26,11 @@ ".": { "import": "./src/index.ts" }, + "./badge": { + "import": "./src/components/badge/index.ts" + }, "./button": { "import": "./src/components/button/index.ts" - }, - "./utils/cn": { - "import": "./src/utils/cn/index.ts" } }, "main": "./dist/index.mjs", @@ -54,8 +54,7 @@ "@react-types/color": "^3.1.4", "@react-types/shared": "^3.33.1", "clsx": "^2.1.1", - "react-aria-components": "^1.16.0", - "tailwind-merge": "^3.5.0" + "react-aria-components": "^1.16.0" }, "devDependencies": { "@fried-ui/vitest": "workspace:*", diff --git a/packages/react/src/components/badge/Badge.tsx b/packages/react/src/components/badge/Badge.tsx new file mode 100644 index 0000000..c0b0f0a --- /dev/null +++ b/packages/react/src/components/badge/Badge.tsx @@ -0,0 +1,39 @@ +"use client"; + +import type { ComponentPropsWithRef } from "react"; + +import { clsx } from "clsx"; + +import { bem } from "../../utils/bem"; + +import type { BadgeVariantsProps } from "./badge.variants"; + +export type BadgeProps = Omit, "className"> & + BadgeVariantsProps & { + className?: string; + }; + +const Badge = (props: Readonly) => { + const { children, className, ref, size, variant, ...rest } = props; + + const badgeClassName = clsx( + bem({ + block: "fri-badge", + modifiers: { + variant, + size, + }, + }), + className, + ); + + return ( + + {children} + + ); +}; + +Badge.displayName = "Badge"; + +export { Badge }; diff --git a/packages/react/src/components/badge/badge.stories.tsx b/packages/react/src/components/badge/badge.stories.tsx new file mode 100644 index 0000000..055a92c --- /dev/null +++ b/packages/react/src/components/badge/badge.stories.tsx @@ -0,0 +1,231 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import { SettingsIcon } from "../icons"; +import { Badge } from "./Badge"; + +const meta = { + title: "Components/Badge", + component: Badge, + tags: ["autodocs"], + parameters: { + layout: "centered", + }, + args: { + children: "Badge", + variant: "primary", + size: "md", + }, + argTypes: { + children: { + control: "text", + description: "Badge content", + table: { + type: { + summary: "ReactNode", + }, + category: "Children", + }, + }, + variant: { + control: "select", + options: ["primary", "secondary", "ghost", "outline", "success", "warning", "danger", "info"], + description: "The visual variant of the badge", + table: { + type: { + summary: '"primary" | "secondary" | "ghost" | "outline" | "success" | "warning" | "danger" | "info"', + }, + defaultValue: { + summary: "primary", + }, + category: "Style Variants", + }, + }, + size: { + control: "select", + options: ["sm", "md", "lg"], + description: "The size of the badge", + table: { + type: { + summary: '"sm" | "md" | "lg"', + }, + defaultValue: { + summary: "md", + }, + category: "Style Variants", + }, + }, + className: { + control: "text", + description: "Additional CSS classes", + table: { + type: { + summary: "string", + }, + category: "Styling", + }, + }, + }, +} satisfies Meta; + +type Story = StoryObj; + +const Default: Story = { + parameters: { + docs: { + source: { + code: `import { Badge } from "@fried-ui/react"; + +const Basic = () => { + return New; +};`, + }, + }, + }, +}; + +const Variants: Story = { + parameters: { + docs: { + source: { + code: `import { Badge } from "@fried-ui/react"; + +const Variants = () => { + return ( +
+ Primary + Secondary + Ghost + Outline + Success + Warning + Danger + Info +
+ ); +};`, + }, + }, + }, + render: (args): React.JSX.Element => ( +
+ + Primary + + + + Secondary + + + + Ghost + + + + Outline + + + + Success + + + + Warning + + + + Danger + + + + Info + +
+ ), +}; + +const Sizes: Story = { + parameters: { + docs: { + source: { + code: `import { Badge } from "@fried-ui/react"; + +const Sizes = () => { + return ( +
+ Small + Medium + Large +
+ ); +};`, + }, + }, + }, + render: (args): React.JSX.Element => ( +
+ + Small + + + + Medium + + + + Large + +
+ ), +}; + +const WithIcon: Story = { + parameters: { + docs: { + source: { + code: `import { Badge, SettingsIcon } from "@fried-ui/react"; + +// Option 1: slot="icon" (recommended) +const WithIcon = () => { + return ( + + + Settings + + ); +}; + +// Option 2: className +const WithIconAlt = () => { + return ( + + + Settings + + ); +};`, + }, + }, + }, + render: (args): React.JSX.Element => ( +
+ + + Settings + + + + + Active + + + + + Error + +
+ ), +}; + +export { Default, Variants, Sizes, WithIcon }; + +export default meta; diff --git a/packages/react/src/components/badge/badge.test.tsx b/packages/react/src/components/badge/badge.test.tsx new file mode 100644 index 0000000..99193ef --- /dev/null +++ b/packages/react/src/components/badge/badge.test.tsx @@ -0,0 +1,79 @@ +import { createRef } from "react"; + +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; + +import { Badge } from "./Badge"; + +describe("Badge", () => { + it("renders with default props", () => { + render(New); + expect(screen.getByText("New")).toBeInTheDocument(); + }); + + it("renders only base class without explicit props", () => { + render(Default); + expect(screen.getByText("Default").className).toBe("fri-badge"); + }); + + it("renders as a span element", () => { + render(Tag); + expect(screen.getByText("Tag").tagName).toBe("SPAN"); + }); + + it("applies variant class only when explicitly set", () => { + render(Done); + expect(screen.getByText("Done").className).toContain("fri-badge--success"); + }); + + it("applies all variant classes", () => { + const variants = ["primary", "secondary", "ghost", "outline", "success", "warning", "danger", "info"] as const; + + variants.forEach((variant) => { + const { unmount } = render({variant}); + expect(screen.getByText(variant).className).toContain(`fri-badge--${variant}`); + unmount(); + }); + }); + + it("applies size class with key-value format", () => { + const sizes = ["sm", "md", "lg"] as const; + + sizes.forEach((size) => { + const { unmount } = render({size}); + expect(screen.getByText(size).className).toContain(`fri-badge--size-${size}`); + unmount(); + }); + }); + + it("merges custom className", () => { + render(Styled); + expect(screen.getByText("Styled").className).toContain("mt-4"); + }); + + it("forwards ref to the span element", () => { + const ref = createRef(); + render(Ref); + expect(ref.current).toBeInstanceOf(HTMLSpanElement); + expect(ref.current).toHaveAttribute("data-slot", "badge"); + }); + + it("sets data-slot attribute", () => { + render(Slot); + expect(screen.getByText("Slot")).toHaveAttribute("data-slot", "badge"); + }); + + it("sets displayName", () => { + expect(Badge.displayName).toBe("Badge"); + }); + + it("passes through native HTML attributes", () => { + render( + + Props + , + ); + + expect(screen.getByTestId("custom-badge")).toHaveAttribute("id", "my-badge"); + }); +}); diff --git a/packages/react/src/components/badge/badge.variants.ts b/packages/react/src/components/badge/badge.variants.ts new file mode 100644 index 0000000..7cce09b --- /dev/null +++ b/packages/react/src/components/badge/badge.variants.ts @@ -0,0 +1,4 @@ +export interface BadgeVariantsProps { + size?: "sm" | "md" | "lg"; + variant?: "primary" | "secondary" | "ghost" | "outline" | "success" | "warning" | "danger" | "info"; +} diff --git a/packages/react/src/components/badge/index.ts b/packages/react/src/components/badge/index.ts new file mode 100644 index 0000000..f57350f --- /dev/null +++ b/packages/react/src/components/badge/index.ts @@ -0,0 +1,3 @@ +export { Badge } from "./Badge"; +export type { BadgeProps } from "./Badge"; +export type { BadgeVariantsProps } from "./badge.variants"; diff --git a/packages/react/src/components/button/Button.tsx b/packages/react/src/components/button/Button.tsx index 9e3adf0..a0daf00 100644 --- a/packages/react/src/components/button/Button.tsx +++ b/packages/react/src/components/button/Button.tsx @@ -1,69 +1,52 @@ "use client"; -import { type ForwardedRef, forwardRef, type ReactNode } from "react"; +import type { ComponentPropsWithRef, ReactNode } from "react"; -import { - Button as RACButton, - type ButtonProps as RACButtonProps, - type ButtonRenderProps, - composeRenderProps, -} from "react-aria-components"; +import { Button as RacButton, composeRenderProps } from "react-aria-components"; +import type { ButtonRenderProps } from "react-aria-components"; -import { Spinner } from "../icons"; -import { cn } from "../../utils/cn"; - -export type ButtonProps = { - variant?: "primary" | "secondary" | "ghost" | "outline" | "success" | "warning" | "danger" | "info"; - size?: "sm" | "md" | "lg" | "xl"; - radius?: "none" | "sm" | "md" | "lg" | "xl" | "full"; - isIconOnly?: boolean; - isFullWidth?: boolean; - className?: string; - children?: ReactNode | ((renderProps: ButtonRenderProps) => ReactNode); -} & Omit; +import { clsx } from "clsx"; -function ButtonInner(props: Readonly, ref: ForwardedRef) { - const { - radius = "md", - size = "md", - variant = "primary", - children, - className, - isFullWidth, - isIconOnly, - ...rest - } = props; +import { Spinner } from "../icons"; +import { bem } from "../../utils/bem"; - const disabledModifier = rest.isDisabled ? "fri-button--disabled" : undefined; - const fullWidthModifier = isFullWidth ? "fri-button--full-width" : undefined; - const iconOnlyModifier = isIconOnly ? "fri-button--icon-only" : undefined; - const pendingModifier = rest.isPending ? "fri-button--pending" : undefined; +import type { ButtonVariantsProps } from "./button.variants"; - const buttonClassName = cn( - "fri-button", - `fri-button--${variant}`, - `fri-button--${size}`, - `fri-button--radius-${radius}`, - disabledModifier, - fullWidthModifier, - iconOnlyModifier, - pendingModifier, - className, - ); +export type ButtonProps = ButtonVariantsProps & { + children?: ReactNode | ((renderProps: ButtonRenderProps) => ReactNode); + className?: string | ((renderProps: ButtonRenderProps) => string); +} & Omit, "className" | "children">; + +const Button = (props: Readonly) => { + const { children, className, isFullWidth, isIconOnly, radius, ref, size, variant, ...rest } = props; + + const baseClassName = bem({ + block: "fri-button", + modifiers: { + variant, + size, + radius, + "full-width": isFullWidth, + "icon-only": isIconOnly, + disabled: rest.isDisabled, + pending: rest.isPending, + }, + }); + + const buttonClassName = composeRenderProps(className, (consumerClassName) => clsx(baseClassName, consumerClassName)); return ( - + {composeRenderProps(children, (child, { isPending }) => ( <> {child} - {isPending && + ); -} +}; -const Button = forwardRef(ButtonInner); +Button.displayName = "Button"; export { Button }; diff --git a/packages/react/src/components/button/button.stories.tsx b/packages/react/src/components/button/button.stories.tsx index decf7fe..d0a77f4 100644 --- a/packages/react/src/components/button/button.stories.tsx +++ b/packages/react/src/components/button/button.stories.tsx @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from "@storybook/react"; -import { MoreIcon, PlusIcon, SettingsIcon, ShareIcon } from "../icons"; +import { ArrowRightIcon, MoreIcon, PlusIcon, SettingsIcon, ShareIcon } from "../icons"; import { Button } from "./Button"; const meta = { @@ -9,12 +9,6 @@ const meta = { tags: ["autodocs"], parameters: { layout: "centered", - docs: { - description: { - component: - "Accessible button built on [React Aria Button](https://react-spectrum.adobe.com/react-aria/Button.html). Supports semantic variants, golden-ratio spacing, and pending state with spinner.", - }, - }, }, args: { children: "Button", @@ -25,7 +19,7 @@ const meta = { argTypes: { children: { control: "text", - description: "Button label", + description: "Button content", table: { type: { summary: "ReactNode", @@ -152,21 +146,128 @@ const meta = { type Story = StoryObj; -const Default: Story = {}; +const Default: Story = { + parameters: { + docs: { + source: { + code: `import { Button } from "@fried-ui/react"; + +const Basic = () => { + return ; +};`, + }, + }, + }, +}; + +const Variants: Story = { + parameters: { + docs: { + source: { + code: `import { Button } from "@fried-ui/react"; + +const Variants = () => { + return ( +
+ + + + + + + + +
+ ); +};`, + }, + }, + }, + render: (args): React.JSX.Element => ( +
+ + + + + + + + + + + + + + + +
+ ), +}; const Sizes: Story = { - render: (): React.JSX.Element => ( + parameters: { + docs: { + source: { + code: `import { Button } from "@fried-ui/react"; + +const Sizes = () => { + return (
+ ); +};`, + }, + }, + }, + render: (args): React.JSX.Element => ( +
+ + + + + + + +
), }; const Radii: Story = { - render: (): React.JSX.Element => ( + parameters: { + docs: { + source: { + code: `import { Button } from "@fried-ui/react"; + +const Radii = () => { + return (
@@ -175,54 +276,129 @@ const Radii: Story = {
- ), -}; - -const Variants: Story = { - render: (): React.JSX.Element => ( + ); +};`, + }, + }, + }, + render: (args): React.JSX.Element => (
- - - - - - - - + + + + + + + + + + +
), }; -const Disabled: Story = { - args: { - children: "Disabled", - isDisabled: true, - }, +const WithIcon: Story = { + parameters: { + docs: { + source: { + code: `import { Button, ArrowRightIcon } from "@fried-ui/react"; + +// Option 1: slot="icon" (recommended) +const WithIcon = () => { + return ( + + ); }; -const Pending: Story = { - args: { - children: "Saving...", - isPending: true, +// Option 2: className +const WithIconAlt = () => { + return ( + + ); +};`, + }, + }, }, + render: (args): React.JSX.Element => ( +
+ + + + + +
+ ), }; const IconOnly: Story = { - render: (): React.JSX.Element => ( + parameters: { + docs: { + source: { + code: `import { Button, SettingsIcon } from "@fried-ui/react"; + +// Option 1: slot="icon" (recommended) +const IconOnly = () => { + return ( + + ); +}; + +// Option 2: className +const IconOnlyAlt = () => { + return ( + + ); +};`, + }, + }, + }, + render: (args): React.JSX.Element => (
- - - -
@@ -230,25 +406,91 @@ const IconOnly: Story = { }; const FullWidth: Story = { - render: (): React.JSX.Element => ( + parameters: { + docs: { + source: { + code: `import { Button } from "@fried-ui/react"; + +const FullWidth = () => { + return ; +};`, + }, + }, + }, + render: (args): React.JSX.Element => (
- + - -
), }; +const Disabled: Story = { + parameters: { + docs: { + source: { + code: `import { Button } from "@fried-ui/react"; + +const Disabled = () => { + return ; +};`, + }, + }, + }, + args: { + children: "Disabled", + isDisabled: true, + }, +}; + +const Pending: Story = { + parameters: { + docs: { + source: { + code: `import { Button } from "@fried-ui/react"; + +const Pending = () => { + return ; +};`, + }, + }, + }, + args: { + children: "Saving...", + isPending: true, + }, +}; + const RenderProps: Story = { - render: (): React.JSX.Element => , + parameters: { + docs: { + source: { + code: `import { Button } from "@fried-ui/react"; + +const RenderProps = () => { + return ( + + ); +};`, + }, + }, + }, + render: (args): React.JSX.Element => ( + + ), }; -export { Default, Variants, Sizes, Radii, IconOnly, FullWidth, Disabled, Pending, RenderProps }; +export { Default, Variants, Sizes, Radii, WithIcon, IconOnly, FullWidth, Disabled, Pending, RenderProps }; export default meta; diff --git a/packages/react/src/components/button/button.test.tsx b/packages/react/src/components/button/button.test.tsx index bcbe4a0..e800064 100644 --- a/packages/react/src/components/button/button.test.tsx +++ b/packages/react/src/components/button/button.test.tsx @@ -13,6 +13,15 @@ describe("Button", () => { expect(el).toBeInTheDocument(); }); + it("renders only base class without explicit props", () => { + render(); + const el = screen.getByRole("button"); + expect(el.className).toContain("fri-button"); + expect(el.className).not.toContain("fri-button--primary"); + expect(el.className).not.toContain("fri-button--size-md"); + expect(el.className).not.toContain("fri-button--radius-md"); + }); + it("fires onPress when clicked", async () => { const onPress = vi.fn(); const user = userEvent.setup(); @@ -38,46 +47,49 @@ describe("Button", () => { expect(onPress).not.toHaveBeenCalled(); }); - it("applies variant class", () => { + it("applies variant class only when explicitly set", () => { render(); - const el = screen.getByRole("button"); - expect(el.className).toContain("fri-button--primary"); + expect(screen.getByRole("button").className).toContain("fri-button--primary"); }); - it("applies size classes", () => { - const { unmount: u1 } = render(); - expect(screen.getByRole("button").className).toContain("fri-button--sm"); - u1(); + it("applies all variant classes", () => { + const variants = ["primary", "secondary", "ghost", "outline", "success", "warning", "danger", "info"] as const; - const { unmount: u2 } = render(); - expect(screen.getByRole("button").className).toContain("fri-button--md"); - u2(); + variants.forEach((variant) => { + const { unmount } = render(); + expect(screen.getByRole("button").className).toContain(`fri-button--${variant}`); + unmount(); + }); + }); - const { unmount: u3 } = render(); - expect(screen.getByRole("button").className).toContain("fri-button--lg"); - u3(); + it("applies size class with key-value format", () => { + const sizes = ["sm", "md", "lg", "xl"] as const; - render(); - expect(screen.getByRole("button").className).toContain("fri-button--xl"); + sizes.forEach((size) => { + const { unmount } = render(); + expect(screen.getByRole("button").className).toContain(`fri-button--size-${size}`); + unmount(); + }); }); - it("applies default variant and size", () => { - render(); - const el = screen.getByRole("button"); - expect(el.className).toContain("fri-button--primary"); - expect(el.className).toContain("fri-button--md"); + it("applies radius class with key-value format", () => { + const radii = ["none", "sm", "md", "lg", "xl", "full"] as const; + + radii.forEach((radius) => { + const { unmount } = render(); + expect(screen.getByRole("button").className).toContain(`fri-button--radius-${radius}`); + unmount(); + }); }); it("merges custom className", () => { render(); - const el = screen.getByRole("button"); - expect(el.className).toContain("mt-4"); + expect(screen.getByRole("button").className).toContain("mt-4"); }); it("renders as disabled with aria attribute", () => { render(); - const el = screen.getByRole("button"); - expect(el).toBeDisabled(); + expect(screen.getByRole("button")).toBeDisabled(); }); it("passes through additional props", () => { @@ -98,20 +110,41 @@ describe("Button", () => { , ); - const el = screen.getByRole("button"); - expect(el.className).toContain("fri-button--icon-only"); + expect(screen.getByRole("button").className).toContain("fri-button--icon-only"); }); it("applies disabled class", () => { render(); - const el = screen.getByRole("button"); - expect(el.className).toContain("fri-button--disabled"); + expect(screen.getByRole("button").className).toContain("fri-button--disabled"); }); it("applies full-width class", () => { render(); + expect(screen.getByRole("button").className).toContain("fri-button--full-width"); + }); + + it("applies pending class", () => { + render(); + expect(screen.getByRole("button").className).toContain("fri-button--pending"); + }); + + it("sets data-slot attribute", () => { + render(); + expect(screen.getByRole("button")).toHaveAttribute("data-slot", "button"); + }); + + it("sets displayName", () => { + expect(Button.displayName).toBe("Button"); + }); + + it("supports className as render prop function", () => { + render( + , + ); + const el = screen.getByRole("button"); - expect(el.className).toContain("fri-button--full-width"); + expect(el.className).toContain("fri-button"); + expect(el.className).toContain("idle-class"); }); it("supports render props children", () => { diff --git a/packages/react/src/components/button/button.variants.ts b/packages/react/src/components/button/button.variants.ts new file mode 100644 index 0000000..528af49 --- /dev/null +++ b/packages/react/src/components/button/button.variants.ts @@ -0,0 +1,7 @@ +export interface ButtonVariantsProps { + isFullWidth?: boolean; + isIconOnly?: boolean; + radius?: "none" | "sm" | "md" | "lg" | "xl" | "full"; + size?: "sm" | "md" | "lg" | "xl"; + variant?: "primary" | "secondary" | "ghost" | "outline" | "success" | "warning" | "danger" | "info"; +} diff --git a/packages/react/src/components/button/index.ts b/packages/react/src/components/button/index.ts index 1c3bdcc..bfd832c 100644 --- a/packages/react/src/components/button/index.ts +++ b/packages/react/src/components/button/index.ts @@ -1,2 +1,3 @@ export { Button } from "./Button"; export type { ButtonProps } from "./Button"; +export type { ButtonVariantsProps } from "./button.variants"; diff --git a/packages/react/src/components/icons/ArrowRightIcon.tsx b/packages/react/src/components/icons/ArrowRightIcon.tsx index 2245521..4f01907 100644 --- a/packages/react/src/components/icons/ArrowRightIcon.tsx +++ b/packages/react/src/components/icons/ArrowRightIcon.tsx @@ -1,15 +1,25 @@ "use client"; -import type { SVGProps } from "react"; +import type { ComponentPropsWithRef } from "react"; -function ArrowRightIcon(props: Readonly>): React.JSX.Element { - const { className, ...rest } = props; +const ArrowRightIcon = (props: Readonly>) => { + const { className, ref, ...rest } = props; return ( - + ); -} +}; + +ArrowRightIcon.displayName = "ArrowRightIcon"; export { ArrowRightIcon }; diff --git a/packages/react/src/components/icons/ClipboardIcon.tsx b/packages/react/src/components/icons/ClipboardIcon.tsx index 1611608..f5e4ce8 100644 --- a/packages/react/src/components/icons/ClipboardIcon.tsx +++ b/packages/react/src/components/icons/ClipboardIcon.tsx @@ -1,15 +1,17 @@ "use client"; -import type { SVGProps } from "react"; +import type { ComponentPropsWithRef } from "react"; -function ClipboardIcon(props: Readonly>): React.JSX.Element { - const { className, ...rest } = props; +const ClipboardIcon = (props: Readonly>) => { + const { className, ref, ...rest } = props; return ( - + ); -} +}; + +ClipboardIcon.displayName = "ClipboardIcon"; export { ClipboardIcon }; diff --git a/packages/react/src/components/icons/GitHubIcon.tsx b/packages/react/src/components/icons/GitHubIcon.tsx index 280184e..e2a8173 100644 --- a/packages/react/src/components/icons/GitHubIcon.tsx +++ b/packages/react/src/components/icons/GitHubIcon.tsx @@ -1,15 +1,17 @@ "use client"; -import type { SVGProps } from "react"; +import type { ComponentPropsWithRef } from "react"; -function GitHubIcon(props: Readonly>): React.JSX.Element { - const { className, ...rest } = props; +const GitHubIcon = (props: Readonly>) => { + const { className, ref, ...rest } = props; return ( - + ); -} +}; + +GitHubIcon.displayName = "GitHubIcon"; export { GitHubIcon }; diff --git a/packages/react/src/components/icons/MoreIcon.tsx b/packages/react/src/components/icons/MoreIcon.tsx index 08e6c42..962617a 100644 --- a/packages/react/src/components/icons/MoreIcon.tsx +++ b/packages/react/src/components/icons/MoreIcon.tsx @@ -1,12 +1,20 @@ "use client"; -import type { SVGProps } from "react"; +import type { ComponentPropsWithRef } from "react"; -function MoreIcon(props: Readonly>): React.JSX.Element { - const { className, ...rest } = props; +const MoreIcon = (props: Readonly>) => { + const { className, ref, ...rest } = props; return ( - + >): React.JSX.Element { /> ); -} +}; + +MoreIcon.displayName = "MoreIcon"; export { MoreIcon }; diff --git a/packages/react/src/components/icons/PlusIcon.tsx b/packages/react/src/components/icons/PlusIcon.tsx index ee1eb08..b535918 100644 --- a/packages/react/src/components/icons/PlusIcon.tsx +++ b/packages/react/src/components/icons/PlusIcon.tsx @@ -1,15 +1,25 @@ "use client"; -import type { SVGProps } from "react"; +import type { ComponentPropsWithRef } from "react"; -function PlusIcon(props: Readonly>): React.JSX.Element { - const { className, ...rest } = props; +const PlusIcon = (props: Readonly>) => { + const { className, ref, ...rest } = props; return ( - + ); -} +}; + +PlusIcon.displayName = "PlusIcon"; export { PlusIcon }; diff --git a/packages/react/src/components/icons/ReactAriaIcon.tsx b/packages/react/src/components/icons/ReactAriaIcon.tsx index 514ff0f..e7940da 100644 --- a/packages/react/src/components/icons/ReactAriaIcon.tsx +++ b/packages/react/src/components/icons/ReactAriaIcon.tsx @@ -1,15 +1,17 @@ "use client"; -import type { SVGProps } from "react"; +import type { ComponentPropsWithRef } from "react"; -function ReactAriaIcon(props: Readonly>): React.JSX.Element { - const { className, ...rest } = props; +const ReactAriaIcon = (props: Readonly>) => { + const { className, ref, ...rest } = props; return ( - + ); -} +}; + +ReactAriaIcon.displayName = "ReactAriaIcon"; export { ReactAriaIcon }; diff --git a/packages/react/src/components/icons/SettingsIcon.tsx b/packages/react/src/components/icons/SettingsIcon.tsx index b9e6b17..7370d70 100644 --- a/packages/react/src/components/icons/SettingsIcon.tsx +++ b/packages/react/src/components/icons/SettingsIcon.tsx @@ -1,12 +1,20 @@ "use client"; -import type { SVGProps } from "react"; +import type { ComponentPropsWithRef } from "react"; -function SettingsIcon(props: Readonly>): React.JSX.Element { - const { className, ...rest } = props; +const SettingsIcon = (props: Readonly>) => { + const { className, ref, ...rest } = props; return ( - + >): React.JSX.Eleme ); -} +}; + +SettingsIcon.displayName = "SettingsIcon"; export { SettingsIcon }; diff --git a/packages/react/src/components/icons/ShareIcon.tsx b/packages/react/src/components/icons/ShareIcon.tsx index 6a1b969..2311b0f 100644 --- a/packages/react/src/components/icons/ShareIcon.tsx +++ b/packages/react/src/components/icons/ShareIcon.tsx @@ -1,12 +1,20 @@ "use client"; -import type { SVGProps } from "react"; +import type { ComponentPropsWithRef } from "react"; -function ShareIcon(props: Readonly>): React.JSX.Element { - const { className, ...rest } = props; +const ShareIcon = (props: Readonly>) => { + const { className, ref, ...rest } = props; return ( - + >): React.JSX.Element /> ); -} +}; + +ShareIcon.displayName = "ShareIcon"; export { ShareIcon }; diff --git a/packages/react/src/components/icons/Spinner.tsx b/packages/react/src/components/icons/Spinner.tsx index b972b6e..3514a89 100644 --- a/packages/react/src/components/icons/Spinner.tsx +++ b/packages/react/src/components/icons/Spinner.tsx @@ -1,16 +1,18 @@ "use client"; -import type { SVGProps } from "react"; +import type { ComponentPropsWithRef } from "react"; -function Spinner(props: Readonly>) { - const { className, ...rest } = props; +const Spinner = (props: Readonly>) => { + const { className, ref, ...rest } = props; return ( - + ); -} +}; + +Spinner.displayName = "Spinner"; export { Spinner }; diff --git a/packages/react/src/components/icons/StorybookIcon.tsx b/packages/react/src/components/icons/StorybookIcon.tsx index 390b60b..445edbb 100644 --- a/packages/react/src/components/icons/StorybookIcon.tsx +++ b/packages/react/src/components/icons/StorybookIcon.tsx @@ -1,15 +1,17 @@ "use client"; -import type { SVGProps } from "react"; +import type { ComponentPropsWithRef } from "react"; -function StorybookIcon(props: Readonly>): React.JSX.Element { - const { className, ...rest } = props; +const StorybookIcon = (props: Readonly>) => { + const { className, ref, ...rest } = props; return ( - - + + ); -} +}; + +StorybookIcon.displayName = "StorybookIcon"; export { StorybookIcon }; diff --git a/packages/react/src/components/index.ts b/packages/react/src/components/index.ts index eb9e30a..76cfdc6 100644 --- a/packages/react/src/components/index.ts +++ b/packages/react/src/components/index.ts @@ -1,37 +1,5 @@ -export { isRTL, useLocale, useFilter, Virtualizer, TableLayout, ListLayout } from "react-aria-components"; -export { getLocalizationScript } from "react-aria-components/i18n"; - -export { parseColor } from "react-aria-components"; - -export type { - Direction, - HoverEvent, - Key, - KeyboardEvent, - Orientation, - PointerType, - PressEvent, - RangeValue, - RouterConfig, - Selection, - ValidationResult, -} from "@react-types/shared"; - -export type { TimeValue, DateValue, DateRange, SortDescriptor } from "react-aria-components"; -export type { Color, ColorFormat, ColorSpace, ColorChannel, ColorChannelRange, ColorAxes } from "@react-types/color"; -export { Collection, ListBoxLoadMoreItem, RouterProvider, I18nProvider } from "react-aria-components"; +export { RouterProvider } from "react-aria-components"; +export * from "./badge"; export * from "./button"; - -export { - ArrowRightIcon, - ClipboardIcon, - GitHubIcon, - MoreIcon, - PlusIcon, - ReactAriaIcon, - SettingsIcon, - ShareIcon, - Spinner, - StorybookIcon, -} from "./icons"; +export * from "./icons"; diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 67d470f..40b494c 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -1,2 +1 @@ export * from "./components"; -export { cn } from "./utils/cn"; diff --git a/packages/react/src/utils/bem/bem.ts b/packages/react/src/utils/bem/bem.ts new file mode 100644 index 0000000..24ab938 --- /dev/null +++ b/packages/react/src/utils/bem/bem.ts @@ -0,0 +1,26 @@ +import { clsx } from "clsx"; + +interface BemParams { + block: string; + modifiers: BemModifiers; +} + +interface BemModifiers { + [key: string]: boolean | string | undefined; +} + +function bem(params: Readonly): string { + const { block, modifiers } = params; + + const classes = Object.entries(modifiers).map(([key, value]) => { + if (!value) return undefined; + if (typeof value === "boolean") return `${block}--${key}`; + if (key === "variant") return `${block}--${value}`; + + return `${block}--${key}-${value}`; + }); + + return clsx(block, ...classes); +} + +export { bem }; diff --git a/packages/react/src/utils/bem/index.ts b/packages/react/src/utils/bem/index.ts new file mode 100644 index 0000000..7283795 --- /dev/null +++ b/packages/react/src/utils/bem/index.ts @@ -0,0 +1 @@ +export { bem } from "./bem"; diff --git a/packages/react/src/utils/cn/cn.ts b/packages/react/src/utils/cn/cn.ts deleted file mode 100644 index 456da86..0000000 --- a/packages/react/src/utils/cn/cn.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { ClassValue } from "clsx"; - -import { clsx } from "clsx"; -import { twMerge } from "tailwind-merge"; - -function cn(...inputs: ClassValue[]): string { - return twMerge(clsx(inputs)); -} - -export { cn }; diff --git a/packages/react/src/utils/cn/index.ts b/packages/react/src/utils/cn/index.ts deleted file mode 100644 index c25627d..0000000 --- a/packages/react/src/utils/cn/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { cn } from "./cn"; diff --git a/packages/react/tsup.config.ts b/packages/react/tsup.config.ts index 48510a0..7131881 100644 --- a/packages/react/tsup.config.ts +++ b/packages/react/tsup.config.ts @@ -3,8 +3,8 @@ import { defineConfig } from "tsup"; export default defineConfig({ entry: { index: "src/index.ts", + "components/badge/index": "src/components/badge/index.ts", "components/button/index": "src/components/button/index.ts", - "utils/cn/index": "src/utils/cn/index.ts", }, format: ["esm"], outExtension: () => ({ js: ".mjs" }), diff --git a/packages/styles/src/components/badge.css b/packages/styles/src/components/badge.css new file mode 100644 index 0000000..4093770 --- /dev/null +++ b/packages/styles/src/components/badge.css @@ -0,0 +1,75 @@ +.fri-badge { + @apply inline-flex items-center justify-center; + @apply leading-none font-medium whitespace-nowrap; + @apply border border-transparent select-none; + + /* default variant = primary */ + --fri-badge-bg: var(--color-primary); + --fri-badge-fg: var(--color-primary-foreground); + + @apply bg-(--fri-badge-bg) text-(--fri-badge-fg); + + /* default size = sm */ + @apply gap-1 px-1.5 py-px text-xs; + + /* default radius = full */ + @apply rounded-full; +} + +.fri-badge--primary { + --fri-badge-bg: var(--color-primary); + --fri-badge-fg: var(--color-primary-foreground); +} + +.fri-badge--secondary { + --fri-badge-bg: var(--color-secondary); + --fri-badge-fg: var(--color-secondary-foreground); +} + +.fri-badge--ghost { + --fri-badge-bg: transparent; + --fri-badge-fg: var(--color-foreground); +} + +.fri-badge--outline { + --fri-badge-bg: transparent; + --fri-badge-fg: var(--color-foreground); + + @apply border-border; +} + +.fri-badge--success { + --fri-badge-bg: var(--color-success); + --fri-badge-fg: var(--color-success-foreground); +} + +.fri-badge--warning { + --fri-badge-bg: var(--color-warning); + --fri-badge-fg: var(--color-warning-foreground); +} + +.fri-badge--danger { + --fri-badge-bg: var(--color-danger); + --fri-badge-fg: var(--color-danger-foreground); +} + +.fri-badge--info { + --fri-badge-bg: var(--color-info); + --fri-badge-fg: var(--color-info-foreground); +} + +.fri-badge [slot="icon"] { + @apply size-match-font; +} + +.fri-badge--size-sm { + @apply gap-1 px-1.5 py-px text-xs; +} + +.fri-badge--size-md { + @apply gap-1.5 px-2 py-0.5 text-sm; +} + +.fri-badge--size-lg { + @apply gap-2 px-2.5 py-1 text-base; +} diff --git a/packages/styles/src/components/button.css b/packages/styles/src/components/button.css index 6a00c83..dfcd9c6 100644 --- a/packages/styles/src/components/button.css +++ b/packages/styles/src/components/button.css @@ -4,13 +4,20 @@ @apply cursor-pointer border border-transparent select-none no-highlight; @apply transition-colors duration-(--duration-fast) ease-smooth motion-reduce:transition-none; - --fri-button-bg: transparent; - --fri-button-bg-hover: var(--fri-button-bg); - --fri-button-bg-pressed: var(--fri-button-bg-hover); - --fri-button-fg: currentColor; + /* default variant = primary */ + --fri-button-bg: var(--color-primary); + --fri-button-bg-hover: var(--color-primary-hover); + --fri-button-bg-pressed: var(--color-primary-active); + --fri-button-fg: var(--color-primary-foreground); @apply bg-(--fri-button-bg) text-(--fri-button-fg); + /* default size = md */ + @apply gap-2.5 px-4 py-2 text-base; + + /* default radius = md */ + @apply rounded-md; + &:focus-visible, &[data-focus-visible] { @apply focus-ring; @@ -95,20 +102,20 @@ --fri-button-fg: var(--color-info-foreground); } -.fri-button--sm { - @apply gap-[0.541rem] px-3.5 py-1.5 text-sm; +.fri-button--size-sm { + @apply gap-2 px-3.5 py-1.5 text-sm; } -.fri-button--md { - @apply gap-[0.618rem] px-4 py-2 text-base; +.fri-button--size-md { + @apply gap-2.5 px-4 py-2 text-base; } -.fri-button--lg { - @apply gap-[0.773rem] px-5 py-2.5 text-xl; +.fri-button--size-lg { + @apply gap-3 px-5 py-2.5 text-xl; } -.fri-button--xl { - @apply gap-[0.927rem] px-6 py-3 text-2xl; +.fri-button--size-xl { + @apply gap-4 px-6 py-3 text-2xl; } .fri-button--full-width { @@ -119,19 +126,19 @@ @apply aspect-square justify-center gap-0 p-0; } -.fri-button--icon-only.fri-button--sm { +.fri-button--icon-only.fri-button--size-sm { @apply w-[1.625rem]; } -.fri-button--icon-only.fri-button--md { +.fri-button--icon-only.fri-button--size-md { @apply w-8; } -.fri-button--icon-only.fri-button--lg { +.fri-button--icon-only.fri-button--size-lg { @apply w-10; } -.fri-button--icon-only.fri-button--xl { +.fri-button--icon-only.fri-button--size-xl { @apply w-12; } @@ -163,6 +170,10 @@ @apply text-transparent; } +.fri-button [slot="icon"] { + @apply size-match-font; +} + .fri-button__spinner { @apply absolute inset-0 m-auto size-match-font animate-spin text-(--fri-button-fg); } diff --git a/packages/styles/src/components/index.css b/packages/styles/src/components/index.css index 5d50e2d..2c972b1 100644 --- a/packages/styles/src/components/index.css +++ b/packages/styles/src/components/index.css @@ -1 +1,2 @@ +@import "./badge.css"; @import "./button.css"; diff --git a/packages/styles/src/tokens/colors.css b/packages/styles/src/tokens/colors.css index 2357244..cbe28fb 100644 --- a/packages/styles/src/tokens/colors.css +++ b/packages/styles/src/tokens/colors.css @@ -101,7 +101,8 @@ --color-link: var(--color-neutral-950); --color-link-hover: var(--color-neutral-600); - --color-ring: var(--color-neutral-950); + --color-focus-outer: var(--color-neutral-950); + --color-focus-inner: var(--color-white); --color-overlay: oklch(0% 0 0 / 50%); } @@ -202,7 +203,8 @@ --color-link: var(--color-blue-500); --color-link-hover: var(--color-blue-400); - --color-ring: var(--color-neutral-50); + --color-focus-outer: var(--color-neutral-50); + --color-focus-inner: var(--color-neutral-950); --color-overlay: oklch(0% 0 0 / 70%); } } diff --git a/packages/styles/src/tokens/layout.css b/packages/styles/src/tokens/layout.css index 9fd4a62..0f677b6 100644 --- a/packages/styles/src/tokens/layout.css +++ b/packages/styles/src/tokens/layout.css @@ -3,7 +3,7 @@ --field-radius: calc(var(--radius) * 1.5); --disabled-opacity: 0.5; - --ring-offset-width: 2px; + --outline-offset-focus: 2px; --cursor-interactive: pointer; --cursor-disabled: not-allowed; diff --git a/packages/styles/src/utilities/focus.css b/packages/styles/src/utilities/focus.css index e63f1ba..a96a39e 100644 --- a/packages/styles/src/utilities/focus.css +++ b/packages/styles/src/utilities/focus.css @@ -1,6 +1,3 @@ @utility focus-ring { - outline: none; - box-shadow: - 0 0 0 var(--ring-offset-width) var(--color-background), - 0 0 0 calc(var(--ring-offset-width) + 2px) var(--color-ring); + @apply ring-2 ring-focus-inner outline-2 outline-offset-focus outline-focus-outer; } diff --git a/packages/styles/src/utilities/sizing.css b/packages/styles/src/utilities/sizing.css index a0ed8c9..1290ed6 100644 --- a/packages/styles/src/utilities/sizing.css +++ b/packages/styles/src/utilities/sizing.css @@ -1,4 +1,3 @@ @utility size-match-font { - width: 1em; - height: 1em; + @apply size-[1em]; } diff --git a/packages/styles/src/utilities/status.css b/packages/styles/src/utilities/status.css index 5fe5931..c7ecdca 100644 --- a/packages/styles/src/utilities/status.css +++ b/packages/styles/src/utilities/status.css @@ -1,7 +1,5 @@ @utility status-disabled { - opacity: var(--disabled-opacity); - cursor: var(--cursor-disabled); - pointer-events: none; + @apply pointer-events-none cursor-(--cursor-disabled) opacity-(--disabled-opacity); } @utility status-pending { diff --git a/packages/styles/tailwind.css b/packages/styles/tailwind.css new file mode 100644 index 0000000..5511f2f --- /dev/null +++ b/packages/styles/tailwind.css @@ -0,0 +1,2 @@ +@import "tailwindcss"; +@import "./src/index.css"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 836d0ab..aa816d1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -252,9 +252,6 @@ importers: react-aria-components: specifier: ^1.16.0 version: 1.16.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - tailwind-merge: - specifier: ^3.5.0 - version: 3.5.0 devDependencies: '@fried-ui/vitest': specifier: workspace:* From 75d465dc37a13e2c8e49e9d5db3b7f63224ffc84 Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Sun, 29 Mar 2026 04:42:10 +0700 Subject: [PATCH 039/199] refactor(styles): Update rules, comments, templates --- packages/styles/src/components/button.css | 2 +- packages/styles/src/components/index.css | 12 ++++ packages/styles/src/index.css | 10 +++ packages/styles/src/tokens/colors.css | 66 +++++++++++++++++++ packages/styles/src/tokens/index.css | 10 +++ packages/styles/src/tokens/layout.css | 25 +++++++ packages/styles/src/tokens/motion.css | 24 +++++++ packages/styles/src/tokens/palette.css | 13 ++++ packages/styles/src/tokens/typography.css | 14 ++++ packages/styles/src/utilities/focus.css | 23 +++++++ packages/styles/src/utilities/index.css | 9 +++ packages/styles/src/utilities/interaction.css | 11 ++++ packages/styles/src/utilities/sizing.css | 16 +++++ packages/styles/src/utilities/status.css | 20 ++++++ 14 files changed, 254 insertions(+), 1 deletion(-) diff --git a/packages/styles/src/components/button.css b/packages/styles/src/components/button.css index dfcd9c6..16ef5f1 100644 --- a/packages/styles/src/components/button.css +++ b/packages/styles/src/components/button.css @@ -127,7 +127,7 @@ } .fri-button--icon-only.fri-button--size-sm { - @apply w-[1.625rem]; + @apply w-6.5; } .fri-button--icon-only.fri-button--size-md { diff --git a/packages/styles/src/components/index.css b/packages/styles/src/components/index.css index 2c972b1..3604e1a 100644 --- a/packages/styles/src/components/index.css +++ b/packages/styles/src/components/index.css @@ -1,2 +1,14 @@ +/* + * Component Styles (BEM) + * + * Each component has its own CSS file with: + * .fri-{name} → base (includes defaults — works without modifiers) + * .fri-{name}--{variant} → variant override + * .fri-{name}--size-{x} → size override + * .fri-{name}--{bool} → boolean modifier (disabled, pending, etc.) + * .fri-{name}__element → internal element (spinner, label, etc.) + * .fri-{name} [slot="icon"] → auto-size icons + */ + @import "./badge.css"; @import "./button.css"; diff --git a/packages/styles/src/index.css b/packages/styles/src/index.css index 7b5ccf7..1d8dd50 100644 --- a/packages/styles/src/index.css +++ b/packages/styles/src/index.css @@ -1,3 +1,13 @@ +/* + * @fried-ui/styles — Entry point + * + * Consumer imports this in their CSS: + * @import "tailwindcss"; + * @import "@fried-ui/styles"; + * + * Load order: tokens → utilities → components + */ + @import "./tokens/index.css"; @import "./utilities/index.css"; @import "./components/index.css"; diff --git a/packages/styles/src/tokens/colors.css b/packages/styles/src/tokens/colors.css index cbe28fb..e86e199 100644 --- a/packages/styles/src/tokens/colors.css +++ b/packages/styles/src/tokens/colors.css @@ -1,6 +1,40 @@ +/* + * Semantic Color Tokens + * + * Maps raw palette colors to semantic meanings. + * Light mode: @theme static (default values) + * Dark mode: @layer base .dark (overrides) + * + * Groups (7 tokens each): + * {name} → solid bg (button, badge) + * {name}-hover → bg on hover + * {name}-active → bg on pressed/active + * {name}-foreground → text on solid bg + * {name}-pale → soft bg (alert, soft button) + * {name}-pale-foreground → text on pale bg + * {name}-pale-border → border on pale bg + * + * Palette sources: + * primary → fried-lime (custom brand) + * secondary → neutral (Tailwind built-in) + * success → green (Tailwind) + * warning → amber (Tailwind) + * danger → rose (Tailwind) + * info → blue (Tailwind) + * + * Layout elevation (dark = lighter is higher): + * Layer 1: background → page bg + * Layer 2: background-secondary → sidebar, header + * Layer 3: surface → card, modal + * Layer 4: popover → tooltip, dropdown + */ + @custom-variant dark (&:is(.dark, [data-theme="dark"]) *); +/* ── Light mode (defaults) ────────────────────────────── */ + @theme static { + /* primary — fried-lime brand */ --color-primary: var(--color-fried-lime-500); --color-primary-hover: var(--color-fried-lime-600); --color-primary-active: var(--color-fried-lime-700); @@ -9,6 +43,7 @@ --color-primary-pale-foreground: var(--color-fried-lime-800); --color-primary-pale-border: var(--color-fried-lime-200); + /* secondary — neutral */ --color-secondary: var(--color-neutral-100); --color-secondary-hover: var(--color-neutral-200); --color-secondary-active: var(--color-neutral-300); @@ -17,6 +52,7 @@ --color-secondary-pale-foreground: var(--color-neutral-700); --color-secondary-pale-border: var(--color-neutral-200); + /* success — green */ --color-success: var(--color-green-600); --color-success-hover: var(--color-green-700); --color-success-active: var(--color-green-800); @@ -25,6 +61,7 @@ --color-success-pale-foreground: var(--color-green-800); --color-success-pale-border: var(--color-green-200); + /* warning — amber */ --color-warning: var(--color-amber-500); --color-warning-hover: var(--color-amber-600); --color-warning-active: var(--color-amber-700); @@ -33,6 +70,7 @@ --color-warning-pale-foreground: var(--color-amber-800); --color-warning-pale-border: var(--color-amber-200); + /* danger — rose */ --color-danger: var(--color-rose-600); --color-danger-hover: var(--color-rose-700); --color-danger-active: var(--color-rose-800); @@ -41,6 +79,7 @@ --color-danger-pale-foreground: var(--color-rose-800); --color-danger-pale-border: var(--color-rose-200); + /* info — blue */ --color-info: var(--color-blue-600); --color-info-hover: var(--color-blue-700); --color-info-active: var(--color-blue-800); @@ -49,40 +88,56 @@ --color-info-pale-foreground: var(--color-blue-800); --color-info-pale-border: var(--color-blue-200); + /* layout — page background (layer 1) */ --color-background: var(--color-white); + /* layout — sidebar, header, footer (layer 2) */ --color-background-secondary: var(--color-neutral-50); + /* layout — primary body text */ --color-foreground: var(--color-neutral-950); + /* surface — card, modal (layer 3) */ --color-surface: var(--color-neutral-100); --color-surface-hover: var(--color-neutral-200); --color-surface-active: var(--color-neutral-300); --color-surface-foreground: var(--color-neutral-900); + /* popover — tooltip, dropdown (layer 4, highest) */ --color-popover: var(--color-white); --color-popover-foreground: var(--color-neutral-950); + /* muted — ghost button, tag, chip bg */ --color-muted: var(--color-neutral-100); --color-muted-hover: var(--color-neutral-200); --color-muted-active: var(--color-neutral-300); --color-muted-foreground: var(--color-neutral-500); + /* separator —
, divider lines */ --color-separator: var(--color-neutral-200); + /* skeleton — loading placeholder bg */ --color-skeleton: var(--color-neutral-200); + /* disabled — disabled element bg + text */ --color-disabled: var(--color-neutral-100); --color-disabled-foreground: var(--color-neutral-400); + /* typography — heading, secondary, muted text */ --color-heading: var(--color-neutral-950); --color-foreground-secondary: var(--color-neutral-600); --color-foreground-muted: var(--color-neutral-400); + + /* code — inline code text + bg */ --color-code: var(--color-neutral-800); --color-code-background: var(--color-neutral-100); + + /* mark — highlight */ --color-mark: var(--color-amber-200); --color-mark-foreground: var(--color-neutral-950); + /* border — input, card, outline button */ --color-border: var(--color-neutral-200); --color-border-hover: var(--color-neutral-300); + /* field — input/textarea/select */ --color-field: var(--color-white); --color-field-hover: var(--color-neutral-50); --color-field-foreground: var(--color-neutral-950); @@ -90,22 +145,33 @@ --color-field-border-hover: var(--color-neutral-400); --color-field-placeholder: var(--color-neutral-400); + /* selection — ::selection highlight */ --color-selection: var(--color-fried-lime-200); --color-selection-foreground: var(--color-neutral-950); + /* chart — data visualization colors */ --color-chart-1: var(--color-fried-lime-500); --color-chart-2: var(--color-blue-500); --color-chart-3: var(--color-rose-500); --color-chart-4: var(--color-amber-500); --color-chart-5: var(--color-green-500); + /* link — text color */ --color-link: var(--color-neutral-950); --color-link-hover: var(--color-neutral-600); + + /* focus — two-color focus ring (WCAG AAA) */ + /* outer = outline (dark in light mode, light in dark mode) */ --color-focus-outer: var(--color-neutral-950); + /* inner = ring/box-shadow (contrast with outer) */ --color-focus-inner: var(--color-white); + + /* overlay — modal/dialog backdrop */ --color-overlay: oklch(0% 0 0 / 50%); } +/* ── Dark mode (overrides) ────────────────────────────── */ + @layer base { .dark, [data-theme="dark"] { diff --git a/packages/styles/src/tokens/index.css b/packages/styles/src/tokens/index.css index d983354..b1d14cb 100644 --- a/packages/styles/src/tokens/index.css +++ b/packages/styles/src/tokens/index.css @@ -1,3 +1,13 @@ +/* + * Design Tokens + * + * palette.css → fried-lime brand colors (oklch) + * colors.css → semantic tokens (light + dark mode) + * typography.css → heading sizes (√φ scale) + * layout.css → radius, shadow, z-index, focus, cursor + * motion.css → duration, easing, keyframes, reduced-motion + */ + @import "./palette.css"; @import "./colors.css"; @import "./typography.css"; diff --git a/packages/styles/src/tokens/layout.css b/packages/styles/src/tokens/layout.css index 0f677b6..fdb6588 100644 --- a/packages/styles/src/tokens/layout.css +++ b/packages/styles/src/tokens/layout.css @@ -1,3 +1,28 @@ +/* + * Layout Tokens + * + * --radius → base border-radius unit (0.5rem) + * --field-radius → input/select radius (1.5x base) + * --disabled-opacity → opacity for disabled elements (used by status-disabled utility) + * --outline-offset-focus → gap between element and focus ring (used by focus-ring utility) + * --cursor-interactive → cursor for clickable elements + * --cursor-disabled → cursor for disabled elements (used by status-disabled utility) + * + * Z-index (semantic layers — use via z-floating, z-dialog, etc.): + * 10 floating → dropdown, tooltip + * 20 navigation → sticky header, sidebar + * 30 overlay → backdrop + * 40 dialog → modal, drawer + * 50 alert → toast, notification + * + * Shadow (Framer-inspired — 4 levels + border glow): + * shadow-1 → subtle (input, small card) + * shadow-2 → card + * shadow-3 → dropdown, popover + * shadow-4 → modal, dialog + * shadow-border → dark mode edge highlight (inset glow) + */ + @theme { --radius: 0.5rem; --field-radius: calc(var(--radius) * 1.5); diff --git a/packages/styles/src/tokens/motion.css b/packages/styles/src/tokens/motion.css index f974268..fb8f8ff 100644 --- a/packages/styles/src/tokens/motion.css +++ b/packages/styles/src/tokens/motion.css @@ -1,3 +1,26 @@ +/* + * Motion Tokens + * + * Duration (all < 400ms Doherty Threshold): + * instant → 100ms (toggle, checkbox) + * fast → 150ms (button hover, color change) + * normal → 200ms (dropdown open, fade) + * slow → 300ms (modal enter, slide) + * + * Easing: + * ease-smooth → default for most transitions + * ease-in → element exiting (fade out, slide out) + * ease-out → element entering (fade in, slide in) + * ease-in-out → element moving position + * + * Keyframes: + * fade-in, fade-out, slide-in-up, slide-in-down, scale-in + * All use 200ms ease-out. + * + * Accessibility: + * prefers-reduced-motion: reduce → all durations = 0ms, all animations = none + */ + @theme { --duration-instant: 100ms; --duration-fast: 150ms; @@ -67,6 +90,7 @@ --animate-scale-in: scale-in 200ms var(--ease-out); } +/* Accessibility: disable all motion for users who prefer reduced motion */ @media (prefers-reduced-motion: reduce) { :root { --duration-fast: 0ms; diff --git a/packages/styles/src/tokens/palette.css b/packages/styles/src/tokens/palette.css index c9e2ba4..842edb3 100644 --- a/packages/styles/src/tokens/palette.css +++ b/packages/styles/src/tokens/palette.css @@ -1,3 +1,16 @@ +/* + * Palette — fried-lime brand color + * + * Custom oklch palette for the primary brand color. + * All other colors (green, amber, rose, blue, neutral) come from Tailwind v4 built-in. + * + * Scale: 50 (lightest) → 950 (darkest) + * Hue: 120° (yellow-green) + * + * Usage: referenced by semantic tokens in colors.css + * --color-primary: var(--color-fried-lime-500); + */ + @theme { --color-fried-lime-50: oklch(0.99 0.027 120.44); --color-fried-lime-100: oklch(0.99 0.027 120.44); diff --git a/packages/styles/src/tokens/typography.css b/packages/styles/src/tokens/typography.css index 079bb6f..f9da762 100644 --- a/packages/styles/src/tokens/typography.css +++ b/packages/styles/src/tokens/typography.css @@ -1,3 +1,17 @@ +/* + * Typography Tokens + * + * Heading sizes using √φ (1.272) scale. + * Skip text-lg (18px) — breaks logarithmic progression. + * + * h1 → 2.0625rem (33px) + * h2 → 1.625rem (26px) + * h3 → 1.25rem (20px) + * h4 → 1.125rem (18px) + * h5 → 1rem (16px) + * h6 → 0.875rem (14px) + */ + @theme { --heading-h1: 2.0625rem; --heading-h2: 1.625rem; diff --git a/packages/styles/src/utilities/focus.css b/packages/styles/src/utilities/focus.css index a96a39e..3047b38 100644 --- a/packages/styles/src/utilities/focus.css +++ b/packages/styles/src/utilities/focus.css @@ -1,3 +1,26 @@ +/* + * focus-ring — Two-color focus indicator (WCAG AAA) + * + * Shows on keyboard navigation only (:focus-visible / [data-focus-visible]). + * Does NOT show on mouse click. + * + * Two-color technique (W3C Technique C40): + * outer ring (outline) = --color-focus-outer (dark in light, light in dark) + * inner ring (shadow) = --color-focus-inner (contrast with outer) + * Guarantees 3:1 contrast against ANY background color. + * + * Customizable via tokens: + * --color-focus-outer → outline color + * --color-focus-inner → ring color + * --outline-offset-focus → gap between element and outline (default 2px) + * + * Usage in component CSS: + * &:focus-visible, + * &[data-focus-visible] { + * @apply focus-ring; + * } + */ + @utility focus-ring { @apply ring-2 ring-focus-inner outline-2 outline-offset-focus outline-focus-outer; } diff --git a/packages/styles/src/utilities/index.css b/packages/styles/src/utilities/index.css index 85ec096..ae9a9ed 100644 --- a/packages/styles/src/utilities/index.css +++ b/packages/styles/src/utilities/index.css @@ -1,3 +1,12 @@ +/* + * Custom Utilities (@utility) + * + * focus.css → focus-ring (WCAG AAA two-color keyboard focus) + * status.css → status-disabled, status-pending + * interaction.css → no-highlight (mobile tap) + * sizing.css → size-match-font (icon = 1em) + */ + @import "./focus.css"; @import "./status.css"; @import "./interaction.css"; diff --git a/packages/styles/src/utilities/interaction.css b/packages/styles/src/utilities/interaction.css index ce6b53c..0df4cbd 100644 --- a/packages/styles/src/utilities/interaction.css +++ b/packages/styles/src/utilities/interaction.css @@ -1,3 +1,14 @@ +/* + * no-highlight — Remove tap highlight on mobile + * + * Removes the blue/gray highlight that appears on tap in mobile browsers. + * Applied to interactive components (button, link, etc.). + * + * No Tailwind utility exists for this — plain CSS required. + * + * Usage: @apply no-highlight; + */ + @utility no-highlight { -webkit-tap-highlight-color: transparent; } diff --git a/packages/styles/src/utilities/sizing.css b/packages/styles/src/utilities/sizing.css index 1290ed6..9a0444b 100644 --- a/packages/styles/src/utilities/sizing.css +++ b/packages/styles/src/utilities/sizing.css @@ -1,3 +1,19 @@ +/* + * size-match-font — Lock element size to current font-size + * + * Sets width and height to 1em, making the element scale + * proportionally with the parent's font-size. + * + * Primary use: SVG icons inside components. + * When a Button is size="lg" (text-xl), icons automatically scale up. + * + * Two ways to apply: + * 1. className="size-match-font" (explicit) + * 2. slot="icon" (auto — CSS: .fri-button [slot="icon"] { @apply size-match-font; }) + * + * Usage: @apply size-match-font; + */ + @utility size-match-font { @apply size-[1em]; } diff --git a/packages/styles/src/utilities/status.css b/packages/styles/src/utilities/status.css index c7ecdca..5089a49 100644 --- a/packages/styles/src/utilities/status.css +++ b/packages/styles/src/utilities/status.css @@ -1,7 +1,27 @@ +/* + * status-disabled — Disabled element styling + * + * Reduces opacity, changes cursor, blocks pointer events. + * Uses tokens: --disabled-opacity (0.5), --cursor-disabled (not-allowed) + * + * Usage: @apply status-disabled; + * Used by: .fri-button--disabled, .fri-input--disabled, etc. + */ + @utility status-disabled { @apply pointer-events-none cursor-(--cursor-disabled) opacity-(--disabled-opacity); } +/* + * status-pending — Loading/pending state + * + * Blocks pointer events while loading. Does NOT change opacity + * (component handles visual feedback like spinner or text-transparent). + * + * Usage: @apply status-pending; + * Used by: button[data-pending="true"] + */ + @utility status-pending { @apply pointer-events-none; } From 7c9eb22eb6a2d6bce439472b80a322f2451e2721 Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Sun, 29 Mar 2026 05:45:06 +0700 Subject: [PATCH 040/199] feat(tooling): Turbo gen templates, research-driven agent --- turbo/generators/display-component/config.ts | 63 +++++++ .../templates/Component.tsx.hbs | 39 ++++ .../templates/component.css.hbs | 72 ++++++++ .../templates/component.stories.tsx.hbs | 139 ++++++++++++++ .../templates/component.test.tsx.hbs | 73 ++++++++ .../templates/component.variants.ts.hbs | 4 + .../display-component/templates/index.ts.hbs | 3 + .../interactive-component/config.ts | 55 ++++++ .../templates/Component.tsx.hbs | 42 +++++ .../templates/component.css.hbs | 112 +++++++++++ .../templates/component.stories.tsx.hbs | 174 ++++++++++++++++++ .../templates/component.test.tsx.hbs | 117 ++++++++++++ .../templates/component.variants.ts.hbs | 4 + .../templates/index.ts.hbs | 3 + 14 files changed, 900 insertions(+) create mode 100644 turbo/generators/display-component/config.ts create mode 100644 turbo/generators/display-component/templates/Component.tsx.hbs create mode 100644 turbo/generators/display-component/templates/component.css.hbs create mode 100644 turbo/generators/display-component/templates/component.stories.tsx.hbs create mode 100644 turbo/generators/display-component/templates/component.test.tsx.hbs create mode 100644 turbo/generators/display-component/templates/component.variants.ts.hbs create mode 100644 turbo/generators/display-component/templates/index.ts.hbs create mode 100644 turbo/generators/interactive-component/config.ts create mode 100644 turbo/generators/interactive-component/templates/Component.tsx.hbs create mode 100644 turbo/generators/interactive-component/templates/component.css.hbs create mode 100644 turbo/generators/interactive-component/templates/component.stories.tsx.hbs create mode 100644 turbo/generators/interactive-component/templates/component.test.tsx.hbs create mode 100644 turbo/generators/interactive-component/templates/component.variants.ts.hbs create mode 100644 turbo/generators/interactive-component/templates/index.ts.hbs diff --git a/turbo/generators/display-component/config.ts b/turbo/generators/display-component/config.ts new file mode 100644 index 0000000..7306542 --- /dev/null +++ b/turbo/generators/display-component/config.ts @@ -0,0 +1,63 @@ +import type { PlopTypes } from "@turbo/gen"; + +export default function generator(plop: PlopTypes.NodePlopAPI): void { + plop.setGenerator("display-component", { + description: "Display-only component (native HTML element)", + prompts: [ + { type: "input", name: "name", message: "Component name (PascalCase, e.g. Divider):" }, + { type: "input", name: "element", message: "HTML element (e.g. hr, span, div):", default: "div" }, + { type: "input", name: "defaultVariant", message: "Default variant:", default: "primary" }, + { type: "input", name: "defaultSize", message: "Default size:", default: "md" }, + ], + actions: [ + // CSS + { + type: "add", + path: "packages/styles/src/components/{{dashCase name}}.css", + templateFile: "templates/component.css.hbs", + }, + // Component + { + type: "add", + path: "packages/react/src/components/{{dashCase name}}/{{pascalCase name}}.tsx", + templateFile: "templates/Component.tsx.hbs", + }, + // Variants + { + type: "add", + path: "packages/react/src/components/{{dashCase name}}/{{dashCase name}}.variants.ts", + templateFile: "templates/component.variants.ts.hbs", + }, + // Test + { + type: "add", + path: "packages/react/src/components/{{dashCase name}}/{{dashCase name}}.test.tsx", + templateFile: "templates/component.test.tsx.hbs", + }, + // Stories + { + type: "add", + path: "packages/react/src/components/{{dashCase name}}/{{dashCase name}}.stories.tsx", + templateFile: "templates/component.stories.tsx.hbs", + }, + // Barrel + { + type: "add", + path: "packages/react/src/components/{{dashCase name}}/index.ts", + templateFile: "templates/index.ts.hbs", + }, + // Register CSS import + { + type: "append", + path: "packages/styles/src/components/index.css", + template: '@import "./{{dashCase name}}.css";', + }, + // Register component export + { + type: "append", + path: "packages/react/src/components/index.ts", + template: 'export * from "./{{dashCase name}}";', + }, + ], + }); +} diff --git a/turbo/generators/display-component/templates/Component.tsx.hbs b/turbo/generators/display-component/templates/Component.tsx.hbs new file mode 100644 index 0000000..0402b63 --- /dev/null +++ b/turbo/generators/display-component/templates/Component.tsx.hbs @@ -0,0 +1,39 @@ +"use client"; + +import type { ComponentPropsWithRef } from "react"; + +import { clsx } from "clsx"; + +import { bem } from "../../utils/bem"; + +import type { {{pascalCase name}}VariantsProps } from "./{{dashCase name}}.variants"; + +export type {{pascalCase name}}Props = Omit, "className"> & + {{pascalCase name}}VariantsProps & { + className?: string; + }; + +const {{pascalCase name}} = (props: Readonly<{{pascalCase name}}Props>) => { + const { children, className, ref, size, variant, ...rest } = props; + + const {{camelCase name}}ClassName = clsx( + bem({ + block: "fri-{{dashCase name}}", + modifiers: { + variant, + size, + }, + }), + className, + ); + + return ( + <{{element}} data-slot="{{dashCase name}}" className={ {{camelCase name}}ClassName} ref={ref} {...rest}> + {children} + + ); +}; + +{{pascalCase name}}.displayName = "{{pascalCase name}}"; + +export { {{pascalCase name}} }; diff --git a/turbo/generators/display-component/templates/component.css.hbs b/turbo/generators/display-component/templates/component.css.hbs new file mode 100644 index 0000000..67f3195 --- /dev/null +++ b/turbo/generators/display-component/templates/component.css.hbs @@ -0,0 +1,72 @@ +.fri-{{dashCase name}} { + @apply inline-flex items-center justify-center; + @apply leading-none font-medium whitespace-nowrap; + @apply border border-transparent select-none; + + /* default variant = {{defaultVariant}} */ + --fri-{{dashCase name}}-bg: var(--color-{{defaultVariant}}); + --fri-{{dashCase name}}-fg: var(--color-{{defaultVariant}}-foreground); + + @apply bg-(--fri-{{dashCase name}}-bg) text-(--fri-{{dashCase name}}-fg); + + /* default size = {{defaultSize}} */ + @apply gap-2.5 px-4 py-2 text-base; +} + +.fri-{{dashCase name}}--primary { + --fri-{{dashCase name}}-bg: var(--color-primary); + --fri-{{dashCase name}}-fg: var(--color-primary-foreground); +} + +.fri-{{dashCase name}}--secondary { + --fri-{{dashCase name}}-bg: var(--color-secondary); + --fri-{{dashCase name}}-fg: var(--color-secondary-foreground); +} + +.fri-{{dashCase name}}--ghost { + --fri-{{dashCase name}}-bg: transparent; + --fri-{{dashCase name}}-fg: var(--color-foreground); +} + +.fri-{{dashCase name}}--outline { + --fri-{{dashCase name}}-bg: transparent; + --fri-{{dashCase name}}-fg: var(--color-foreground); + + @apply border-border; +} + +.fri-{{dashCase name}}--success { + --fri-{{dashCase name}}-bg: var(--color-success); + --fri-{{dashCase name}}-fg: var(--color-success-foreground); +} + +.fri-{{dashCase name}}--warning { + --fri-{{dashCase name}}-bg: var(--color-warning); + --fri-{{dashCase name}}-fg: var(--color-warning-foreground); +} + +.fri-{{dashCase name}}--danger { + --fri-{{dashCase name}}-bg: var(--color-danger); + --fri-{{dashCase name}}-fg: var(--color-danger-foreground); +} + +.fri-{{dashCase name}}--info { + --fri-{{dashCase name}}-bg: var(--color-info); + --fri-{{dashCase name}}-fg: var(--color-info-foreground); +} + +.fri-{{dashCase name}} [slot="icon"] { + @apply size-match-font; +} + +.fri-{{dashCase name}}--size-sm { + @apply gap-2 px-3.5 py-1.5 text-sm; +} + +.fri-{{dashCase name}}--size-md { + @apply gap-2.5 px-4 py-2 text-base; +} + +.fri-{{dashCase name}}--size-lg { + @apply gap-3 px-5 py-2.5 text-xl; +} diff --git a/turbo/generators/display-component/templates/component.stories.tsx.hbs b/turbo/generators/display-component/templates/component.stories.tsx.hbs new file mode 100644 index 0000000..af03280 --- /dev/null +++ b/turbo/generators/display-component/templates/component.stories.tsx.hbs @@ -0,0 +1,139 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import { {{pascalCase name}} } from "./{{pascalCase name}}"; + +const meta = { + title: "Components/{{pascalCase name}}", + component: {{pascalCase name}}, + tags: ["autodocs"], + parameters: { + layout: "centered", + }, + args: { + children: "{{pascalCase name}}", + variant: "{{defaultVariant}}", + size: "{{defaultSize}}", + }, + argTypes: { + children: { + control: "text", + description: "{{pascalCase name}} content", + table: { + type: { summary: "ReactNode" }, + category: "Children", + }, + }, + variant: { + control: "select", + options: ["primary", "secondary", "ghost", "outline", "success", "warning", "danger", "info"], + description: "The visual variant of the {{dashCase name}}", + table: { + type: { summary: '"primary" | "secondary" | "ghost" | "outline" | "success" | "warning" | "danger" | "info"' }, + defaultValue: { summary: "{{defaultVariant}}" }, + category: "Style Variants", + }, + }, + size: { + control: "select", + options: ["sm", "md", "lg"], + description: "The size of the {{dashCase name}}", + table: { + type: { summary: '"sm" | "md" | "lg"' }, + defaultValue: { summary: "{{defaultSize}}" }, + category: "Style Variants", + }, + }, + className: { + control: "text", + description: "Additional CSS classes", + table: { + type: { summary: "string" }, + category: "Styling", + }, + }, + }, +} satisfies Meta; + +type Story = StoryObj; + +const Default: Story = { + parameters: { + docs: { + source: { + code: `import { {{pascalCase name}} } from "@fried-ui/react"; + +const Basic = () => { + return <{{pascalCase name}}>Content; +};`, + }, + }, + }, +}; + +const Variants: Story = { + parameters: { + docs: { + source: { + code: `import { {{pascalCase name}} } from "@fried-ui/react"; + +const Variants = () => { + return ( +
+ <{{pascalCase name}} variant="primary">Primary + <{{pascalCase name}} variant="secondary">Secondary + <{{pascalCase name}} variant="ghost">Ghost + <{{pascalCase name}} variant="outline">Outline + <{{pascalCase name}} variant="success">Success + <{{pascalCase name}} variant="warning">Warning + <{{pascalCase name}} variant="danger">Danger + <{{pascalCase name}} variant="info">Info +
+ ); +};`, + }, + }, + }, + render: (args): React.JSX.Element => ( +
+ <{{pascalCase name}} {...args} variant="primary">Primary + <{{pascalCase name}} {...args} variant="secondary">Secondary + <{{pascalCase name}} {...args} variant="ghost">Ghost + <{{pascalCase name}} {...args} variant="outline">Outline + <{{pascalCase name}} {...args} variant="success">Success + <{{pascalCase name}} {...args} variant="warning">Warning + <{{pascalCase name}} {...args} variant="danger">Danger + <{{pascalCase name}} {...args} variant="info">Info +
+ ), +}; + +const Sizes: Story = { + parameters: { + docs: { + source: { + code: `import { {{pascalCase name}} } from "@fried-ui/react"; + +const Sizes = () => { + return ( +
+ <{{pascalCase name}} size="sm">Small + <{{pascalCase name}} size="md">Medium + <{{pascalCase name}} size="lg">Large +
+ ); +};`, + }, + }, + }, + render: (args): React.JSX.Element => ( +
+ <{{pascalCase name}} {...args} size="sm">Small + <{{pascalCase name}} {...args} size="md">Medium + <{{pascalCase name}} {...args} size="lg">Large +
+ ), +}; + +export { Default, Variants, Sizes }; + +export default meta; diff --git a/turbo/generators/display-component/templates/component.test.tsx.hbs b/turbo/generators/display-component/templates/component.test.tsx.hbs new file mode 100644 index 0000000..c974311 --- /dev/null +++ b/turbo/generators/display-component/templates/component.test.tsx.hbs @@ -0,0 +1,73 @@ +import { createRef } from "react"; + +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; + +import { {{pascalCase name}} } from "./{{pascalCase name}}"; + +describe("{{pascalCase name}}", () => { + it("renders with default props", () => { + render(<{{pascalCase name}}>Content); + expect(screen.getByText("Content")).toBeInTheDocument(); + }); + + it("renders only base class without explicit props", () => { + render(<{{pascalCase name}}>Default); + expect(screen.getByText("Default").className).toBe("fri-{{dashCase name}}"); + }); + + it("applies variant class only when explicitly set", () => { + render(<{{pascalCase name}} variant="primary">Test); + expect(screen.getByText("Test").className).toContain("fri-{{dashCase name}}--primary"); + }); + + it("applies all variant classes", () => { + const variants = ["primary", "secondary", "ghost", "outline", "success", "warning", "danger", "info"] as const; + + variants.forEach((variant) => { + const { unmount } = render(<{{pascalCase name}} variant={variant}>{variant}); + expect(screen.getByText(variant).className).toContain(`fri-{{dashCase name}}--${variant}`); + unmount(); + }); + }); + + it("applies size class with key-value format", () => { + const sizes = ["sm", "md", "lg"] as const; + + sizes.forEach((size) => { + const { unmount } = render(<{{pascalCase name}} size={size}>{size}); + expect(screen.getByText(size).className).toContain(`fri-{{dashCase name}}--size-${size}`); + unmount(); + }); + }); + + it("merges custom className", () => { + render(<{{pascalCase name}} className="mt-4">Styled); + expect(screen.getByText("Styled").className).toContain("mt-4"); + }); + + it("forwards ref", () => { + const ref = createRef(); + render(<{{pascalCase name}} ref={ref}>Ref); + expect(ref.current).toBeInstanceOf(HTMLElementTagNameMap["{{element}}"]); + }); + + it("sets data-slot attribute", () => { + render(<{{pascalCase name}}>Slot); + expect(screen.getByText("Slot")).toHaveAttribute("data-slot", "{{dashCase name}}"); + }); + + it("sets displayName", () => { + expect({{pascalCase name}}.displayName).toBe("{{pascalCase name}}"); + }); + + it("passes through native HTML attributes", () => { + render( + <{{pascalCase name}} id="my-{{dashCase name}}" data-testid="custom"> + Props + , + ); + + expect(screen.getByTestId("custom")).toHaveAttribute("id", "my-{{dashCase name}}"); + }); +}); diff --git a/turbo/generators/display-component/templates/component.variants.ts.hbs b/turbo/generators/display-component/templates/component.variants.ts.hbs new file mode 100644 index 0000000..7b916e0 --- /dev/null +++ b/turbo/generators/display-component/templates/component.variants.ts.hbs @@ -0,0 +1,4 @@ +export interface {{pascalCase name}}VariantsProps { + size?: "sm" | "md" | "lg"; + variant?: "primary" | "secondary" | "ghost" | "outline" | "success" | "warning" | "danger" | "info"; +} diff --git a/turbo/generators/display-component/templates/index.ts.hbs b/turbo/generators/display-component/templates/index.ts.hbs new file mode 100644 index 0000000..72ce556 --- /dev/null +++ b/turbo/generators/display-component/templates/index.ts.hbs @@ -0,0 +1,3 @@ +export { {{pascalCase name}} } from "./{{pascalCase name}}"; +export type { {{pascalCase name}}Props } from "./{{pascalCase name}}"; +export type { {{pascalCase name}}VariantsProps } from "./{{dashCase name}}.variants"; diff --git a/turbo/generators/interactive-component/config.ts b/turbo/generators/interactive-component/config.ts new file mode 100644 index 0000000..716bcca --- /dev/null +++ b/turbo/generators/interactive-component/config.ts @@ -0,0 +1,55 @@ +import type { PlopTypes } from "@turbo/gen"; + +export default function generator(plop: PlopTypes.NodePlopAPI): void { + plop.setGenerator("interactive-component", { + description: "Interactive component (wraps React Aria)", + prompts: [ + { type: "input", name: "name", message: "Component name (PascalCase, e.g. Link):" }, + { type: "input", name: "racName", message: "React Aria component name (e.g. Link, Switch):" }, + { type: "input", name: "defaultVariant", message: "Default variant:", default: "primary" }, + { type: "input", name: "defaultSize", message: "Default size:", default: "md" }, + ], + actions: [ + { + type: "add", + path: "packages/styles/src/components/{{dashCase name}}.css", + templateFile: "templates/component.css.hbs", + }, + { + type: "add", + path: "packages/react/src/components/{{dashCase name}}/{{pascalCase name}}.tsx", + templateFile: "templates/Component.tsx.hbs", + }, + { + type: "add", + path: "packages/react/src/components/{{dashCase name}}/{{dashCase name}}.variants.ts", + templateFile: "templates/component.variants.ts.hbs", + }, + { + type: "add", + path: "packages/react/src/components/{{dashCase name}}/{{dashCase name}}.test.tsx", + templateFile: "templates/component.test.tsx.hbs", + }, + { + type: "add", + path: "packages/react/src/components/{{dashCase name}}/{{dashCase name}}.stories.tsx", + templateFile: "templates/component.stories.tsx.hbs", + }, + { + type: "add", + path: "packages/react/src/components/{{dashCase name}}/index.ts", + templateFile: "templates/index.ts.hbs", + }, + { + type: "append", + path: "packages/styles/src/components/index.css", + template: '@import "./{{dashCase name}}.css";', + }, + { + type: "append", + path: "packages/react/src/components/index.ts", + template: 'export * from "./{{dashCase name}}";', + }, + ], + }); +} diff --git a/turbo/generators/interactive-component/templates/Component.tsx.hbs b/turbo/generators/interactive-component/templates/Component.tsx.hbs new file mode 100644 index 0000000..8b12138 --- /dev/null +++ b/turbo/generators/interactive-component/templates/Component.tsx.hbs @@ -0,0 +1,42 @@ +"use client"; + +import type { ComponentPropsWithRef, ReactNode } from "react"; + +import { {{pascalCase racName}} as RAC{{pascalCase name}}, composeRenderProps } from "react-aria-components"; +import type { {{pascalCase racName}}RenderProps } from "react-aria-components"; + +import { clsx } from "clsx"; + +import { bem } from "../../utils/bem"; + +import type { {{pascalCase name}}VariantsProps } from "./{{dashCase name}}.variants"; + +export type {{pascalCase name}}Props = {{pascalCase name}}VariantsProps & { + children?: ReactNode | ((renderProps: {{pascalCase racName}}RenderProps) => ReactNode); + className?: string | ((renderProps: {{pascalCase racName}}RenderProps) => string); +} & Omit, "className" | "children">; + +const {{pascalCase name}} = (props: Readonly<{{pascalCase name}}Props>) => { + const { children, className, ref, size, variant, ...rest } = props; + + const baseClassName = bem({ + block: "fri-{{dashCase name}}", + modifiers: { + variant, + size, + disabled: rest.isDisabled, + }, + }); + + const {{camelCase name}}ClassName = composeRenderProps(className, (consumerClassName) => clsx(baseClassName, consumerClassName)); + + return ( + + {children} + + ); +}; + +{{pascalCase name}}.displayName = "{{pascalCase name}}"; + +export { {{pascalCase name}} }; diff --git a/turbo/generators/interactive-component/templates/component.css.hbs b/turbo/generators/interactive-component/templates/component.css.hbs new file mode 100644 index 0000000..c6db6ba --- /dev/null +++ b/turbo/generators/interactive-component/templates/component.css.hbs @@ -0,0 +1,112 @@ +.fri-{{dashCase name}} { + @apply inline-flex items-center justify-center; + @apply leading-none font-medium whitespace-nowrap; + @apply cursor-pointer border border-transparent select-none no-highlight; + @apply transition-colors duration-(--duration-fast) ease-smooth motion-reduce:transition-none; + + /* default variant = {{defaultVariant}} */ + --fri-{{dashCase name}}-bg: var(--color-{{defaultVariant}}); + --fri-{{dashCase name}}-bg-hover: var(--color-{{defaultVariant}}-hover); + --fri-{{dashCase name}}-bg-pressed: var(--color-{{defaultVariant}}-active); + --fri-{{dashCase name}}-fg: var(--color-{{defaultVariant}}-foreground); + + @apply bg-(--fri-{{dashCase name}}-bg) text-(--fri-{{dashCase name}}-fg); + + /* default size = {{defaultSize}} */ + @apply gap-2.5 px-4 py-2 text-base; + + &:focus-visible, + &[data-focus-visible] { + @apply focus-ring; + } + + &.fri-{{dashCase name}}--disabled { + @apply status-disabled; + } + + &:active, + &[data-pressed] { + @apply bg-(--fri-{{dashCase name}}-bg-pressed); + } + + @media (hover: hover) { + &:hover, + &[data-hovered] { + @apply bg-(--fri-{{dashCase name}}-bg-hover); + } + } +} + +.fri-{{dashCase name}}--primary { + --fri-{{dashCase name}}-bg: var(--color-primary); + --fri-{{dashCase name}}-bg-hover: var(--color-primary-hover); + --fri-{{dashCase name}}-bg-pressed: var(--color-primary-active); + --fri-{{dashCase name}}-fg: var(--color-primary-foreground); +} + +.fri-{{dashCase name}}--secondary { + --fri-{{dashCase name}}-bg: var(--color-secondary); + --fri-{{dashCase name}}-bg-hover: var(--color-secondary-hover); + --fri-{{dashCase name}}-bg-pressed: var(--color-secondary-active); + --fri-{{dashCase name}}-fg: var(--color-secondary-foreground); +} + +.fri-{{dashCase name}}--ghost { + --fri-{{dashCase name}}-bg: transparent; + --fri-{{dashCase name}}-bg-hover: var(--color-muted); + --fri-{{dashCase name}}-bg-pressed: var(--color-muted-hover); + --fri-{{dashCase name}}-fg: var(--color-foreground); +} + +.fri-{{dashCase name}}--outline { + --fri-{{dashCase name}}-bg: transparent; + --fri-{{dashCase name}}-bg-hover: var(--color-surface-hover); + --fri-{{dashCase name}}-bg-pressed: var(--color-surface-active); + --fri-{{dashCase name}}-fg: var(--color-foreground); + + @apply border-border; +} + +.fri-{{dashCase name}}--success { + --fri-{{dashCase name}}-bg: var(--color-success); + --fri-{{dashCase name}}-bg-hover: var(--color-success-hover); + --fri-{{dashCase name}}-bg-pressed: var(--color-success-active); + --fri-{{dashCase name}}-fg: var(--color-success-foreground); +} + +.fri-{{dashCase name}}--warning { + --fri-{{dashCase name}}-bg: var(--color-warning); + --fri-{{dashCase name}}-bg-hover: var(--color-warning-hover); + --fri-{{dashCase name}}-bg-pressed: var(--color-warning-active); + --fri-{{dashCase name}}-fg: var(--color-warning-foreground); +} + +.fri-{{dashCase name}}--danger { + --fri-{{dashCase name}}-bg: var(--color-danger); + --fri-{{dashCase name}}-bg-hover: var(--color-danger-hover); + --fri-{{dashCase name}}-bg-pressed: var(--color-danger-active); + --fri-{{dashCase name}}-fg: var(--color-danger-foreground); +} + +.fri-{{dashCase name}}--info { + --fri-{{dashCase name}}-bg: var(--color-info); + --fri-{{dashCase name}}-bg-hover: var(--color-info-hover); + --fri-{{dashCase name}}-bg-pressed: var(--color-info-active); + --fri-{{dashCase name}}-fg: var(--color-info-foreground); +} + +.fri-{{dashCase name}} [slot="icon"] { + @apply size-match-font; +} + +.fri-{{dashCase name}}--size-sm { + @apply gap-2 px-3.5 py-1.5 text-sm; +} + +.fri-{{dashCase name}}--size-md { + @apply gap-2.5 px-4 py-2 text-base; +} + +.fri-{{dashCase name}}--size-lg { + @apply gap-3 px-5 py-2.5 text-xl; +} diff --git a/turbo/generators/interactive-component/templates/component.stories.tsx.hbs b/turbo/generators/interactive-component/templates/component.stories.tsx.hbs new file mode 100644 index 0000000..febce88 --- /dev/null +++ b/turbo/generators/interactive-component/templates/component.stories.tsx.hbs @@ -0,0 +1,174 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import { {{pascalCase name}} } from "./{{pascalCase name}}"; + +const meta = { + title: "Components/{{pascalCase name}}", + component: {{pascalCase name}}, + tags: ["autodocs"], + parameters: { + layout: "centered", + }, + args: { + children: "{{pascalCase name}}", + variant: "{{defaultVariant}}", + size: "{{defaultSize}}", + }, + argTypes: { + children: { + control: "text", + description: "{{pascalCase name}} content", + table: { + type: { summary: "ReactNode" }, + category: "Children", + }, + }, + variant: { + control: "select", + options: ["primary", "secondary", "ghost", "outline", "success", "warning", "danger", "info"], + description: "The visual variant of the {{dashCase name}}", + table: { + type: { summary: '"primary" | "secondary" | "ghost" | "outline" | "success" | "warning" | "danger" | "info"' }, + defaultValue: { summary: "{{defaultVariant}}" }, + category: "Style Variants", + }, + }, + size: { + control: "select", + options: ["sm", "md", "lg"], + description: "The size of the {{dashCase name}}", + table: { + type: { summary: '"sm" | "md" | "lg"' }, + defaultValue: { summary: "{{defaultSize}}" }, + category: "Style Variants", + }, + }, + isDisabled: { + control: "boolean", + description: "Whether the {{dashCase name}} is disabled", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "false" }, + category: "State", + }, + }, + onPress: { + control: false, + description: "Handler called when pressed", + table: { + type: { summary: "(e: PressEvent) => void" }, + category: "Events", + }, + }, + className: { + control: "text", + description: "Additional CSS classes", + table: { + type: { summary: "string" }, + category: "Styling", + }, + }, + }, +} satisfies Meta; + +type Story = StoryObj; + +const Default: Story = { + parameters: { + docs: { + source: { + code: `import { {{pascalCase name}} } from "@fried-ui/react"; + +const Basic = () => { + return <{{pascalCase name}} onPress={() => console.log("pressed")}>Click me; +};`, + }, + }, + }, +}; + +const Variants: Story = { + parameters: { + docs: { + source: { + code: `import { {{pascalCase name}} } from "@fried-ui/react"; + +const Variants = () => { + return ( +
+ <{{pascalCase name}} variant="primary">Primary + <{{pascalCase name}} variant="secondary">Secondary + <{{pascalCase name}} variant="ghost">Ghost + <{{pascalCase name}} variant="outline">Outline + <{{pascalCase name}} variant="success">Success + <{{pascalCase name}} variant="warning">Warning + <{{pascalCase name}} variant="danger">Danger + <{{pascalCase name}} variant="info">Info +
+ ); +};`, + }, + }, + }, + render: (args): React.JSX.Element => ( +
+ <{{pascalCase name}} {...args} variant="primary">Primary + <{{pascalCase name}} {...args} variant="secondary">Secondary + <{{pascalCase name}} {...args} variant="ghost">Ghost + <{{pascalCase name}} {...args} variant="outline">Outline + <{{pascalCase name}} {...args} variant="success">Success + <{{pascalCase name}} {...args} variant="warning">Warning + <{{pascalCase name}} {...args} variant="danger">Danger + <{{pascalCase name}} {...args} variant="info">Info +
+ ), +}; + +const Sizes: Story = { + parameters: { + docs: { + source: { + code: `import { {{pascalCase name}} } from "@fried-ui/react"; + +const Sizes = () => { + return ( +
+ <{{pascalCase name}} size="sm">Small + <{{pascalCase name}} size="md">Medium + <{{pascalCase name}} size="lg">Large +
+ ); +};`, + }, + }, + }, + render: (args): React.JSX.Element => ( +
+ <{{pascalCase name}} {...args} size="sm">Small + <{{pascalCase name}} {...args} size="md">Medium + <{{pascalCase name}} {...args} size="lg">Large +
+ ), +}; + +const Disabled: Story = { + parameters: { + docs: { + source: { + code: `import { {{pascalCase name}} } from "@fried-ui/react"; + +const Disabled = () => { + return <{{pascalCase name}} isDisabled>Disabled; +};`, + }, + }, + }, + args: { + children: "Disabled", + isDisabled: true, + }, +}; + +export { Default, Variants, Sizes, Disabled }; + +export default meta; diff --git a/turbo/generators/interactive-component/templates/component.test.tsx.hbs b/turbo/generators/interactive-component/templates/component.test.tsx.hbs new file mode 100644 index 0000000..7e00bd0 --- /dev/null +++ b/turbo/generators/interactive-component/templates/component.test.tsx.hbs @@ -0,0 +1,117 @@ +import { createRef } from "react"; + +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { describe, expect, it, vi } from "vitest"; + +import { {{pascalCase name}} } from "./{{pascalCase name}}"; + +describe("{{pascalCase name}}", () => { + it("renders with default props", () => { + render(<{{pascalCase name}}>Content); + expect(screen.getByText("Content")).toBeInTheDocument(); + }); + + it("renders only base class without explicit props", () => { + render(<{{pascalCase name}}>Default); + const el = screen.getByText("Default"); + expect(el.className).toContain("fri-{{dashCase name}}"); + expect(el.className).not.toContain("fri-{{dashCase name}}--primary"); + expect(el.className).not.toContain("fri-{{dashCase name}}--size-md"); + }); + + it("applies variant class only when explicitly set", () => { + render(<{{pascalCase name}} variant="primary">Test); + expect(screen.getByText("Test").className).toContain("fri-{{dashCase name}}--primary"); + }); + + it("applies all variant classes", () => { + const variants = ["primary", "secondary", "ghost", "outline", "success", "warning", "danger", "info"] as const; + + variants.forEach((variant) => { + const { unmount } = render(<{{pascalCase name}} variant={variant}>{variant}); + expect(screen.getByText(variant).className).toContain(`fri-{{dashCase name}}--${variant}`); + unmount(); + }); + }); + + it("applies size class with key-value format", () => { + const sizes = ["sm", "md", "lg"] as const; + + sizes.forEach((size) => { + const { unmount } = render(<{{pascalCase name}} size={size}>{size}); + expect(screen.getByText(size).className).toContain(`fri-{{dashCase name}}--size-${size}`); + unmount(); + }); + }); + + it("merges custom className", () => { + render(<{{pascalCase name}} className="mt-4">Styled); + expect(screen.getByText("Styled").className).toContain("mt-4"); + }); + + it("forwards ref", () => { + const ref = createRef(); + render(<{{pascalCase name}} ref={ref}>Ref); + expect(ref.current).toBeInstanceOf(HTMLElement); + }); + + it("sets data-slot attribute", () => { + render(<{{pascalCase name}}>Slot); + expect(screen.getByText("Slot")).toHaveAttribute("data-slot", "{{dashCase name}}"); + }); + + it("sets displayName", () => { + expect({{pascalCase name}}.displayName).toBe("{{pascalCase name}}"); + }); + + it("applies disabled class", () => { + render(<{{pascalCase name}} isDisabled>Disabled); + expect(screen.getByText("Disabled").className).toContain("fri-{{dashCase name}}--disabled"); + }); + + it("fires onPress when clicked", async () => { + const onPress = vi.fn(); + const user = userEvent.setup(); + render(<{{pascalCase name}} onPress={onPress}>Press); + + await user.click(screen.getByText("Press")); + + expect(onPress).toHaveBeenCalledOnce(); + }); + + it("does not fire onPress when disabled", async () => { + const onPress = vi.fn(); + const user = userEvent.setup(); + + render( + <{{pascalCase name}} onPress={onPress} isDisabled> + Disabled + , + ); + + await user.click(screen.getByText("Disabled")); + + expect(onPress).not.toHaveBeenCalled(); + }); + + it("supports className as render prop function", () => { + render( + <{{pascalCase name}} className={({ isHovered }): string => (isHovered ? "hovered" : "idle")}>Dynamic, + ); + + const el = screen.getByText("Dynamic"); + expect(el.className).toContain("fri-{{dashCase name}}"); + expect(el.className).toContain("idle"); + }); + + it("passes through native HTML attributes", () => { + render( + <{{pascalCase name}} data-testid="custom"> + Props + , + ); + + expect(screen.getByTestId("custom")).toBeInTheDocument(); + }); +}); diff --git a/turbo/generators/interactive-component/templates/component.variants.ts.hbs b/turbo/generators/interactive-component/templates/component.variants.ts.hbs new file mode 100644 index 0000000..7b916e0 --- /dev/null +++ b/turbo/generators/interactive-component/templates/component.variants.ts.hbs @@ -0,0 +1,4 @@ +export interface {{pascalCase name}}VariantsProps { + size?: "sm" | "md" | "lg"; + variant?: "primary" | "secondary" | "ghost" | "outline" | "success" | "warning" | "danger" | "info"; +} diff --git a/turbo/generators/interactive-component/templates/index.ts.hbs b/turbo/generators/interactive-component/templates/index.ts.hbs new file mode 100644 index 0000000..72ce556 --- /dev/null +++ b/turbo/generators/interactive-component/templates/index.ts.hbs @@ -0,0 +1,3 @@ +export { {{pascalCase name}} } from "./{{pascalCase name}}"; +export type { {{pascalCase name}}Props } from "./{{pascalCase name}}"; +export type { {{pascalCase name}}VariantsProps } from "./{{dashCase name}}.variants"; From b1332a22b8eeebad534df16f351c1bad8e8ee141 Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Sun, 29 Mar 2026 06:38:25 +0700 Subject: [PATCH 041/199] docs(decisions): Update 006, add 009-019, remove 001/014 --- packages/styles/src/utilities/sizing.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/styles/src/utilities/sizing.css b/packages/styles/src/utilities/sizing.css index 9a0444b..ee84308 100644 --- a/packages/styles/src/utilities/sizing.css +++ b/packages/styles/src/utilities/sizing.css @@ -15,5 +15,6 @@ */ @utility size-match-font { - @apply size-[1em]; + width: 1em; + height: 1em; } From aeac96147cbf8e54f8443698451272a5973d2a6c Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Sun, 29 Mar 2026 10:36:13 +0700 Subject: [PATCH 042/199] feat(styles): Add isShadow, Surface component, primary neutral theme - Add Surface component with variant, radius, isShadow props - Add isShadow prop to Button and Badge with per-variant colored shadows - Add isIconOnly to Badge component - Add InformationCircleIcon - Change primary color from fried-lime to neutral-950 - Remove palette.css (fried-lime no longer used) - Add pale-shadow tokens for all semantic color groups (light + dark) - Dark mode shadows use colored glow with spread - Golden ratio shadow geometry (2px/3px offset, 3px/5px blur) - Add badge size xl - Remove ghost variant from Surface --- packages/react/package.json | 3 + packages/react/src/components/badge/Badge.tsx | 4 +- .../src/components/badge/badge.stories.tsx | 121 +++++++++- .../react/src/components/badge/badge.test.tsx | 17 +- .../src/components/badge/badge.variants.ts | 4 +- .../react/src/components/button/Button.tsx | 3 +- .../src/components/button/button.stories.tsx | 60 ++++- .../src/components/button/button.test.tsx | 10 + .../src/components/button/button.variants.ts | 1 + .../icons/InformationCircleIcon.tsx | 29 +++ packages/react/src/components/icons/index.ts | 1 + packages/react/src/components/index.ts | 4 + .../react/src/components/surface/Surface.tsx | 49 ++++ .../react/src/components/surface/index.ts | 3 + .../components/surface/surface.stories.tsx | 224 ++++++++++++++++++ .../src/components/surface/surface.test.tsx | 90 +++++++ .../components/surface/surface.variants.ts | 5 + packages/react/tsup.config.ts | 1 + packages/styles/src/components/badge.css | 36 +++ packages/styles/src/components/button.css | 12 + packages/styles/src/components/index.css | 2 + packages/styles/src/components/surface.css | 44 ++++ packages/styles/src/tokens/colors.css | 53 +++-- packages/styles/src/tokens/index.css | 6 +- packages/styles/src/tokens/layout.css | 13 - packages/styles/src/tokens/palette.css | 26 -- turbo/generators/config.ts | 37 +++ .../{templates => }/Component.tsx.hbs | 8 +- .../{templates => }/component.css.hbs | 8 +- .../{templates => }/component.stories.tsx.hbs | 8 +- .../{templates => }/component.test.tsx.hbs | 4 +- .../{templates => }/component.variants.ts.hbs | 0 turbo/generators/display-component/config.ts | 63 ----- .../{templates => }/index.ts.hbs | 0 .../{templates => }/Component.tsx.hbs | 16 +- .../{templates => }/component.css.hbs | 12 +- .../{templates => }/component.stories.tsx.hbs | 8 +- .../{templates => }/component.test.tsx.hbs | 0 .../{templates => }/component.variants.ts.hbs | 0 .../interactive-component/config.ts | 55 ----- .../{templates => }/index.ts.hbs | 0 41 files changed, 815 insertions(+), 225 deletions(-) create mode 100644 packages/react/src/components/icons/InformationCircleIcon.tsx create mode 100644 packages/react/src/components/surface/Surface.tsx create mode 100644 packages/react/src/components/surface/index.ts create mode 100644 packages/react/src/components/surface/surface.stories.tsx create mode 100644 packages/react/src/components/surface/surface.test.tsx create mode 100644 packages/react/src/components/surface/surface.variants.ts create mode 100644 packages/styles/src/components/surface.css delete mode 100644 packages/styles/src/tokens/palette.css create mode 100644 turbo/generators/config.ts rename turbo/generators/display-component/{templates => }/Component.tsx.hbs (78%) rename turbo/generators/display-component/{templates => }/component.css.hbs (88%) rename turbo/generators/display-component/{templates => }/component.stories.tsx.hbs (95%) rename turbo/generators/display-component/{templates => }/component.test.tsx.hbs (94%) rename turbo/generators/display-component/{templates => }/component.variants.ts.hbs (100%) delete mode 100644 turbo/generators/display-component/config.ts rename turbo/generators/display-component/{templates => }/index.ts.hbs (100%) rename turbo/generators/interactive-component/{templates => }/Component.tsx.hbs (50%) rename turbo/generators/interactive-component/{templates => }/component.css.hbs (89%) rename turbo/generators/interactive-component/{templates => }/component.stories.tsx.hbs (96%) rename turbo/generators/interactive-component/{templates => }/component.test.tsx.hbs (100%) rename turbo/generators/interactive-component/{templates => }/component.variants.ts.hbs (100%) delete mode 100644 turbo/generators/interactive-component/config.ts rename turbo/generators/interactive-component/{templates => }/index.ts.hbs (100%) diff --git a/packages/react/package.json b/packages/react/package.json index 512efe0..46eef24 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -31,6 +31,9 @@ }, "./button": { "import": "./src/components/button/index.ts" + }, + "./surface": { + "import": "./src/components/surface/index.ts" } }, "main": "./dist/index.mjs", diff --git a/packages/react/src/components/badge/Badge.tsx b/packages/react/src/components/badge/Badge.tsx index c0b0f0a..a4b8264 100644 --- a/packages/react/src/components/badge/Badge.tsx +++ b/packages/react/src/components/badge/Badge.tsx @@ -14,7 +14,7 @@ export type BadgeProps = Omit, "className"> & }; const Badge = (props: Readonly) => { - const { children, className, ref, size, variant, ...rest } = props; + const { children, className, isIconOnly, isShadow, ref, size, variant, ...rest } = props; const badgeClassName = clsx( bem({ @@ -22,6 +22,8 @@ const Badge = (props: Readonly) => { modifiers: { variant, size, + "icon-only": isIconOnly, + shadow: isShadow, }, }), className, diff --git a/packages/react/src/components/badge/badge.stories.tsx b/packages/react/src/components/badge/badge.stories.tsx index 055a92c..3f6a767 100644 --- a/packages/react/src/components/badge/badge.stories.tsx +++ b/packages/react/src/components/badge/badge.stories.tsx @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from "@storybook/react"; -import { SettingsIcon } from "../icons"; +import { InformationCircleIcon, SettingsIcon } from "../icons"; import { Badge } from "./Badge"; const meta = { @@ -42,11 +42,11 @@ const meta = { }, size: { control: "select", - options: ["sm", "md", "lg"], + options: ["sm", "md", "lg", "xl"], description: "The size of the badge", table: { type: { - summary: '"sm" | "md" | "lg"', + summary: '"sm" | "md" | "lg" | "xl"', }, defaultValue: { summary: "md", @@ -54,6 +54,32 @@ const meta = { category: "Style Variants", }, }, + isIconOnly: { + control: "boolean", + description: "Whether the badge is icon-only (square)", + table: { + type: { + summary: "boolean", + }, + defaultValue: { + summary: "false", + }, + category: "Style Variants", + }, + }, + isShadow: { + control: "boolean", + description: "Whether to show a colored drop shadow", + table: { + type: { + summary: "boolean", + }, + defaultValue: { + summary: "false", + }, + category: "Style Variants", + }, + }, className: { control: "text", description: "Additional CSS classes", @@ -155,6 +181,7 @@ const Sizes = () => { Small Medium Large + Extra Large
); };`, @@ -174,6 +201,10 @@ const Sizes = () => { Large + + + Extra Large +
), }; @@ -226,6 +257,88 @@ const WithIconAlt = () => { ), }; -export { Default, Variants, Sizes, WithIcon }; +const Shadow: Story = { + parameters: { + docs: { + source: { + code: `import { Badge } from "@fried-ui/react"; + +const Shadow = () => { + return ( +
+ Primary + Success + Warning + Danger + Info +
+ ); +};`, + }, + }, + }, + render: (args): React.JSX.Element => ( +
+ + Primary + + + + Success + + + + Warning + + + + Danger + + + + Info + +
+ ), +}; + +const IconOnly: Story = { + parameters: { + docs: { + source: { + code: `import { Badge, InformationCircleIcon } from "@fried-ui/react"; + +const IconOnly = () => { + return ( + + + + ); +};`, + }, + }, + }, + render: (args): React.JSX.Element => ( +
+ + + + + + + + + + + + + + + +
+ ), +}; + +export { Default, Variants, Sizes, Shadow, WithIcon, IconOnly }; export default meta; diff --git a/packages/react/src/components/badge/badge.test.tsx b/packages/react/src/components/badge/badge.test.tsx index 99193ef..58e35a5 100644 --- a/packages/react/src/components/badge/badge.test.tsx +++ b/packages/react/src/components/badge/badge.test.tsx @@ -37,7 +37,7 @@ describe("Badge", () => { }); it("applies size class with key-value format", () => { - const sizes = ["sm", "md", "lg"] as const; + const sizes = ["sm", "md", "lg", "xl"] as const; sizes.forEach((size) => { const { unmount } = render({size}); @@ -46,6 +46,21 @@ describe("Badge", () => { }); }); + it("applies icon-only class", () => { + render(X); + expect(screen.getByText("X").className).toContain("fri-badge--icon-only"); + }); + + it("applies shadow class when isShadow is true", () => { + render(Shadow); + expect(screen.getByText("Shadow").className).toContain("fri-badge--shadow"); + }); + + it("does not apply shadow class when isShadow is false", () => { + render(No shadow); + expect(screen.getByText("No shadow").className).not.toContain("fri-badge--shadow"); + }); + it("merges custom className", () => { render(Styled); expect(screen.getByText("Styled").className).toContain("mt-4"); diff --git a/packages/react/src/components/badge/badge.variants.ts b/packages/react/src/components/badge/badge.variants.ts index 7cce09b..ee361aa 100644 --- a/packages/react/src/components/badge/badge.variants.ts +++ b/packages/react/src/components/badge/badge.variants.ts @@ -1,4 +1,6 @@ export interface BadgeVariantsProps { - size?: "sm" | "md" | "lg"; + isIconOnly?: boolean; + isShadow?: boolean; + size?: "sm" | "md" | "lg" | "xl"; variant?: "primary" | "secondary" | "ghost" | "outline" | "success" | "warning" | "danger" | "info"; } diff --git a/packages/react/src/components/button/Button.tsx b/packages/react/src/components/button/Button.tsx index a0daf00..8f889e2 100644 --- a/packages/react/src/components/button/Button.tsx +++ b/packages/react/src/components/button/Button.tsx @@ -18,7 +18,7 @@ export type ButtonProps = ButtonVariantsProps & { } & Omit, "className" | "children">; const Button = (props: Readonly) => { - const { children, className, isFullWidth, isIconOnly, radius, ref, size, variant, ...rest } = props; + const { children, className, isFullWidth, isIconOnly, isShadow, radius, ref, size, variant, ...rest } = props; const baseClassName = bem({ block: "fri-button", @@ -28,6 +28,7 @@ const Button = (props: Readonly) => { radius, "full-width": isFullWidth, "icon-only": isIconOnly, + shadow: isShadow, disabled: rest.isDisabled, pending: rest.isPending, }, diff --git a/packages/react/src/components/button/button.stories.tsx b/packages/react/src/components/button/button.stories.tsx index d0a77f4..4e01056 100644 --- a/packages/react/src/components/button/button.stories.tsx +++ b/packages/react/src/components/button/button.stories.tsx @@ -82,6 +82,19 @@ const meta = { category: "Style Variants", }, }, + isShadow: { + control: "boolean", + description: "Whether to show a colored drop shadow", + table: { + type: { + summary: "boolean", + }, + defaultValue: { + summary: "false", + }, + category: "Style Variants", + }, + }, isFullWidth: { control: "boolean", description: "Whether the button takes full width of its container", @@ -491,6 +504,51 @@ const RenderProps = () => { ), }; -export { Default, Variants, Sizes, Radii, WithIcon, IconOnly, FullWidth, Disabled, Pending, RenderProps }; +const Shadow: Story = { + parameters: { + docs: { + source: { + code: `import { Button } from "@fried-ui/react"; + +const Shadow = () => { + return ( +
+ + + + + +
+ ); +};`, + }, + }, + }, + render: (args): React.JSX.Element => ( +
+ + + + + + + + + +
+ ), +}; + +export { Default, Variants, Sizes, Radii, Shadow, WithIcon, IconOnly, FullWidth, Disabled, Pending, RenderProps }; export default meta; diff --git a/packages/react/src/components/button/button.test.tsx b/packages/react/src/components/button/button.test.tsx index e800064..928ab9c 100644 --- a/packages/react/src/components/button/button.test.tsx +++ b/packages/react/src/components/button/button.test.tsx @@ -103,6 +103,16 @@ describe("Button", () => { expect(ref.current).toBeInstanceOf(HTMLButtonElement); }); + it("applies shadow class when isShadow is true", () => { + render(); + expect(screen.getByRole("button").className).toContain("fri-button--shadow"); + }); + + it("does not apply shadow class when isShadow is false", () => { + render(); + expect(screen.getByRole("button").className).not.toContain("fri-button--shadow"); + }); + it("applies icon-only class", () => { render(
); };`, @@ -165,6 +199,54 @@ const Variants = () => { Info + + + Primary Soft + + + + Secondary Soft + + + + Success Soft + + + + Warning Soft + + + + Danger Soft + + + + Info Soft + + + + Primary Outline + + + + Secondary Outline + + + + Success Outline + + + + Warning Outline + + + + Danger Outline + + + + Info Outline +
), }; @@ -181,7 +263,6 @@ const Sizes = () => { Small Medium Large - Extra Large
); };`, @@ -201,10 +282,6 @@ const Sizes = () => { Large - - - Extra Large -
), }; @@ -331,14 +408,60 @@ const IconOnly = () => { +
+ ), +}; - - +const SoftVariants: Story = { + parameters: { + docs: { + source: { + code: `import { Badge } from "@fried-ui/react"; + +const SoftVariants = () => { + return ( +
+ Primary + Secondary + Success + Warning + Danger + Info +
+ ); +};`, + }, + }, + }, + render: (args): React.JSX.Element => ( +
+ + Primary + + + + Secondary + + + + Success + + + + Warning + + + + Danger + + + + Info
), }; -export { Default, Variants, Sizes, Shadow, WithIcon, IconOnly }; +export { Default, Variants, SoftVariants, Sizes, Shadow, WithIcon, IconOnly }; export default meta; diff --git a/packages/react/src/components/badge/badge.test.tsx b/packages/react/src/components/badge/badge.test.tsx index 58e35a5..68aba4d 100644 --- a/packages/react/src/components/badge/badge.test.tsx +++ b/packages/react/src/components/badge/badge.test.tsx @@ -27,7 +27,28 @@ describe("Badge", () => { }); it("applies all variant classes", () => { - const variants = ["primary", "secondary", "ghost", "outline", "success", "warning", "danger", "info"] as const; + const variants = [ + "primary", + "secondary", + "ghost", + "outline", + "success", + "warning", + "danger", + "info", + "primary-soft", + "secondary-soft", + "success-soft", + "warning-soft", + "danger-soft", + "info-soft", + "primary-outline", + "secondary-outline", + "success-outline", + "warning-outline", + "danger-outline", + "info-outline", + ] as const; variants.forEach((variant) => { const { unmount } = render({variant}); @@ -37,7 +58,7 @@ describe("Badge", () => { }); it("applies size class with key-value format", () => { - const sizes = ["sm", "md", "lg", "xl"] as const; + const sizes = ["sm", "md", "lg"] as const; sizes.forEach((size) => { const { unmount } = render({size}); diff --git a/packages/react/src/components/badge/badge.variants.ts b/packages/react/src/components/badge/badge.variants.ts index ee361aa..d2125c5 100644 --- a/packages/react/src/components/badge/badge.variants.ts +++ b/packages/react/src/components/badge/badge.variants.ts @@ -1,6 +1,26 @@ export interface BadgeVariantsProps { isIconOnly?: boolean; isShadow?: boolean; - size?: "sm" | "md" | "lg" | "xl"; - variant?: "primary" | "secondary" | "ghost" | "outline" | "success" | "warning" | "danger" | "info"; + size?: "sm" | "md" | "lg"; + variant?: + | "primary" + | "secondary" + | "ghost" + | "outline" + | "success" + | "warning" + | "danger" + | "info" + | "primary-soft" + | "secondary-soft" + | "success-soft" + | "warning-soft" + | "danger-soft" + | "info-soft" + | "primary-outline" + | "secondary-outline" + | "success-outline" + | "warning-outline" + | "danger-outline" + | "info-outline"; } diff --git a/packages/styles/src/components/badge.css b/packages/styles/src/components/badge.css index 8fa44b2..173d1f1 100644 --- a/packages/styles/src/components/badge.css +++ b/packages/styles/src/components/badge.css @@ -9,8 +9,8 @@ @apply bg-(--fri-badge-bg) text-(--fri-badge-fg); - /* default size = sm */ - @apply gap-1 px-1.5 py-px text-xs; + /* default size = md */ + @apply gap-1.5 px-2 py-0.5 text-sm; /* default radius = full */ @apply rounded-full; @@ -70,6 +70,90 @@ --fri-badge-shadow: var(--color-info-shadow); } +.fri-badge--primary-soft { + --fri-badge-bg: var(--color-primary-pale); + --fri-badge-fg: var(--color-primary-pale-foreground); + + @apply border border-primary-pale-border; +} + +.fri-badge--secondary-soft { + --fri-badge-bg: var(--color-secondary-pale); + --fri-badge-fg: var(--color-secondary-pale-foreground); + + @apply border border-secondary-pale-border; +} + +.fri-badge--success-soft { + --fri-badge-bg: var(--color-success-pale); + --fri-badge-fg: var(--color-success-pale-foreground); + + @apply border border-success-pale-border; +} + +.fri-badge--warning-soft { + --fri-badge-bg: var(--color-warning-pale); + --fri-badge-fg: var(--color-warning-pale-foreground); + + @apply border border-warning-pale-border; +} + +.fri-badge--danger-soft { + --fri-badge-bg: var(--color-danger-pale); + --fri-badge-fg: var(--color-danger-pale-foreground); + + @apply border border-danger-pale-border; +} + +.fri-badge--info-soft { + --fri-badge-bg: var(--color-info-pale); + --fri-badge-fg: var(--color-info-pale-foreground); + + @apply border border-info-pale-border; +} + +.fri-badge--primary-outline { + --fri-badge-bg: transparent; + --fri-badge-fg: var(--color-primary); + + @apply border border-primary; +} + +.fri-badge--secondary-outline { + --fri-badge-bg: transparent; + --fri-badge-fg: var(--color-secondary-foreground); + + @apply border border-secondary; +} + +.fri-badge--success-outline { + --fri-badge-bg: transparent; + --fri-badge-fg: var(--color-success); + + @apply border border-success; +} + +.fri-badge--warning-outline { + --fri-badge-bg: transparent; + --fri-badge-fg: var(--color-warning); + + @apply border border-warning; +} + +.fri-badge--danger-outline { + --fri-badge-bg: transparent; + --fri-badge-fg: var(--color-danger); + + @apply border border-danger; +} + +.fri-badge--info-outline { + --fri-badge-bg: transparent; + --fri-badge-fg: var(--color-info); + + @apply border border-info; +} + .fri-badge--icon-only { @apply justify-center gap-0 rounded-full; } @@ -86,10 +170,6 @@ @apply p-1; } -.fri-badge--icon-only.fri-badge--size-xl { - @apply p-1.5; -} - .fri-badge [slot="icon"] { @apply size-match-font; } @@ -105,7 +185,3 @@ .fri-badge--size-lg { @apply gap-2 px-2.5 py-1 text-base; } - -.fri-badge--size-xl { - @apply gap-2.5 px-3 py-1.5 text-lg; -} From c073f2a4f0285c9b49064dd34c6803491036fe65 Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Mon, 30 Mar 2026 01:13:30 +0700 Subject: [PATCH 046/199] docs(styles): Update design tokens docs, remove @research requirement --- apps/docs/src/content/docs/styles/colors.mdx | 23 ++- .../src/content/docs/styles/design-tokens.mdx | 144 +++++++++++++++--- .../react/src/components/surface/Surface.tsx | 9 -- 3 files changed, 145 insertions(+), 31 deletions(-) diff --git a/apps/docs/src/content/docs/styles/colors.mdx b/apps/docs/src/content/docs/styles/colors.mdx index 9ea7ff2..8ac3049 100644 --- a/apps/docs/src/content/docs/styles/colors.mdx +++ b/apps/docs/src/content/docs/styles/colors.mdx @@ -10,9 +10,9 @@ Your main brand color used for primary actions and CTAs. +## Focus Ring + +Two-color focus indicator for keyboard navigation (WCAG AAA). + + + ## How to Use ```tsx diff --git a/apps/docs/src/content/docs/styles/design-tokens.mdx b/apps/docs/src/content/docs/styles/design-tokens.mdx index 61d2413..0260249 100644 --- a/apps/docs/src/content/docs/styles/design-tokens.mdx +++ b/apps/docs/src/content/docs/styles/design-tokens.mdx @@ -1,6 +1,6 @@ --- title: Design Tokens -description: Color, spacing, elevation, and motion tokens that power every fried-ui component. +description: Color, spacing, elevation, motion, and layout tokens that power every fried-ui component. --- ## Overview @@ -33,6 +33,23 @@ Available groups: `primary`, `secondary`, `success`, `warning`, `danger`, `info`
Soft
``` +## Shadow + +Each color group has a shadow token using `em` units — shadows scale proportionally with the component's font-size. + +| Token | Usage | +| ----------------------- | ------------------------------- | +| `--color-{name}-shadow` | Colored drop shadow per variant | + +Light mode uses a tight two-layer shadow (Fibonacci: 2, 3, 5 → em). Dark mode uses a softer glow with larger blur. + +Applied via the `isShadow` prop on components: + +```tsx + +Larger shadow +``` + ## Layout Elevation 4 layers from lowest to highest: @@ -51,7 +68,9 @@ Available groups: `primary`, `secondary`, `success`, `warning`, `danger`, `info`
Dropdown
``` -## Typography Hierarchy +## Typography + +### Hierarchy | Token | Purpose | | ---------------------- | ------------------------ | @@ -67,31 +86,116 @@ Available groups: `primary`, `secondary`, `success`, `warning`, `danger`, `info` Updated 2 hours ago ``` +### Heading Sizes (√φ scale) + +Sizes derived from √φ (1.272): + +| Level | Token | Size | +| ----- | -------------- | --------- | +| h1 | `--heading-h1` | 2.0625rem | +| h2 | `--heading-h2` | 1.625rem | +| h3 | `--heading-h3` | 1.25rem | +| h4 | `--heading-h4` | 1.125rem | +| h5 | `--heading-h5` | 1rem | +| h6 | `--heading-h6` | 0.875rem | + ## Spacing (Golden Ratio) -All spacing derives from font-size × φ (1.618): +All spacing derives from font-size × φ (1.618), rounded to nearest Tailwind step: + +| Property | Formula | sm | md | lg | xl | +| -------------- | --------- | ------- | --------- | ------- | -------- | +| Font | x | text-sm | text-base | text-xl | text-2xl | +| Padding Inline | x | px-3.5 | px-4 | px-5 | px-6 | +| Padding Block | x × 0.485 | py-1.5 | py-2 | py-2.5 | py-3 | +| Gap | x / φ | gap-2 | gap-2.5 | gap-3 | gap-4 | + +## Layout + +### Radius + +| Token | Value | Usage | +| ---------------- | ------------------------- | -------------------- | +| `--radius` | 0.5rem | Base border-radius | +| `--field-radius` | calc(var(--radius) × 1.5) | Input, select radius | + +### Z-Index (Semantic Layers) -| Property | Formula | sm | md | lg | xl | -| -------------- | --------- | -------- | -------- | -------- | -------- | -| Font | x | 0.875rem | 1rem | 1.25rem | 1.5rem | -| Padding Inline | x | 0.875rem | 1rem | 1.25rem | 1.5rem | -| Padding Block | x × 0.485 | 0.375rem | 0.5rem | 0.625rem | 0.75rem | -| Gap | x / φ | 0.541rem | 0.618rem | 0.773rem | 0.927rem | +| Token | Value | Usage | +| ---------------- | ----- | ---------------------- | +| `--z-floating` | 10 | Dropdown, tooltip | +| `--z-navigation` | 20 | Sticky header, sidebar | +| `--z-overlay` | 30 | Backdrop | +| `--z-dialog` | 40 | Modal, drawer | +| `--z-alert` | 50 | Toast, notification | -## Shadow (Framer-inspired) +```tsx +
Dropdown
+
Modal
+``` + +### Disabled & Focus + +| Token | Value | Usage | +| ------------------------ | ----------- | ---------------------------------- | +| `--disabled-opacity` | 0.5 | Opacity for disabled elements | +| `--outline-offset-focus` | 2px | Gap between element and focus ring | +| `--cursor-interactive` | pointer | Clickable elements | +| `--cursor-disabled` | not-allowed | Disabled elements | + +## Motion + +All durations stay under 400ms (Doherty Threshold). Automatically disabled when `prefers-reduced-motion: reduce`. + +### Duration + +| Token | Value | Usage | +| -------------------- | ----- | -------------------------- | +| `--duration-instant` | 100ms | Toggle, checkbox | +| `--duration-fast` | 150ms | Button hover, color change | +| `--duration-normal` | 200ms | Dropdown open, fade | +| `--duration-slow` | 300ms | Modal enter, slide | + +### Easing -| Token | Usage | -| --------------- | ------------------------ | -| `shadow-1` | Subtle hover lift | -| `shadow-2` | Card, panel | -| `shadow-3` | Dropdown, popover | -| `shadow-4` | Modal, dialog | -| `shadow-border` | Dark mode card edge glow | +| Token | Value | Usage | +| --------------- | ---------------------------- | ----------------------- | +| `--ease-smooth` | cubic-bezier(0.4, 0, 0.2, 1) | Default for most | +| `--ease-in` | cubic-bezier(0.4, 0, 1, 1) | Element exiting | +| `--ease-out` | cubic-bezier(0, 0, 0.2, 1) | Element entering | +| `--ease-in-out` | cubic-bezier(0.4, 0, 0.2, 1) | Element moving position | ```tsx -
Card with shadow
+
Hover me
``` +### Animations + +| Token | Keyframes | Usage | +| ------------------------- | ------------------- | ------------- | +| `--animate-fade-in` | fade-in 200ms | Enter | +| `--animate-fade-out` | fade-out 200ms | Exit | +| `--animate-slide-in-up` | slide-in-up 200ms | Dropdown open | +| `--animate-slide-in-down` | slide-in-down 200ms | Tooltip below | +| `--animate-scale-in` | scale-in 200ms | Modal enter | + +```tsx +
Entering
+
Dropdown
+``` + +## Utilities + +Custom `@utility` directives available as Tailwind classes: + +| Utility | Purpose | +| ----------------- | ------------------------------------------------ | +| `focus-ring` | Two-color focus indicator (WCAG AAA) | +| `status-disabled` | opacity + not-allowed cursor + no pointer events | +| `status-pending` | No pointer events (component shows spinner) | +| `no-highlight` | Remove mobile tap highlight | +| `size-match-font` | 1em × 1em (icon scales with font-size) | + ## Border & Field | Token | Purpose | @@ -107,10 +211,10 @@ All spacing derives from font-size × φ (1.618): Dark mode activates via `.dark` class or `data-theme="dark"`: ```html - + ``` -All tokens automatically switch. Light values are defined in `@theme`, dark overrides in `@layer base .dark`. +All tokens automatically switch. Light values are defined in `@theme`, dark overrides in `@layer base`. ## Chart Colors diff --git a/packages/react/src/components/surface/Surface.tsx b/packages/react/src/components/surface/Surface.tsx index 8bc24ca..857ef7f 100644 --- a/packages/react/src/components/surface/Surface.tsx +++ b/packages/react/src/components/surface/Surface.tsx @@ -8,15 +8,6 @@ import { bem } from "../../utils/bem"; import type { SurfaceVariantsProps } from "./surface.variants"; -/** - * Surface — visual layer for elevated content. - * - * @research Nielsen Norman Group (2019) — Visual layering through elevation - * reduces cognitive load by grouping related content. - * @research Material Design 3 — Elevation system maps shadow + surface tint - * to create depth hierarchy. Dark mode uses lighter surfaces for higher elevation. - */ - export type SurfaceProps = Omit, "className"> & SurfaceVariantsProps & { className?: string; From e30a4d45f77e27546ceee9114fbfebcee501bba7 Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Mon, 30 Mar 2026 01:23:48 +0700 Subject: [PATCH 047/199] docs(readme): Rewrite README, update introduction --- README.md | 52 ++++--------------- .../content/docs/overview/introduction.mdx | 3 +- 2 files changed, 13 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index ca027ba..951aeb0 100644 --- a/README.md +++ b/README.md @@ -1,55 +1,25 @@ # fried-ui -Beautiful, accessible React components for building modern web apps at scale. +Beautiful, accessible React components with zero runtime styling. -## Packages +## Features -| Package | Description | -| ------------------------------------- | -------------------------------------------------- | -| [`@fried-ui/react`](packages/react) | React component library | -| [`@fried-ui/styles`](packages/styles) | Theme engine, design tokens, and component presets | +- **Zero runtime** — Pure CSS with BEM, 0.3KB total (clsx only) +- **Accessible** — React Aria components, WCAG AAA two-color focus ring +- **Single prop API** — `variant="success-soft"` — one decision, not two +- **Golden ratio spacing** — All spacing derived from φ (1.618) +- **em-based shadows** — Colored shadows that scale with font-size +- **Multi-framework CSS** — `@fried-ui/styles` works with React, Vue, Svelte, plain HTML +- **Dark mode** — All tokens switch automatically -## Quick Start +## Documentation -```bash -pnpm add @fried-ui/react @fried-ui/styles -``` - -```css -@import "tailwindcss"; -@import "@fried-ui/styles"; -``` - -```tsx -import { Button } from "@fried-ui/react"; - -; -``` - -## Development - -```bash -pnpm install # Install dependencies -pnpm dev # Dev mode (all apps + packages) -pnpm build # Build all -pnpm test # Run tests -pnpm lint # Lint -pnpm typecheck # Type check -pnpm format # Format check -pnpm dev:storybook # Storybook on port 6006 -pnpm dev:docs # Docs on port 3001 -``` +Visit [fried-ui.com](https://fried-ui.com) for full documentation, component demos, and getting started guide. ## Contributing This project uses [conventional commits](https://www.conventionalcommits.org/) and [changesets](https://github.com/changesets/changesets) for versioning. -```bash -pnpm changeset # Add a changeset before submitting a PR -``` - ## License [MIT](LICENSE) diff --git a/apps/docs/src/content/docs/overview/introduction.mdx b/apps/docs/src/content/docs/overview/introduction.mdx index bb84f4f..824008b 100644 --- a/apps/docs/src/content/docs/overview/introduction.mdx +++ b/apps/docs/src/content/docs/overview/introduction.mdx @@ -13,7 +13,8 @@ fried-ui is a React component library built on top of [React Aria Components](ht - **Golden ratio spacing** — All sizing uses φ (1.618) for mathematically harmonious proportions - **Pure CSS tokens** — Design tokens live in `@fried-ui/styles` as plain CSS, usable with any framework - **Dark mode** — Framer-inspired dark theme with 4-layer elevation system -- **LLM-friendly** — Single `variant` prop, inline union types for IDE autocomplete, structured docs for AI consumption +- **Developer-friendly** — Single `variant` prop, inline union types for IDE autocomplete +- **LLM-friendly** — Structured docs and MCP server for AI consumption ## Architecture From e5002d617663cdc0203b6bdb9a8d4e564c16a0bb Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Mon, 30 Mar 2026 02:49:56 +0700 Subject: [PATCH 048/199] feat(icons): Add slot prop to all icon components --- CODE_OF_CONDUCT.md | 2 +- CONTRIBUTING.md | 208 +++++++++++++++--- README.md | 5 +- apps/docs/README.md | 4 +- apps/docs/public/llm/badge.md | 117 ++++++---- apps/docs/public/llm/surface.md | 112 ++++++++++ apps/docs/public/llms.txt | 7 +- apps/docs/src/components/MdxComponents.tsx | 153 +++++++++++++ .../src/content/docs/components/badge.mdx | 122 ++++++++-- .../src/content/docs/components/surface.mdx | 99 +++++++++ apps/docs/src/content/docs/index.mdx | 4 +- apps/docs/src/content/docs/meta.json | 3 +- .../content/docs/overview/introduction.mdx | 4 +- .../src/content/docs/overview/quick-start.mdx | 2 +- .../src/content/docs/styles/design-tokens.mdx | 4 +- .../src/components/icons/ArrowRightIcon.tsx | 2 +- .../src/components/icons/ClipboardIcon.tsx | 2 +- .../react/src/components/icons/GitHubIcon.tsx | 2 +- .../icons/InformationCircleIcon.tsx | 2 +- .../react/src/components/icons/MoreIcon.tsx | 2 +- .../react/src/components/icons/PlusIcon.tsx | 2 +- .../src/components/icons/ReactAriaIcon.tsx | 2 +- .../src/components/icons/SettingsIcon.tsx | 2 +- .../react/src/components/icons/ShareIcon.tsx | 2 +- .../react/src/components/icons/Spinner.tsx | 2 +- .../src/components/icons/StorybookIcon.tsx | 2 +- packages/styles/README.md | 19 +- 27 files changed, 747 insertions(+), 140 deletions(-) create mode 100644 apps/docs/public/llm/surface.md create mode 100644 apps/docs/src/content/docs/components/surface.mdx diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 5da2db8..90954d3 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -22,7 +22,7 @@ Examples of unacceptable behavior: ## Enforcement -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting the maintainers. All complaints will be reviewed and investigated. +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the maintainer at . All complaints will be reviewed and investigated. ## Attribution diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 835bf95..4e81334 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,40 +1,76 @@ -# Contributing to fried-ui +# Contributing to Fried UI -Thanks for your interest in contributing! +Thanks for your interest in contributing to Fried UI! This guide will help you get started. -## Getting Started +## Table of Contents + +- [Tooling](#tooling) +- [Prerequisites](#prerequisites) +- [Development Setup](#development-setup) +- [Commit Convention](#commit-convention) +- [Steps to PR](#steps-to-pr) +- [Pull Request Guidelines](#pull-request-guidelines) +- [Code Style](#code-style) +- [Testing](#testing) +- [Visual Changes](#visual-changes) +- [Documentation](#documentation) +- [Adding a New Component](#adding-a-new-component) +- [Breaking Changes](#breaking-changes) +- [Becoming a Maintainer](#becoming-a-maintainer) +- [License](#license) + +## Tooling + +- [PNPM](https://pnpm.io/) — Package manager (enforced, no npm/yarn) +- [Turborepo](https://turbo.build/) — Monorepo build system +- [Tsup](https://tsup.egoist.dev/) — TypeScript bundler +- [Storybook](https://storybook.js.org/) — Component development and documentation +- [Vitest](https://vitest.dev/) — Unit testing +- [Testing Library](https://testing-library.com/) — Component testing utilities +- [Changesets](https://github.com/changesets/changesets) — Version management + +## Prerequisites + +- Node.js 22+ +- PNPM 9+ + +## Development Setup + +1. Fork and clone the repository ```bash -git clone https://github.com/fried-day/fried-ui.git +git clone https://github.com//fried-ui.git cd fried-ui pnpm install -pnpm dev ``` -## Development +2. Start development -| Command | Description | -| -------------------- | ------------------------------------- | -| `pnpm dev` | Start all apps + packages in dev mode | -| `pnpm build` | Build all packages | -| `pnpm test` | Run tests | -| `pnpm lint` | Lint | -| `pnpm typecheck` | Type check | -| `pnpm format` | Format check | -| `pnpm dev:storybook` | Storybook on port 6006 | -| `pnpm dev:docs` | Docs on port 3001 | +```bash +pnpm dev # All apps + packages +pnpm dev:storybook # Storybook on port 6006 +pnpm dev:docs # Docs on port 3001 +``` -## Pull Requests +3. Build and test -1. Fork the repo and create a branch from `main` -2. Make your changes -3. Add a changeset: `pnpm changeset` -4. Ensure all checks pass: `pnpm lint && pnpm typecheck && pnpm test && pnpm format` -5. Submit your PR +```bash +pnpm build # Build all +pnpm test # Run tests +pnpm lint # Lint all +pnpm typecheck # Type check all +pnpm format # Format check +``` ## Commit Convention -We use [conventional commits](https://www.conventionalcommits.org/). Format: `type(scope): Subject` +We follow [Conventional Commits](https://www.conventionalcommits.org/). Every commit must use this format: + +```txt +type(scope): Subject +``` + +**Types:** | Type | When to use | | ---------- | ---------------------------------------- | @@ -44,20 +80,124 @@ We use [conventional commits](https://www.conventionalcommits.org/). Format: `ty | `docs` | Documentation | | `style` | Formatting, lint fixes | | `test` | Adding or updating tests | +| `build` | Build system or dependencies | +| `ci` | CI configuration | | `chore` | Maintenance | +| `perf` | Performance improvement | +| `revert` | Revert a previous commit | | `setup` | Project setup or configuration | -## Code Quality +**Scope is required.** Subject must be sentence-case, max 50 characters. + +Examples: + +```txt +feat(badge): Add soft variants +fix(button): Fix focus ring on Safari +docs(styles): Update color token docs +``` + +## Steps to PR + +1. Create a branch from `main`: + +```bash +git checkout -b feat/my-feature +``` + +2. Make your changes and commit following the convention above + +3. Add a changeset if your change affects `@fried-ui/react` or `@fried-ui/styles`: + +```bash +pnpm changeset +``` + +4. Ensure all checks pass: + +```bash +pnpm lint && pnpm typecheck && pnpm test && pnpm format +``` + +5. Push and open a PR against `main` + +## Pull Request Guidelines + +- PRs should be focused — one feature or fix per PR +- Include a clear description of what changed and why +- All tests must pass +- All linting must pass +- All types must check +- Add tests for new features or bug fixes + +## Code Style + +- **No `eslint-disable`** — fix the source code +- **No `@ts-nocheck` / `@ts-ignore` / `@ts-expect-error`** — fix the type error +- **No arbitrary Tailwind values** — use built-in classes only +- **All spacing in rem** — never px (except 1px borders) +- **clsx only** — no tailwind-merge, no tv() +- **BEM naming** — `.fri-{component}--{modifier}` +- Run `pnpm format:fix` before committing + +## Testing + +Every component must have tests covering: + +- Renders with default props (correct HTML element, base class only) +- Every variant value +- Every size value +- Every boolean prop +- className merge +- Ref forwarding +- data-slot attribute +- displayName +- Native HTML attributes passthrough + +Interactive components additionally test: onPress, disabled state, render props, className as function. + +## Visual Changes + +When making visual changes, include a screenshot or screen recording in your PR. + +## Documentation + +Keep documentation in sync with code changes. Docs live in `apps/docs/src/content/docs/` using MDX format. + +## Adding a New Component + +New components follow the scaffold-then-customize workflow: + +1. Scaffold with turbo gen: + +```bash +pnpm turbo gen display-component # For Badge, Surface, Divider, etc. +pnpm turbo gen interactive-component # For Button, Link, Switch, etc. +``` + +2. Customize the 6 generated files: + +```txt +packages/styles/src/components/{name}.css +packages/react/src/components/{name}/{Name}.tsx +packages/react/src/components/{name}/{name}.variants.ts +packages/react/src/components/{name}/{name}.test.tsx +packages/react/src/components/{name}/{name}.stories.tsx +packages/react/src/components/{name}/index.ts +``` + +3. Register exports (alphabetical order): + - `packages/react/tsup.config.ts` — add entry + - `packages/react/package.json` — add to exports map + +## Breaking Changes + +Deprecate before removing. Add a deprecation notice in the docs and changelog at least one minor version before removal. + +## Becoming a Maintainer -- No `eslint-disable` comments. Fix the code. -- No `@ts-nocheck` / `@ts-ignore`. Fix the types. -- All components need tests and Storybook stories. -- Run `pnpm format:fix` before committing. +Start by helping with issues, reviewing PRs, and contributing code. Active contributors will be invited to the team. -## Adding a Component +## License -1. Create directory: `packages/react/src/components//` -2. Add files: `.tsx`, `Test.tsx`, `Stories.tsx`, `index.ts` -3. Add export in `packages/react/package.json` -4. Add tsup entry in `packages/react/tsup.config.ts` -5. Add documentation in `apps/docs/content/docs/components/.mdx` +By contributing, you agree that your contributions will be licensed under the [MIT License](LICENSE). diff --git a/README.md b/README.md index 951aeb0..cb2c4fe 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# fried-ui +# Fried UI Beautiful, accessible React components with zero runtime styling. @@ -14,7 +14,8 @@ Beautiful, accessible React components with zero runtime styling. ## Documentation -Visit [fried-ui.com](https://fried-ui.com) for full documentation, component demos, and getting started guide. +- [Documentation](https://fried-ui.vercel.app) +- [Storybook](https://fried-ui-storybook.vercel.app) ## Contributing diff --git a/apps/docs/README.md b/apps/docs/README.md index 15e804a..b31b659 100644 --- a/apps/docs/README.md +++ b/apps/docs/README.md @@ -1,6 +1,6 @@ -# fried-ui docs +# Fried UI Docs -Documentation site for fried-ui, built with [Next.js 16](https://nextjs.org) and [Fumadocs](https://fumadocs.dev). +Documentation site for Fried UI, built with [Next.js 16](https://nextjs.org) and [Fumadocs](https://fumadocs.dev). ## Development diff --git a/apps/docs/public/llm/badge.md b/apps/docs/public/llm/badge.md index 2de3c1d..6db426b 100644 --- a/apps/docs/public/llm/badge.md +++ b/apps/docs/public/llm/badge.md @@ -16,30 +16,46 @@ import { Badge } from "@fried-ui/react"; ## Props -| Prop | Type | Default | Description | -| ----------- | -------------------------------------------------------------------------------------------------- | ----------- | ----------------------------- | -| `variant` | `"primary" \| "secondary" \| "ghost" \| "outline" \| "success" \| "warning" \| "danger" \| "info"` | `"primary"` | Visual variant | -| `size` | `"sm" \| "md" \| "lg"` | `"md"` | Size — controls padding, font | -| `className` | `string` | - | Additional CSS classes | -| `ref` | `Ref` | - | Forwarded ref to span element | -| `children` | `ReactNode` | - | Badge content | +| Prop | Type | Default | Description | +| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------- | ------------------------------------- | +| `variant` | `"primary"` \| `"secondary"` \| `"ghost"` \| `"outline"` \| `"success"` \| `"warning"` \| `"danger"` \| `"info"` \| `"primary-soft"` \| `"secondary-soft"` \| `"success-soft"` \| `"warning-soft"` \| `"danger-soft"` \| `"info-soft"` \| `"primary-outline"` \| `"secondary-outline"` \| `"success-outline"` \| `"warning-outline"` \| `"danger-outline"` \| `"info-outline"` | `"primary"` | Visual variant | +| `size` | `"sm"` \| `"md"` \| `"lg"` | `"md"` | Size — controls padding, font | +| `isIconOnly` | `boolean` | `false` | Square badge for icon-only indicators | +| `isShadow` | `boolean` | `false` | Show colored drop shadow (em-based) | +| `className` | `string` | - | Additional CSS classes | +| `ref` | `Ref` | - | Forwarded ref to span element | +| `children` | `ReactNode` | - | Badge content | ## CSS Classes -| Class | Description | -| ---------------------- | ----------------- | -| `fri-badge` | Base styles | -| `fri-badge--primary` | Primary variant | -| `fri-badge--secondary` | Secondary variant | -| `fri-badge--ghost` | Ghost variant | -| `fri-badge--outline` | Outline variant | -| `fri-badge--success` | Success variant | -| `fri-badge--warning` | Warning variant | -| `fri-badge--danger` | Danger variant | -| `fri-badge--info` | Info variant | -| `fri-badge--sm` | Small size | -| `fri-badge--md` | Medium size | -| `fri-badge--lg` | Large size | +| Class | Description | +| ------------------------------ | ------------------------- | +| `fri-badge` | Base styles | +| `fri-badge--primary` | Primary variant | +| `fri-badge--secondary` | Secondary variant | +| `fri-badge--ghost` | Ghost variant | +| `fri-badge--outline` | Outline variant | +| `fri-badge--success` | Success variant | +| `fri-badge--warning` | Warning variant | +| `fri-badge--danger` | Danger variant | +| `fri-badge--info` | Info variant | +| `fri-badge--primary-soft` | Primary soft variant | +| `fri-badge--secondary-soft` | Secondary soft variant | +| `fri-badge--success-soft` | Success soft variant | +| `fri-badge--warning-soft` | Warning soft variant | +| `fri-badge--danger-soft` | Danger soft variant | +| `fri-badge--info-soft` | Info soft variant | +| `fri-badge--primary-outline` | Primary outline variant | +| `fri-badge--secondary-outline` | Secondary outline variant | +| `fri-badge--success-outline` | Success outline variant | +| `fri-badge--warning-outline` | Warning outline variant | +| `fri-badge--danger-outline` | Danger outline variant | +| `fri-badge--info-outline` | Info outline variant | +| `fri-badge--size-sm` | Small size | +| `fri-badge--size-md` | Medium size | +| `fri-badge--size-lg` | Large size | +| `fri-badge--icon-only` | Icon-only square | +| `fri-badge--shadow` | Colored drop shadow | ## CSS Variables @@ -50,61 +66,69 @@ import { Badge } from "@fried-ui/react"; ## Examples -### Variants - -Semantic and structural variants for different intents. +### Solid Variants ```tsx Primary Secondary -Ghost -Outline Success Warning Danger Info ``` -### Basic +### Soft Variants -Default badge with medium size and pill shape. +Pale background with colored text and border. ```tsx -New +Primary +Success +Danger ``` -### Sizes +### Colored Outline Variants -Three sizes for different contexts. +Transparent background with colored border and text. ```tsx -Small -Medium -Large +Primary +Success +Danger ``` -### Custom Class +### Shadow -Use `className` for layout or typography utilities. +Colored drop shadow that scales with font-size (em-based). ```tsx -VIP +Primary +Success ``` -### Ref Forwarding +### Icon Only -Supports ref forwarding for integration with Tooltip, Popover, etc. +Square badge for icon-only indicators. Always provide `aria-label`. ```tsx -const ref = useRef(null); -Status; + + + ``` -## Anatomy +### Sizes +```tsx +Small +Medium +Large ``` + +## Anatomy + +```txt ┌─ Badge (span) ─────┐ -│ children │ +│ [icon] children │ └─────────────────────┘ ``` @@ -116,8 +140,9 @@ const ref = useRef(null); ## Constraints -- 8 variants: primary, secondary, ghost, outline, success, warning, danger, info -- 3 sizes: sm, md, lg (no xl — badges should stay compact) +- 20 variants: 8 solid + 6 soft + 6 colored outline +- 3 sizes: sm, md, lg - Fixed `rounded-full` border radius (pill shape) - Non-interactive — no hover, focus, or click states -- Uses BEM classes (`fri-badge`, `fri-badge--primary`, etc.) +- BEM classes (`fri-badge`, `fri-badge--primary-soft`, etc.) +- Shadow uses em units — scales with component font-size diff --git a/apps/docs/public/llm/surface.md b/apps/docs/public/llm/surface.md new file mode 100644 index 0000000..e5bca29 --- /dev/null +++ b/apps/docs/public/llm/surface.md @@ -0,0 +1,112 @@ +# Surface + +Visual layer component for cards, panels, and elevated content areas. + +## Install + +```bash +pnpm add @fried-ui/react +``` + +## Import + +```tsx +import { Surface } from "@fried-ui/react"; +``` + +## Props + +| Prop | Type | Default | Description | +| ----------- | ------------------------------------------------------------ | ----------- | ------------------------ | +| `variant` | `"default"` \| `"bordered"` \| `"glass"` \| `"primary"` | `"default"` | The visual variant | +| `radius` | `"none"` \| `"sm"` \| `"md"` \| `"lg"` \| `"xl"` \| `"full"` | `"md"` | Border radius | +| `isShadow` | `boolean` | `false` | Show drop shadow | +| `className` | `string` | - | Additional CSS classes | +| `ref` | `Ref` | - | Forwarded ref to the div | +| `children` | `ReactNode` | - | Surface content | + +## CSS Classes + +| Class | Description | +| -------------------------- | -------------------- | +| `fri-surface` | Base styles | +| `fri-surface--default` | Default variant | +| `fri-surface--bordered` | Bordered variant | +| `fri-surface--glass` | Glass variant | +| `fri-surface--primary` | Primary variant | +| `fri-surface--shadow` | Drop shadow | +| `fri-surface--radius-none` | No border radius | +| `fri-surface--radius-sm` | Small border radius | +| `fri-surface--radius-md` | Medium border radius | +| `fri-surface--radius-lg` | Large border radius | +| `fri-surface--radius-xl` | Extra large radius | +| `fri-surface--radius-full` | Full (pill) radius | + +## Examples + +### Basic Card + +```tsx + +

Card Title

+

Content goes here.

+
+``` + +### Variants + +```tsx +Default +Bordered +Glass +Primary +``` + +### With Shadow + +```tsx + + Elevated card + +``` + +### Radius + +```tsx +Sharp +Rounded +Pill +``` + +### Glass Over Image + +```tsx +
+ + + Content over image + +
+``` + +## Anatomy + +```txt +┌─ Surface (div) ────┐ +│ children │ +└─────────────────────┘ +``` + +## Accessibility + +- Surface is a non-interactive `
` element +- Use semantic elements inside (headings, paragraphs, lists) +- Add `role` and `aria-label` if the surface represents a landmark + +## Constraints + +- 4 variants: default, bordered, glass, primary +- 6 radius values: none, sm, md, lg, xl, full +- Non-interactive — no hover, focus, or click states +- Surface provides no padding — use `className="p-6"` etc. +- BEM classes (`fri-surface`, `fri-surface--bordered`, etc.) diff --git a/apps/docs/public/llms.txt b/apps/docs/public/llms.txt index 5266d01..29b9b72 100644 --- a/apps/docs/public/llms.txt +++ b/apps/docs/public/llms.txt @@ -1,10 +1,11 @@ -# fried-ui +# Fried UI -> Beautiful, accessible React components built on React Aria Components + Tailwind CSS v4. +> Beautiful, accessible React components with zero runtime styling. ## Docs -- [Badge](/llm/badge.md): Display-only label for status indicators, counts, and categories +- [Badge](/llm/badge.md): Display-only label with 20 variants (solid, soft, colored outline) - [Button](/llm/button.md): Accessible button with golden ratio spacing, React Aria keyboard/focus/screen reader support +- [Surface](/llm/surface.md): Visual layer for cards and panels with bordered, glass, primary variants ## Optional diff --git a/apps/docs/src/components/MdxComponents.tsx b/apps/docs/src/components/MdxComponents.tsx index 6b110ee..cc5fa86 100644 --- a/apps/docs/src/components/MdxComponents.tsx +++ b/apps/docs/src/components/MdxComponents.tsx @@ -5,12 +5,14 @@ import { Badge, Button, GitHubIcon, + InformationCircleIcon, MoreIcon, PlusIcon, ReactAriaIcon, SettingsIcon, ShareIcon, StorybookIcon, + Surface, } from "@fried-ui/react"; import { BadgeLinks } from "./BadgeLinks"; @@ -56,6 +58,62 @@ const BadgeVariants = (): React.JSX.Element => { ); }; +const BadgeSoftVariants = (): React.JSX.Element => { + return ( +
+ Primary + Secondary + Success + Warning + Danger + Info +
+ ); +}; + +const BadgeOutlineVariants = (): React.JSX.Element => { + return ( +
+ Primary + Secondary + Success + Warning + Danger + Info +
+ ); +}; + +const BadgeShadow = (): React.JSX.Element => { + return ( +
+ Primary + + Success + + + Danger + +
+ ); +}; + +const BadgeIconOnly = (): React.JSX.Element => { + return ( +
+ + + + + + + + + +
+ ); +}; + const BadgeSizes = (): React.JSX.Element => { return (
@@ -215,10 +273,100 @@ const ButtonAsLink = (): React.JSX.Element => { ); }; +const surfaceBadgeLinksData = [ + { + href: "https://fried-ui-storybook.vercel.app/?path=/story/components-surface--default", + icon: , + label: "Storybook", + }, + { + href: "https://github.com/fried-day/fried-ui/blob/main/packages/react/src/components/surface/Surface.tsx", + icon: , + label: "Source", + }, + { + href: "https://github.com/fried-day/fried-ui/blob/main/packages/styles/src/components/surface.css", + icon: , + label: "Styles source", + }, +]; + +const SurfaceBadgeLinks = (): React.JSX.Element => { + return ; +}; + +const SurfaceDemo = (): React.JSX.Element => { + return ( + +

Card Title

+

Content goes here.

+
+ ); +}; + +const SurfaceVariants = (): React.JSX.Element => { + return ( +
+ + Default + + + Bordered + + + Glass + + + Primary + +
+ ); +}; + +const SurfaceRadii = (): React.JSX.Element => { + return ( +
+ + none + + + sm + + + md + + + lg + + + xl + + + full + +
+ ); +}; + +const SurfaceShadow = (): React.JSX.Element => { + return ( +
+ No shadow + + With shadow + +
+ ); +}; + export { BadgeBadgeLinks, BadgeDemo, + BadgeIconOnly, + BadgeOutlineVariants, + BadgeShadow, BadgeSizes, + BadgeSoftVariants, BadgeVariants, ButtonAsLink, ButtonBadgeLinks, @@ -232,4 +380,9 @@ export { ButtonSizes, ButtonVariants, ButtonWithIcons, + SurfaceBadgeLinks, + SurfaceDemo, + SurfaceRadii, + SurfaceShadow, + SurfaceVariants, }; diff --git a/apps/docs/src/content/docs/components/badge.mdx b/apps/docs/src/content/docs/components/badge.mdx index 578e84a..9fbc0a0 100644 --- a/apps/docs/src/content/docs/components/badge.mdx +++ b/apps/docs/src/content/docs/components/badge.mdx @@ -25,6 +25,8 @@ import { Badge } from "@fried-ui/react"; The `variant` prop controls the visual style using semantic color tokens. +### Solid + @@ -40,6 +42,68 @@ The `variant` prop controls the visual style using semantic color tokens. Info ``` +### Soft + +Pale background with colored text and border. Uses the `{color}-soft` naming. + + + + + +```tsx +Primary +Secondary +Success +Warning +Danger +Info +``` + +### Colored Outline + +Transparent background with colored border and text. Uses the `{color}-outline` naming. + + + + + +```tsx +Primary +Secondary +Success +Warning +Danger +Info +``` + +## Shadow + +The `isShadow` prop adds a colored drop shadow that scales with font-size (em-based). + + + + + +```tsx +Primary +Success +Danger +``` + +## Icon Only + +Square badge for icon-only indicators. Always provide `aria-label`. + + + + + +```tsx + + + +``` + ## Sizes The `size` prop controls padding, gap, and font-size. @@ -58,30 +122,46 @@ The `size` prop controls padding, gap, and font-size. ### Badge Props -| Prop | Type | Default | Description | -| ----------- | -------------------------------------------------------------------------------------------------- | ----------- | ------------------------------- | -| `variant` | `"primary" \| "secondary" \| "ghost" \| "outline" \| "success" \| "warning" \| "danger" \| "info"` | `"primary"` | The visual variant of the badge | -| `size` | `"sm" \| "md" \| "lg"` | `"md"` | The size of the badge | -| `className` | `string` | - | Additional CSS classes | -| `ref` | `Ref` | - | Forwarded ref to the span | -| `children` | `ReactNode` | - | Badge content | +| Prop | Type | Default | Description | +| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------- | ------------------------------------- | +| `variant` | `"primary"` \| `"secondary"` \| `"ghost"` \| `"outline"` \| `"success"` \| `"warning"` \| `"danger"` \| `"info"` \| `"primary-soft"` \| `"secondary-soft"` \| `"success-soft"` \| `"warning-soft"` \| `"danger-soft"` \| `"info-soft"` \| `"primary-outline"` \| `"secondary-outline"` \| `"success-outline"` \| `"warning-outline"` \| `"danger-outline"` \| `"info-outline"` | `"primary"` | The visual variant of the badge | +| `size` | `"sm"` \| `"md"` \| `"lg"` | `"md"` | The size of the badge | +| `isIconOnly` | `boolean` | `false` | Square badge for icon-only indicators | +| `isShadow` | `boolean` | `false` | Show colored drop shadow (em-based) | +| `className` | `string` | - | Additional CSS classes | +| `ref` | `Ref` | - | Forwarded ref to the span | +| `children` | `ReactNode` | - | Badge content | ### CSS Classes -| Class | Description | -| ---------------------- | ----------------- | -| `fri-badge` | Base styles | -| `fri-badge--primary` | Primary variant | -| `fri-badge--secondary` | Secondary variant | -| `fri-badge--ghost` | Ghost variant | -| `fri-badge--outline` | Outline variant | -| `fri-badge--success` | Success variant | -| `fri-badge--warning` | Warning variant | -| `fri-badge--danger` | Danger variant | -| `fri-badge--info` | Info variant | -| `fri-badge--sm` | Small size | -| `fri-badge--md` | Medium size | -| `fri-badge--lg` | Large size | +| Class | Description | +| ------------------------------ | ------------------------- | +| `fri-badge` | Base styles | +| `fri-badge--primary` | Primary variant | +| `fri-badge--secondary` | Secondary variant | +| `fri-badge--ghost` | Ghost variant | +| `fri-badge--outline` | Outline variant | +| `fri-badge--success` | Success variant | +| `fri-badge--warning` | Warning variant | +| `fri-badge--danger` | Danger variant | +| `fri-badge--info` | Info variant | +| `fri-badge--primary-soft` | Primary soft variant | +| `fri-badge--secondary-soft` | Secondary soft variant | +| `fri-badge--success-soft` | Success soft variant | +| `fri-badge--warning-soft` | Warning soft variant | +| `fri-badge--danger-soft` | Danger soft variant | +| `fri-badge--info-soft` | Info soft variant | +| `fri-badge--primary-outline` | Primary outline variant | +| `fri-badge--secondary-outline` | Secondary outline variant | +| `fri-badge--success-outline` | Success outline variant | +| `fri-badge--warning-outline` | Warning outline variant | +| `fri-badge--danger-outline` | Danger outline variant | +| `fri-badge--info-outline` | Info outline variant | +| `fri-badge--size-sm` | Small size | +| `fri-badge--size-md` | Medium size | +| `fri-badge--size-lg` | Large size | +| `fri-badge--icon-only` | Icon-only square | +| `fri-badge--shadow` | Colored drop shadow | ### CSS Variables diff --git a/apps/docs/src/content/docs/components/surface.mdx b/apps/docs/src/content/docs/components/surface.mdx new file mode 100644 index 0000000..11e6fb8 --- /dev/null +++ b/apps/docs/src/content/docs/components/surface.mdx @@ -0,0 +1,99 @@ +--- +title: Surface +description: Visual layer component for cards, panels, and elevated content areas. +--- + + + +## Import + +```tsx +import { Surface } from "@fried-ui/react"; +``` + +## Usage + + + + + +```tsx + +

Card Title

+

Content goes here.

+
+``` + +## Variants + +The `variant` prop controls the visual style of the surface. + + + + + +```tsx +Default +Bordered +Glass +Primary +``` + +## Radius + +The `radius` prop controls border-radius. + + + + + +```tsx +none +sm +md +lg +xl +full +``` + +## Shadow + +The `isShadow` prop adds a drop shadow. + + + + + +```tsx +With shadow +``` + +## API Reference + +### Surface Props + +| Prop | Type | Default | Description | +| ----------- | ------------------------------------------------------------ | ----------- | ------------------------ | +| `variant` | `"default"` \| `"bordered"` \| `"glass"` \| `"primary"` | `"default"` | The visual variant | +| `radius` | `"none"` \| `"sm"` \| `"md"` \| `"lg"` \| `"xl"` \| `"full"` | `"md"` | Border radius | +| `isShadow` | `boolean` | `false` | Show drop shadow | +| `className` | `string` | - | Additional CSS classes | +| `ref` | `Ref` | - | Forwarded ref to the div | +| `children` | `ReactNode` | - | Surface content | + +### CSS Classes + +| Class | Description | +| -------------------------- | -------------------- | +| `fri-surface` | Base styles | +| `fri-surface--default` | Default variant | +| `fri-surface--bordered` | Bordered variant | +| `fri-surface--glass` | Glass variant | +| `fri-surface--primary` | Primary variant | +| `fri-surface--shadow` | Drop shadow | +| `fri-surface--radius-none` | No border radius | +| `fri-surface--radius-sm` | Small border radius | +| `fri-surface--radius-md` | Medium border radius | +| `fri-surface--radius-lg` | Large border radius | +| `fri-surface--radius-xl` | Extra large radius | +| `fri-surface--radius-full` | Full (pill) radius | diff --git a/apps/docs/src/content/docs/index.mdx b/apps/docs/src/content/docs/index.mdx index 7c98cc1..c2522e7 100644 --- a/apps/docs/src/content/docs/index.mdx +++ b/apps/docs/src/content/docs/index.mdx @@ -9,10 +9,12 @@ Fried UI is a production-ready React component library with accessible primitive ## Get Started -- [Introduction](/docs/overview/introduction) — What is fried-ui and why use it +- [Introduction](/docs/overview/introduction) — What is Fried UI and why use it - [Quick Start](/docs/overview/quick-start) — Install and render your first component - [Design Tokens](/docs/overview/design-tokens) — Color, spacing, elevation, and motion tokens ## Components +- [Badge](/docs/components/badge) — 20 variants: solid, soft, colored outline - [Button](/docs/components/button) — 8 variants, icon-only, render props, ref forwarding +- [Surface](/docs/components/surface) — Visual layer with bordered, glass, primary variants diff --git a/apps/docs/src/content/docs/meta.json b/apps/docs/src/content/docs/meta.json index 96fd520..8eb814d 100644 --- a/apps/docs/src/content/docs/meta.json +++ b/apps/docs/src/content/docs/meta.json @@ -10,6 +10,7 @@ "styles/colors", "---Components---", "components/badge", - "components/button" + "components/button", + "components/surface" ] } diff --git a/apps/docs/src/content/docs/overview/introduction.mdx b/apps/docs/src/content/docs/overview/introduction.mdx index 824008b..966e037 100644 --- a/apps/docs/src/content/docs/overview/introduction.mdx +++ b/apps/docs/src/content/docs/overview/introduction.mdx @@ -3,9 +3,9 @@ title: Introduction description: Beautiful, accessible React components built on React Aria + Tailwind CSS v4. --- -## What is fried-ui? +## What is Fried UI? -fried-ui is a React component library built on top of [React Aria Components](https://react-spectrum.adobe.com/react-aria/) and [Tailwind CSS v4](https://tailwindcss.com/). It provides accessible, composable UI primitives with a design system powered by the golden ratio. +Fried UI is a React component library built on top of [React Aria Components](https://react-spectrum.adobe.com/react-aria/) and [Tailwind CSS v4](https://tailwindcss.com/). It provides accessible, composable UI primitives with a design system powered by the golden ratio. ## Key Features diff --git a/apps/docs/src/content/docs/overview/quick-start.mdx b/apps/docs/src/content/docs/overview/quick-start.mdx index bd4405a..6c78136 100644 --- a/apps/docs/src/content/docs/overview/quick-start.mdx +++ b/apps/docs/src/content/docs/overview/quick-start.mdx @@ -1,6 +1,6 @@ --- title: Quick Start -description: Install fried-ui and render your first component in under a minute. +description: Install Fried UI and render your first component in under a minute. --- ## Installation diff --git a/apps/docs/src/content/docs/styles/design-tokens.mdx b/apps/docs/src/content/docs/styles/design-tokens.mdx index 0260249..8cc24e1 100644 --- a/apps/docs/src/content/docs/styles/design-tokens.mdx +++ b/apps/docs/src/content/docs/styles/design-tokens.mdx @@ -1,11 +1,11 @@ --- title: Design Tokens -description: Color, spacing, elevation, motion, and layout tokens that power every fried-ui component. +description: Color, spacing, elevation, motion, and layout tokens that power every Fried UI component. --- ## Overview -fried-ui uses CSS custom properties as design tokens. All tokens are defined in `@fried-ui/styles` and automatically available as Tailwind CSS v4 utilities. +Fried UI uses CSS custom properties as design tokens. All tokens are defined in `@fried-ui/styles` and automatically available as Tailwind CSS v4 utilities. ```css @import "tailwindcss"; diff --git a/packages/react/src/components/icons/ArrowRightIcon.tsx b/packages/react/src/components/icons/ArrowRightIcon.tsx index 4f01907..959e40c 100644 --- a/packages/react/src/components/icons/ArrowRightIcon.tsx +++ b/packages/react/src/components/icons/ArrowRightIcon.tsx @@ -2,7 +2,7 @@ import type { ComponentPropsWithRef } from "react"; -const ArrowRightIcon = (props: Readonly>) => { +const ArrowRightIcon = (props: Readonly & { slot?: string }>) => { const { className, ref, ...rest } = props; return ( diff --git a/packages/react/src/components/icons/ClipboardIcon.tsx b/packages/react/src/components/icons/ClipboardIcon.tsx index f5e4ce8..56aaf5a 100644 --- a/packages/react/src/components/icons/ClipboardIcon.tsx +++ b/packages/react/src/components/icons/ClipboardIcon.tsx @@ -2,7 +2,7 @@ import type { ComponentPropsWithRef } from "react"; -const ClipboardIcon = (props: Readonly>) => { +const ClipboardIcon = (props: Readonly & { slot?: string }>) => { const { className, ref, ...rest } = props; return ( diff --git a/packages/react/src/components/icons/GitHubIcon.tsx b/packages/react/src/components/icons/GitHubIcon.tsx index e2a8173..af9bc27 100644 --- a/packages/react/src/components/icons/GitHubIcon.tsx +++ b/packages/react/src/components/icons/GitHubIcon.tsx @@ -2,7 +2,7 @@ import type { ComponentPropsWithRef } from "react"; -const GitHubIcon = (props: Readonly>) => { +const GitHubIcon = (props: Readonly & { slot?: string }>) => { const { className, ref, ...rest } = props; return ( diff --git a/packages/react/src/components/icons/InformationCircleIcon.tsx b/packages/react/src/components/icons/InformationCircleIcon.tsx index 0a93ba8..6a91315 100644 --- a/packages/react/src/components/icons/InformationCircleIcon.tsx +++ b/packages/react/src/components/icons/InformationCircleIcon.tsx @@ -2,7 +2,7 @@ import type { ComponentPropsWithRef } from "react"; -const InformationCircleIcon = (props: Readonly>) => { +const InformationCircleIcon = (props: Readonly & { slot?: string }>) => { const { className, ref, ...rest } = props; return ( diff --git a/packages/react/src/components/icons/MoreIcon.tsx b/packages/react/src/components/icons/MoreIcon.tsx index 962617a..293d706 100644 --- a/packages/react/src/components/icons/MoreIcon.tsx +++ b/packages/react/src/components/icons/MoreIcon.tsx @@ -2,7 +2,7 @@ import type { ComponentPropsWithRef } from "react"; -const MoreIcon = (props: Readonly>) => { +const MoreIcon = (props: Readonly & { slot?: string }>) => { const { className, ref, ...rest } = props; return ( diff --git a/packages/react/src/components/icons/PlusIcon.tsx b/packages/react/src/components/icons/PlusIcon.tsx index b535918..47be65b 100644 --- a/packages/react/src/components/icons/PlusIcon.tsx +++ b/packages/react/src/components/icons/PlusIcon.tsx @@ -2,7 +2,7 @@ import type { ComponentPropsWithRef } from "react"; -const PlusIcon = (props: Readonly>) => { +const PlusIcon = (props: Readonly & { slot?: string }>) => { const { className, ref, ...rest } = props; return ( diff --git a/packages/react/src/components/icons/ReactAriaIcon.tsx b/packages/react/src/components/icons/ReactAriaIcon.tsx index e7940da..d74c852 100644 --- a/packages/react/src/components/icons/ReactAriaIcon.tsx +++ b/packages/react/src/components/icons/ReactAriaIcon.tsx @@ -2,7 +2,7 @@ import type { ComponentPropsWithRef } from "react"; -const ReactAriaIcon = (props: Readonly>) => { +const ReactAriaIcon = (props: Readonly & { slot?: string }>) => { const { className, ref, ...rest } = props; return ( diff --git a/packages/react/src/components/icons/SettingsIcon.tsx b/packages/react/src/components/icons/SettingsIcon.tsx index 7370d70..4db0f14 100644 --- a/packages/react/src/components/icons/SettingsIcon.tsx +++ b/packages/react/src/components/icons/SettingsIcon.tsx @@ -2,7 +2,7 @@ import type { ComponentPropsWithRef } from "react"; -const SettingsIcon = (props: Readonly>) => { +const SettingsIcon = (props: Readonly & { slot?: string }>) => { const { className, ref, ...rest } = props; return ( diff --git a/packages/react/src/components/icons/ShareIcon.tsx b/packages/react/src/components/icons/ShareIcon.tsx index 2311b0f..394e836 100644 --- a/packages/react/src/components/icons/ShareIcon.tsx +++ b/packages/react/src/components/icons/ShareIcon.tsx @@ -2,7 +2,7 @@ import type { ComponentPropsWithRef } from "react"; -const ShareIcon = (props: Readonly>) => { +const ShareIcon = (props: Readonly & { slot?: string }>) => { const { className, ref, ...rest } = props; return ( diff --git a/packages/react/src/components/icons/Spinner.tsx b/packages/react/src/components/icons/Spinner.tsx index 3514a89..e6bc575 100644 --- a/packages/react/src/components/icons/Spinner.tsx +++ b/packages/react/src/components/icons/Spinner.tsx @@ -2,7 +2,7 @@ import type { ComponentPropsWithRef } from "react"; -const Spinner = (props: Readonly>) => { +const Spinner = (props: Readonly & { slot?: string }>) => { const { className, ref, ...rest } = props; return ( diff --git a/packages/react/src/components/icons/StorybookIcon.tsx b/packages/react/src/components/icons/StorybookIcon.tsx index 445edbb..ccd9c6d 100644 --- a/packages/react/src/components/icons/StorybookIcon.tsx +++ b/packages/react/src/components/icons/StorybookIcon.tsx @@ -2,7 +2,7 @@ import type { ComponentPropsWithRef } from "react"; -const StorybookIcon = (props: Readonly>) => { +const StorybookIcon = (props: Readonly & { slot?: string }>) => { const { className, ref, ...rest } = props; return ( diff --git a/packages/styles/README.md b/packages/styles/README.md index 6cfb895..0354237 100644 --- a/packages/styles/README.md +++ b/packages/styles/README.md @@ -1,6 +1,8 @@ # @fried-ui/styles -Base styles and variant utilities for the fried-ui component library. Built on [Tailwind CSS v4](https://tailwindcss.com/) and [tailwind-variants](https://www.tailwind-variants.org/). +Pure CSS design tokens and component styles for Fried UI. Built on [Tailwind CSS v4](https://tailwindcss.com/). + +Zero JavaScript dependency — works with React, Vue, Svelte, or plain HTML. ## Installation @@ -12,25 +14,16 @@ Peer dependency: `tailwindcss >=4.0.0` ## Usage -Import base styles in your CSS entry file: +Import styles in your CSS entry file: ```css @import "tailwindcss"; @import "@fried-ui/styles"; ``` -## Exports - -| Path | Description | -| ----------------------------- | ------------------------------------------------------------------------------------- | -| `@fried-ui/styles` | Base CSS (Tailwind + animations) | -| `@fried-ui/styles/variants` | `tv`, `cn`, `VariantProps` from tailwind-variants | -| `@fried-ui/styles/utils` | Shared utility classes (`focusRingClasses`, `disabledClasses`, `ariaDisabledClasses`) | -| `@fried-ui/styles/components` | Component variant definitions | - ## Development ```bash -pnpm dev # Watch mode (tsup) -pnpm build # Build (tsup, ESM-only) +pnpm dev # Watch mode +pnpm build # Build ``` From 733930a6cec27afec39c2c558a13bedb2868ddba Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Mon, 30 Mar 2026 02:50:10 +0700 Subject: [PATCH 049/199] docs(docs): Register new MDX components for badge and surface --- apps/docs/src/app/docs/[[...slug]]/page.tsx | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/apps/docs/src/app/docs/[[...slug]]/page.tsx b/apps/docs/src/app/docs/[[...slug]]/page.tsx index 4d6d304..5edfdfd 100644 --- a/apps/docs/src/app/docs/[[...slug]]/page.tsx +++ b/apps/docs/src/app/docs/[[...slug]]/page.tsx @@ -7,7 +7,11 @@ import { CopyMarkdown } from "@/components/CopyMarkdown"; import { BadgeBadgeLinks, BadgeDemo, + BadgeIconOnly, + BadgeOutlineVariants, + BadgeShadow, BadgeSizes, + BadgeSoftVariants, BadgeVariants, ButtonAsLink, ButtonBadgeLinks, @@ -21,6 +25,11 @@ import { ButtonSizes, ButtonVariants, ButtonWithIcons, + SurfaceBadgeLinks, + SurfaceDemo, + SurfaceRadii, + SurfaceShadow, + SurfaceVariants, } from "@/components/MdxComponents"; import { Preview } from "@/components/Preview"; import { source } from "@/lib/source"; @@ -37,7 +46,11 @@ const mdxComponents = { ...defaultMdxComponents, BadgeBadgeLinks, BadgeDemo, + BadgeIconOnly, + BadgeOutlineVariants, + BadgeShadow, BadgeSizes, + BadgeSoftVariants, BadgeVariants, ButtonAsLink, ButtonBadgeLinks, @@ -53,6 +66,11 @@ const mdxComponents = { ButtonWithIcons, ColorSwatch, Preview, + SurfaceBadgeLinks, + SurfaceDemo, + SurfaceRadii, + SurfaceShadow, + SurfaceVariants, }; const Page = async (props: Readonly): Promise => { From cf94b0954ae6f527e3491bde994d72d9f415ce0c Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Mon, 30 Mar 2026 02:53:42 +0700 Subject: [PATCH 050/199] fix(docs): Add empty lines between JSX siblings for lint --- apps/docs/src/components/MdxComponents.tsx | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/apps/docs/src/components/MdxComponents.tsx b/apps/docs/src/components/MdxComponents.tsx index cc5fa86..32a44c4 100644 --- a/apps/docs/src/components/MdxComponents.tsx +++ b/apps/docs/src/components/MdxComponents.tsx @@ -88,9 +88,11 @@ const BadgeShadow = (): React.JSX.Element => { return (
Primary + Success + Danger @@ -104,9 +106,11 @@ const BadgeIconOnly = (): React.JSX.Element => { + + @@ -310,12 +314,15 @@ const SurfaceVariants = (): React.JSX.Element => { Default + Bordered + Glass + Primary @@ -329,18 +336,23 @@ const SurfaceRadii = (): React.JSX.Element => { none + sm + md + lg + xl + full @@ -352,6 +364,7 @@ const SurfaceShadow = (): React.JSX.Element => { return (
No shadow + With shadow From d99755b5bc02c007c29331ddde3ffc7215d46a45 Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Mon, 30 Mar 2026 02:55:52 +0700 Subject: [PATCH 051/199] fix(husky): Add execute permissions to git hooks --- .husky/commit-msg | 0 .husky/pre-commit | 0 .husky/pre-push | 0 3 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 .husky/commit-msg mode change 100644 => 100755 .husky/pre-commit mode change 100644 => 100755 .husky/pre-push diff --git a/.husky/commit-msg b/.husky/commit-msg old mode 100644 new mode 100755 diff --git a/.husky/pre-commit b/.husky/pre-commit old mode 100644 new mode 100755 diff --git a/.husky/pre-push b/.husky/pre-push old mode 100644 new mode 100755 From ff27915e6695c43a871dbf1fccca202242341c71 Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Mon, 30 Mar 2026 03:08:00 +0700 Subject: [PATCH 052/199] fix(ci): Exclude CSS from SonarCloud duplication check --- pnpm-lock.yaml | 182 ++++++++++++++++++++++----------------- sonar-project.properties | 3 + 2 files changed, 105 insertions(+), 80 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aa816d1..4fd7474 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -74,16 +74,16 @@ importers: version: 2.0.13 fumadocs-core: specifier: ^16.7.5 - version: 16.7.5(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.2)(lucide-react@0.577.0(react@19.2.0))(next@16.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(zod@4.3.6) + version: 16.7.5(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.2)(lucide-react@0.577.0(react@19.2.0))(next@16.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(sass@1.98.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(zod@4.3.6) fumadocs-mdx: specifier: ^14.2.11 - version: 14.2.11(@types/mdast@4.0.4)(@types/mdx@2.0.13)(@types/react@19.2.2)(fumadocs-core@16.7.5(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.2)(lucide-react@0.577.0(react@19.2.0))(next@16.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(zod@4.3.6))(next@16.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 14.2.11(@types/mdast@4.0.4)(@types/mdx@2.0.13)(@types/react@19.2.2)(fumadocs-core@16.7.5(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.2)(lucide-react@0.577.0(react@19.2.0))(next@16.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(sass@1.98.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(zod@4.3.6))(next@16.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(sass@1.98.0))(react@19.2.0)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3)) fumadocs-ui: specifier: ^16.7.5 - version: 16.7.5(@types/mdx@2.0.13)(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(fumadocs-core@16.7.5(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.2)(lucide-react@0.577.0(react@19.2.0))(next@16.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(zod@4.3.6))(next@16.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(shiki@4.0.2)(tailwindcss@4.2.2) + version: 16.7.5(@types/mdx@2.0.13)(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(fumadocs-core@16.7.5(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.2)(lucide-react@0.577.0(react@19.2.0))(next@16.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(sass@1.98.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(zod@4.3.6))(next@16.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(sass@1.98.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(shiki@4.0.2)(tailwindcss@4.2.2) next: specifier: 16.2.0 - version: 16.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 16.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(sass@1.98.0) react: specifier: ^19.2.0 version: 19.2.0 @@ -139,25 +139,25 @@ importers: version: link:../../packages/quality '@storybook/addon-docs': specifier: ^10.3.3 - version: 10.3.3(@types/react@19.2.2)(esbuild@0.27.4)(rollup@4.60.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 10.3.3(@types/react@19.2.2)(esbuild@0.27.4)(rollup@4.60.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3)) '@storybook/addon-vitest': specifier: ^10.3.3 - version: 10.3.3(@vitest/browser-playwright@4.1.2(playwright@1.58.2)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2))(@vitest/browser@4.1.2(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))))(@vitest/runner@4.1.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vitest@4.1.2(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))) + version: 10.3.3(@vitest/browser-playwright@4.1.2(playwright@1.58.2)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2))(@vitest/browser@4.1.2(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3))))(@vitest/runner@4.1.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vitest@4.1.2(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3))) '@storybook/react': specifier: ^10.3.3 version: 10.3.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(typescript@5.9.2) '@storybook/react-vite': specifier: ^10.3.3 - version: 10.3.3(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(rollup@4.60.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(typescript@5.9.2)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 10.3.3(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(rollup@4.60.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(typescript@5.9.2)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3)) '@tailwindcss/vite': specifier: ^4.1.0 - version: 4.2.2(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.2.2(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3)) '@vitest/browser-playwright': specifier: ^4.1.2 - version: 4.1.2(playwright@1.58.2)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) + version: 4.1.2(playwright@1.58.2)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) '@vitest/coverage-v8': specifier: ^4.1.2 - version: 4.1.2(@vitest/browser@4.1.2(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))))(vitest@4.1.2(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))) + version: 4.1.2(@vitest/browser@4.1.2(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3))))(vitest@4.1.2(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3))) eslint: specifier: ^9.39.1 version: 9.39.1(jiti@2.6.1) @@ -172,10 +172,10 @@ importers: version: 4.2.2 vite: specifier: ^8.0.2 - version: 8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) + version: 8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: ^4.1.2 - version: 4.1.2(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.2(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3)) packages/mcp: dependencies: @@ -282,7 +282,7 @@ importers: version: 19.2.2(@types/react@19.2.2) '@vitejs/plugin-react': specifier: ^6.0.1 - version: 6.0.1(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 6.0.1(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3)) eslint: specifier: ^9.39.1 version: 9.39.1(jiti@2.6.1) @@ -306,7 +306,7 @@ importers: version: 5.9.2 vitest: specifier: ^4.1.1 - version: 4.1.1(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.1(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3)) packages/styles: dependencies: @@ -343,10 +343,10 @@ importers: version: 14.6.1(@testing-library/dom@10.4.1) '@vitejs/plugin-react': specifier: ^6.0.1 - version: 6.0.1(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 6.0.1(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3)) '@vitest/coverage-istanbul': specifier: ^4.1.1 - version: 4.1.1(vitest@4.1.1(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))) + version: 4.1.1(vitest@4.1.1(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3))) glob: specifier: ^11.0.2 version: 11.1.0 @@ -361,7 +361,7 @@ importers: version: 4.21.0 vitest: specifier: ^4.1.1 - version: 4.1.1(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.1(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3)) packages: @@ -4312,6 +4312,9 @@ packages: engines: {node: '>=16.x'} hasBin: true + immutable@5.1.5: + resolution: {integrity: sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -5736,6 +5739,11 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + sass@1.98.0: + resolution: {integrity: sha512-+4N/u9dZ4PrgzGgPlKnaaRQx64RO0JBKs9sDhQ2pLgN6JQZ25uPQZKQYaBJU48Kd5BxgXoJ4e09Dq7nMcOUW3A==} + engines: {node: '>=14.0.0'} + hasBin: true + saxes@6.0.0: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} @@ -7346,11 +7354,11 @@ snapshots: '@istanbuljs/schema@0.1.3': {} - '@joshwooding/vite-plugin-react-docgen-typescript@0.6.4(typescript@5.9.2)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))': + '@joshwooding/vite-plugin-react-docgen-typescript@0.6.4(typescript@5.9.2)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: glob: 13.0.6 react-docgen-typescript: 2.4.0(typescript@5.9.2) - vite: 8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) + vite: 8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3) optionalDependencies: typescript: 5.9.2 @@ -9159,10 +9167,10 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@storybook/addon-docs@10.3.3(@types/react@19.2.2)(esbuild@0.27.4)(rollup@4.60.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))': + '@storybook/addon-docs@10.3.3(@types/react@19.2.2)(esbuild@0.27.4)(rollup@4.60.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@mdx-js/react': 3.1.1(@types/react@19.2.2)(react@19.2.0) - '@storybook/csf-plugin': 10.3.3(esbuild@0.27.4)(rollup@4.60.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) + '@storybook/csf-plugin': 10.3.3(esbuild@0.27.4)(rollup@4.60.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3)) '@storybook/icons': 2.0.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@storybook/react-dom-shim': 10.3.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)) react: 19.2.0 @@ -9176,39 +9184,39 @@ snapshots: - vite - webpack - '@storybook/addon-vitest@10.3.3(@vitest/browser-playwright@4.1.2(playwright@1.58.2)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2))(@vitest/browser@4.1.2(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))))(@vitest/runner@4.1.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vitest@4.1.2(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)))': + '@storybook/addon-vitest@10.3.3(@vitest/browser-playwright@4.1.2(playwright@1.58.2)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2))(@vitest/browser@4.1.2(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3))))(@vitest/runner@4.1.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vitest@4.1.2(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3)))': dependencies: '@storybook/global': 5.0.0 '@storybook/icons': 2.0.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) storybook: 10.3.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) optionalDependencies: - '@vitest/browser': 4.1.2(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))) - '@vitest/browser-playwright': 4.1.2(playwright@1.58.2)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) + '@vitest/browser': 4.1.2(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3))) + '@vitest/browser-playwright': 4.1.2(playwright@1.58.2)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) '@vitest/runner': 4.1.2 - vitest: 4.1.2(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) + vitest: 4.1.2(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3)) transitivePeerDependencies: - react - react-dom - '@storybook/builder-vite@10.3.3(esbuild@0.27.4)(rollup@4.60.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))': + '@storybook/builder-vite@10.3.3(esbuild@0.27.4)(rollup@4.60.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: - '@storybook/csf-plugin': 10.3.3(esbuild@0.27.4)(rollup@4.60.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) + '@storybook/csf-plugin': 10.3.3(esbuild@0.27.4)(rollup@4.60.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3)) storybook: 10.3.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) ts-dedent: 2.2.0 - vite: 8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) + vite: 8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - esbuild - rollup - webpack - '@storybook/csf-plugin@10.3.3(esbuild@0.27.4)(rollup@4.60.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))': + '@storybook/csf-plugin@10.3.3(esbuild@0.27.4)(rollup@4.60.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: storybook: 10.3.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) unplugin: 2.3.11 optionalDependencies: esbuild: 0.27.4 rollup: 4.60.0 - vite: 8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) + vite: 8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3) '@storybook/global@5.0.0': {} @@ -9223,11 +9231,11 @@ snapshots: react-dom: 19.2.0(react@19.2.0) storybook: 10.3.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@storybook/react-vite@10.3.3(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(rollup@4.60.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(typescript@5.9.2)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))': + '@storybook/react-vite@10.3.3(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(rollup@4.60.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(typescript@5.9.2)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.6.4(typescript@5.9.2)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) + '@joshwooding/vite-plugin-react-docgen-typescript': 0.6.4(typescript@5.9.2)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3)) '@rollup/pluginutils': 5.3.0(rollup@4.60.0) - '@storybook/builder-vite': 10.3.3(esbuild@0.27.4)(rollup@4.60.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) + '@storybook/builder-vite': 10.3.3(esbuild@0.27.4)(rollup@4.60.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3)) '@storybook/react': 10.3.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(typescript@5.9.2) empathic: 2.0.0 magic-string: 0.30.21 @@ -9237,7 +9245,7 @@ snapshots: resolve: 1.22.11 storybook: 10.3.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) tsconfig-paths: 4.2.0 - vite: 8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) + vite: 8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - esbuild - rollup @@ -9342,12 +9350,12 @@ snapshots: postcss: 8.5.8 tailwindcss: 4.2.2 - '@tailwindcss/vite@4.2.2(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))': + '@tailwindcss/vite@4.2.2(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@tailwindcss/node': 4.2.2 '@tailwindcss/oxide': 4.2.2 tailwindcss: 4.2.2 - vite: 8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) + vite: 8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3) '@testing-library/dom@10.4.1': dependencies: @@ -9575,18 +9583,18 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@vitejs/plugin-react@6.0.1(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))': + '@vitejs/plugin-react@6.0.1(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.7 - vite: 8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) + vite: 8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3) - '@vitest/browser-playwright@4.1.2(playwright@1.58.2)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.1)': + '@vitest/browser-playwright@4.1.2(playwright@1.58.2)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.1)': dependencies: - '@vitest/browser': 4.1.2(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.1(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))) - '@vitest/mocker': 4.1.2(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/browser': 4.1.2(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.1(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3))) + '@vitest/mocker': 4.1.2(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3)) playwright: 1.58.2 tinyrainbow: 3.1.0 - vitest: 4.1.1(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) + vitest: 4.1.1(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3)) transitivePeerDependencies: - bufferutil - msw @@ -9594,29 +9602,29 @@ snapshots: - vite optional: true - '@vitest/browser-playwright@4.1.2(playwright@1.58.2)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2)': + '@vitest/browser-playwright@4.1.2(playwright@1.58.2)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2)': dependencies: - '@vitest/browser': 4.1.2(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))) - '@vitest/mocker': 4.1.2(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/browser': 4.1.2(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3))) + '@vitest/mocker': 4.1.2(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3)) playwright: 1.58.2 tinyrainbow: 3.1.0 - vitest: 4.1.2(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) + vitest: 4.1.2(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3)) transitivePeerDependencies: - bufferutil - msw - utf-8-validate - vite - '@vitest/browser@4.1.2(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.1(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)))': + '@vitest/browser@4.1.2(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.1(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3)))': dependencies: '@blazediff/core': 1.9.1 - '@vitest/mocker': 4.1.2(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/mocker': 4.1.2(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3)) '@vitest/utils': 4.1.2 magic-string: 0.30.21 pngjs: 7.0.0 sirv: 3.0.2 tinyrainbow: 3.1.0 - vitest: 4.1.1(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) + vitest: 4.1.1(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3)) ws: 8.20.0 transitivePeerDependencies: - bufferutil @@ -9625,16 +9633,16 @@ snapshots: - vite optional: true - '@vitest/browser@4.1.2(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)))': + '@vitest/browser@4.1.2(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3)))': dependencies: '@blazediff/core': 1.9.1 - '@vitest/mocker': 4.1.2(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/mocker': 4.1.2(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3)) '@vitest/utils': 4.1.2 magic-string: 0.30.21 pngjs: 7.0.0 sirv: 3.0.2 tinyrainbow: 3.1.0 - vitest: 4.1.2(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) + vitest: 4.1.2(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3)) ws: 8.20.0 transitivePeerDependencies: - bufferutil @@ -9642,7 +9650,7 @@ snapshots: - utf-8-validate - vite - '@vitest/coverage-istanbul@4.1.1(vitest@4.1.1(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)))': + '@vitest/coverage-istanbul@4.1.1(vitest@4.1.1(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3)))': dependencies: '@babel/core': 7.29.0 '@istanbuljs/schema': 0.1.3 @@ -9654,11 +9662,11 @@ snapshots: magicast: 0.5.2 obug: 2.1.1 tinyrainbow: 3.1.0 - vitest: 4.1.1(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) + vitest: 4.1.1(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3)) transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@4.1.2(@vitest/browser@4.1.2(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))))(vitest@4.1.2(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)))': + '@vitest/coverage-v8@4.1.2(@vitest/browser@4.1.2(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3))))(vitest@4.1.2(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3)))': dependencies: '@bcoe/v8-coverage': 1.0.2 '@vitest/utils': 4.1.2 @@ -9670,9 +9678,9 @@ snapshots: obug: 2.1.1 std-env: 4.0.0 tinyrainbow: 3.1.0 - vitest: 4.1.2(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) + vitest: 4.1.2(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3)) optionalDependencies: - '@vitest/browser': 4.1.2(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))) + '@vitest/browser': 4.1.2(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3))) '@vitest/expect@3.2.4': dependencies: @@ -9700,21 +9708,21 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.1(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))': + '@vitest/mocker@4.1.1(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@vitest/spy': 4.1.1 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) + vite: 8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3) - '@vitest/mocker@4.1.2(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))': + '@vitest/mocker@4.1.2(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@vitest/spy': 4.1.2 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) + vite: 8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3) '@vitest/pretty-format@3.2.4': dependencies: @@ -10888,7 +10896,7 @@ snapshots: fsevents@2.3.3: optional: true - fumadocs-core@16.7.5(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.2)(lucide-react@0.577.0(react@19.2.0))(next@16.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(zod@4.3.6): + fumadocs-core@16.7.5(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.2)(lucide-react@0.577.0(react@19.2.0))(next@16.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(sass@1.98.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(zod@4.3.6): dependencies: '@formatjs/intl-localematcher': 0.8.2 '@orama/orama': 3.1.18 @@ -10920,21 +10928,21 @@ snapshots: '@types/mdast': 4.0.4 '@types/react': 19.2.2 lucide-react: 0.577.0(react@19.2.0) - next: 16.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + next: 16.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(sass@1.98.0) react: 19.2.0 react-dom: 19.2.0(react@19.2.0) zod: 4.3.6 transitivePeerDependencies: - supports-color - fumadocs-mdx@14.2.11(@types/mdast@4.0.4)(@types/mdx@2.0.13)(@types/react@19.2.2)(fumadocs-core@16.7.5(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.2)(lucide-react@0.577.0(react@19.2.0))(next@16.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(zod@4.3.6))(next@16.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)): + fumadocs-mdx@14.2.11(@types/mdast@4.0.4)(@types/mdx@2.0.13)(@types/react@19.2.2)(fumadocs-core@16.7.5(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.2)(lucide-react@0.577.0(react@19.2.0))(next@16.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(sass@1.98.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(zod@4.3.6))(next@16.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(sass@1.98.0))(react@19.2.0)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3)): dependencies: '@mdx-js/mdx': 3.1.1 '@standard-schema/spec': 1.1.0 chokidar: 5.0.0 esbuild: 0.27.4 estree-util-value-to-estree: 3.5.0 - fumadocs-core: 16.7.5(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.2)(lucide-react@0.577.0(react@19.2.0))(next@16.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(zod@4.3.6) + fumadocs-core: 16.7.5(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.2)(lucide-react@0.577.0(react@19.2.0))(next@16.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(sass@1.98.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(zod@4.3.6) js-yaml: 4.1.1 mdast-util-mdx: 3.0.0 mdast-util-to-markdown: 2.1.2 @@ -10951,13 +10959,13 @@ snapshots: '@types/mdast': 4.0.4 '@types/mdx': 2.0.13 '@types/react': 19.2.2 - next: 16.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + next: 16.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(sass@1.98.0) react: 19.2.0 - vite: 8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) + vite: 8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - supports-color - fumadocs-ui@16.7.5(@types/mdx@2.0.13)(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(fumadocs-core@16.7.5(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.2)(lucide-react@0.577.0(react@19.2.0))(next@16.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(zod@4.3.6))(next@16.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(shiki@4.0.2)(tailwindcss@4.2.2): + fumadocs-ui@16.7.5(@types/mdx@2.0.13)(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(fumadocs-core@16.7.5(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.2)(lucide-react@0.577.0(react@19.2.0))(next@16.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(sass@1.98.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(zod@4.3.6))(next@16.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(sass@1.98.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(shiki@4.0.2)(tailwindcss@4.2.2): dependencies: '@fumadocs/tailwind': 0.0.3(tailwindcss@4.2.2) '@radix-ui/react-accordion': 1.2.12(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -10971,7 +10979,7 @@ snapshots: '@radix-ui/react-slot': 1.2.4(@types/react@19.2.2)(react@19.2.0) '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) class-variance-authority: 0.7.1 - fumadocs-core: 16.7.5(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.2)(lucide-react@0.577.0(react@19.2.0))(next@16.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(zod@4.3.6) + fumadocs-core: 16.7.5(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.2)(lucide-react@0.577.0(react@19.2.0))(next@16.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(sass@1.98.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(zod@4.3.6) lucide-react: 0.577.0(react@19.2.0) motion: 12.38.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) next-themes: 0.4.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -10986,7 +10994,7 @@ snapshots: optionalDependencies: '@types/mdx': 2.0.13 '@types/react': 19.2.2 - next: 16.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + next: 16.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(sass@1.98.0) shiki: 4.0.2 transitivePeerDependencies: - '@emotion/is-prop-valid' @@ -11291,6 +11299,9 @@ snapshots: image-size@2.0.2: {} + immutable@5.1.5: + optional: true + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -12287,7 +12298,7 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) - next@16.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + next@16.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(sass@1.98.0): dependencies: '@next/env': 16.2.0 '@swc/helpers': 0.5.15 @@ -12306,6 +12317,7 @@ snapshots: '@next/swc-linux-x64-musl': 16.2.0 '@next/swc-win32-arm64-msvc': 16.2.0 '@next/swc-win32-x64-msvc': 16.2.0 + sass: 1.98.0 sharp: 0.34.5 transitivePeerDependencies: - '@babel/core' @@ -13109,6 +13121,15 @@ snapshots: safer-buffer@2.1.2: {} + sass@1.98.0: + dependencies: + chokidar: 4.0.3 + immutable: 5.1.5 + source-map-js: 1.2.1 + optionalDependencies: + '@parcel/watcher': 2.5.6 + optional: true + saxes@6.0.0: dependencies: xmlchars: 2.2.0 @@ -13784,7 +13805,7 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3): + vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 @@ -13796,13 +13817,14 @@ snapshots: esbuild: 0.27.4 fsevents: 2.3.3 jiti: 2.6.1 + sass: 1.98.0 tsx: 4.21.0 yaml: 2.8.3 - vitest@4.1.1(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)): + vitest@4.1.1(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3)): dependencies: '@vitest/expect': 4.1.1 - '@vitest/mocker': 4.1.1(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/mocker': 4.1.1(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3)) '@vitest/pretty-format': 4.1.1 '@vitest/runner': 4.1.1 '@vitest/snapshot': 4.1.1 @@ -13819,19 +13841,19 @@ snapshots: tinyexec: 1.0.4 tinyglobby: 0.2.15 tinyrainbow: 3.1.0 - vite: 8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) + vite: 8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.15.3 - '@vitest/browser-playwright': 4.1.2(playwright@1.58.2)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.1) + '@vitest/browser-playwright': 4.1.2(playwright@1.58.2)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.1) jsdom: 29.0.1 transitivePeerDependencies: - msw - vitest@4.1.2(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)): + vitest@4.1.2(@types/node@22.15.3)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3)): dependencies: '@vitest/expect': 4.1.2 - '@vitest/mocker': 4.1.2(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/mocker': 4.1.2(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3)) '@vitest/pretty-format': 4.1.2 '@vitest/runner': 4.1.2 '@vitest/snapshot': 4.1.2 @@ -13848,11 +13870,11 @@ snapshots: tinyexec: 1.0.4 tinyglobby: 0.2.15 tinyrainbow: 3.1.0 - vite: 8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) + vite: 8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.15.3 - '@vitest/browser-playwright': 4.1.2(playwright@1.58.2)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) + '@vitest/browser-playwright': 4.1.2(playwright@1.58.2)(vite@8.0.2(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) jsdom: 29.0.1 transitivePeerDependencies: - msw diff --git a/sonar-project.properties b/sonar-project.properties index 27ae6f4..ce57162 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -9,3 +9,6 @@ sonar.exclusions=**/node_modules/**,**/dist/**,**/.next/**,**/.turbo/**,**/cover sonar.typescript.lcov.reportPaths=packages/react/coverage/lcov.info sonar.javascript.lcov.reportPaths=packages/react/coverage/lcov.info + +# CSS uses BEM with explicit variant classes — duplication is by design +sonar.cpd.exclusions=**/*.css From 43275bca63558b31d03a263f5ff35c536037de1c Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Mon, 30 Mar 2026 03:15:45 +0700 Subject: [PATCH 053/199] style(format): Fix prettier and eslint issues --- packages/react/src/components/index.ts | 2 +- packages/styles/src/components/index.css | 2 +- turbo/generators/config.ts | 100 +++++++++++++++++++---- 3 files changed, 85 insertions(+), 19 deletions(-) diff --git a/packages/react/src/components/index.ts b/packages/react/src/components/index.ts index 82e1a1f..89cb2dd 100644 --- a/packages/react/src/components/index.ts +++ b/packages/react/src/components/index.ts @@ -6,4 +6,4 @@ export * from "./icons"; export * from "./surface"; export * from "./surface"; -export * from "./surface"; \ No newline at end of file +export * from "./surface"; diff --git a/packages/styles/src/components/index.css b/packages/styles/src/components/index.css index 27e089a..04c34e1 100644 --- a/packages/styles/src/components/index.css +++ b/packages/styles/src/components/index.css @@ -13,4 +13,4 @@ @import "./badge.css"; @import "./button.css"; -@import "./surface.css"; \ No newline at end of file +@import "./surface.css"; diff --git a/turbo/generators/config.ts b/turbo/generators/config.ts index 97188c7..e1a21fe 100644 --- a/turbo/generators/config.ts +++ b/turbo/generators/config.ts @@ -1,20 +1,52 @@ import type { PlopTypes } from "@turbo/gen"; -export default function generator(plop: PlopTypes.NodePlopAPI): void { +function generator(plop: PlopTypes.NodePlopAPI): void { plop.setGenerator("display-component", { description: "Display-only component (native HTML element)", prompts: [ { type: "input", name: "name", message: "Component name (PascalCase, e.g. Surface):" }, ], actions: [ - { type: "add", path: "packages/styles/src/components/{{dashCase name}}.css", templateFile: "display-component/component.css.hbs" }, - { type: "add", path: "packages/react/src/components/{{dashCase name}}/{{pascalCase name}}.tsx", templateFile: "display-component/Component.tsx.hbs" }, - { type: "add", path: "packages/react/src/components/{{dashCase name}}/{{dashCase name}}.variants.ts", templateFile: "display-component/component.variants.ts.hbs" }, - { type: "add", path: "packages/react/src/components/{{dashCase name}}/{{dashCase name}}.test.tsx", templateFile: "display-component/component.test.tsx.hbs" }, - { type: "add", path: "packages/react/src/components/{{dashCase name}}/{{dashCase name}}.stories.tsx", templateFile: "display-component/component.stories.tsx.hbs" }, - { type: "add", path: "packages/react/src/components/{{dashCase name}}/index.ts", templateFile: "display-component/index.ts.hbs" }, - { type: "append", path: "packages/styles/src/components/index.css", template: '@import "./{{dashCase name}}.css";' }, - { type: "append", path: "packages/react/src/components/index.ts", template: 'export * from "./{{dashCase name}}";' }, + { + type: "add", + path: "packages/styles/src/components/{{dashCase name}}.css", + templateFile: "display-component/component.css.hbs", + }, + { + type: "add", + path: "packages/react/src/components/{{dashCase name}}/{{pascalCase name}}.tsx", + templateFile: "display-component/Component.tsx.hbs", + }, + { + type: "add", + path: "packages/react/src/components/{{dashCase name}}/{{dashCase name}}.variants.ts", + templateFile: "display-component/component.variants.ts.hbs", + }, + { + type: "add", + path: "packages/react/src/components/{{dashCase name}}/{{dashCase name}}.test.tsx", + templateFile: "display-component/component.test.tsx.hbs", + }, + { + type: "add", + path: "packages/react/src/components/{{dashCase name}}/{{dashCase name}}.stories.tsx", + templateFile: "display-component/component.stories.tsx.hbs", + }, + { + type: "add", + path: "packages/react/src/components/{{dashCase name}}/index.ts", + templateFile: "display-component/index.ts.hbs", + }, + { + type: "append", + path: "packages/styles/src/components/index.css", + template: '@import "./{{dashCase name}}.css";', + }, + { + type: "append", + path: "packages/react/src/components/index.ts", + template: 'export * from "./{{dashCase name}}";', + }, ], }); @@ -24,14 +56,48 @@ export default function generator(plop: PlopTypes.NodePlopAPI): void { { type: "input", name: "name", message: "Component name (PascalCase, e.g. Link):" }, ], actions: [ - { type: "add", path: "packages/styles/src/components/{{dashCase name}}.css", templateFile: "interactive-component/component.css.hbs" }, - { type: "add", path: "packages/react/src/components/{{dashCase name}}/{{pascalCase name}}.tsx", templateFile: "interactive-component/Component.tsx.hbs" }, - { type: "add", path: "packages/react/src/components/{{dashCase name}}/{{dashCase name}}.variants.ts", templateFile: "interactive-component/component.variants.ts.hbs" }, - { type: "add", path: "packages/react/src/components/{{dashCase name}}/{{dashCase name}}.test.tsx", templateFile: "interactive-component/component.test.tsx.hbs" }, - { type: "add", path: "packages/react/src/components/{{dashCase name}}/{{dashCase name}}.stories.tsx", templateFile: "interactive-component/component.stories.tsx.hbs" }, - { type: "add", path: "packages/react/src/components/{{dashCase name}}/index.ts", templateFile: "interactive-component/index.ts.hbs" }, - { type: "append", path: "packages/styles/src/components/index.css", template: '@import "./{{dashCase name}}.css";' }, - { type: "append", path: "packages/react/src/components/index.ts", template: 'export * from "./{{dashCase name}}";' }, + { + type: "add", + path: "packages/styles/src/components/{{dashCase name}}.css", + templateFile: "interactive-component/component.css.hbs", + }, + { + type: "add", + path: "packages/react/src/components/{{dashCase name}}/{{pascalCase name}}.tsx", + templateFile: "interactive-component/Component.tsx.hbs", + }, + { + type: "add", + path: "packages/react/src/components/{{dashCase name}}/{{dashCase name}}.variants.ts", + templateFile: "interactive-component/component.variants.ts.hbs", + }, + { + type: "add", + path: "packages/react/src/components/{{dashCase name}}/{{dashCase name}}.test.tsx", + templateFile: "interactive-component/component.test.tsx.hbs", + }, + { + type: "add", + path: "packages/react/src/components/{{dashCase name}}/{{dashCase name}}.stories.tsx", + templateFile: "interactive-component/component.stories.tsx.hbs", + }, + { + type: "add", + path: "packages/react/src/components/{{dashCase name}}/index.ts", + templateFile: "interactive-component/index.ts.hbs", + }, + { + type: "append", + path: "packages/styles/src/components/index.css", + template: '@import "./{{dashCase name}}.css";', + }, + { + type: "append", + path: "packages/react/src/components/index.ts", + template: 'export * from "./{{dashCase name}}";', + }, ], }); } + +export default generator; From 5a9bcb88dbe5a61c0af27df1c0aee09230a1a60c Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Mon, 30 Mar 2026 03:22:06 +0700 Subject: [PATCH 054/199] style(tooling): Fix generator config lint and format conflict --- turbo/generators/config.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/turbo/generators/config.ts b/turbo/generators/config.ts index e1a21fe..2571c7a 100644 --- a/turbo/generators/config.ts +++ b/turbo/generators/config.ts @@ -4,7 +4,11 @@ function generator(plop: PlopTypes.NodePlopAPI): void { plop.setGenerator("display-component", { description: "Display-only component (native HTML element)", prompts: [ - { type: "input", name: "name", message: "Component name (PascalCase, e.g. Surface):" }, + { + type: "input", + name: "name", + message: "Component name (PascalCase, e.g. Surface):", + }, ], actions: [ { @@ -53,7 +57,11 @@ function generator(plop: PlopTypes.NodePlopAPI): void { plop.setGenerator("interactive-component", { description: "Interactive component (wraps React Aria)", prompts: [ - { type: "input", name: "name", message: "Component name (PascalCase, e.g. Link):" }, + { + type: "input", + name: "name", + message: "Component name (PascalCase, e.g. Link):", + }, ], actions: [ { From 35abbc6fcc37b732014bf179398b0be85e5bd11d Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Mon, 30 Mar 2026 03:26:54 +0700 Subject: [PATCH 055/199] fix(ci): Exclude generators from duplication --- sonar-project.properties | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sonar-project.properties b/sonar-project.properties index ce57162..9758f34 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -11,4 +11,5 @@ sonar.typescript.lcov.reportPaths=packages/react/coverage/lcov.info sonar.javascript.lcov.reportPaths=packages/react/coverage/lcov.info # CSS uses BEM with explicit variant classes — duplication is by design -sonar.cpd.exclusions=**/*.css +# Generator config has two similar generators (display + interactive) — by design +sonar.cpd.exclusions=**/*.css,turbo/generators/** From 893e6875b30782ce1a213860c2f05092f5bb58c1 Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Mon, 30 Mar 2026 03:29:42 +0700 Subject: [PATCH 056/199] refactor(tooling): Deduplicate generator config --- sonar-project.properties | 3 +- turbo/generators/config.ts | 133 ++++++++++++++----------------------- 2 files changed, 50 insertions(+), 86 deletions(-) diff --git a/sonar-project.properties b/sonar-project.properties index 9758f34..ce57162 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -11,5 +11,4 @@ sonar.typescript.lcov.reportPaths=packages/react/coverage/lcov.info sonar.javascript.lcov.reportPaths=packages/react/coverage/lcov.info # CSS uses BEM with explicit variant classes — duplication is by design -# Generator config has two similar generators (display + interactive) — by design -sonar.cpd.exclusions=**/*.css,turbo/generators/** +sonar.cpd.exclusions=**/*.css diff --git a/turbo/generators/config.ts b/turbo/generators/config.ts index 2571c7a..42419b3 100644 --- a/turbo/generators/config.ts +++ b/turbo/generators/config.ts @@ -1,5 +1,52 @@ import type { PlopTypes } from "@turbo/gen"; +const TEMPLATE_FILES = [ + { ext: "css", path: "packages/styles/src/components/{{dashCase name}}.css", file: "component.css.hbs" }, + { + ext: "tsx", + path: "packages/react/src/components/{{dashCase name}}/{{pascalCase name}}.tsx", + file: "Component.tsx.hbs", + }, + { + ext: "variants", + path: "packages/react/src/components/{{dashCase name}}/{{dashCase name}}.variants.ts", + file: "component.variants.ts.hbs", + }, + { + ext: "test", + path: "packages/react/src/components/{{dashCase name}}/{{dashCase name}}.test.tsx", + file: "component.test.tsx.hbs", + }, + { + ext: "stories", + path: "packages/react/src/components/{{dashCase name}}/{{dashCase name}}.stories.tsx", + file: "component.stories.tsx.hbs", + }, + { ext: "index", path: "packages/react/src/components/{{dashCase name}}/index.ts", file: "index.ts.hbs" }, +]; + +function createActions(templateDir: string): PlopTypes.ActionType[] { + const addActions: PlopTypes.ActionType[] = TEMPLATE_FILES.map(({ path, file }) => ({ + type: "add" as const, + path, + templateFile: `${templateDir}/${file}`, + })); + + return [ + ...addActions, + { + type: "append", + path: "packages/styles/src/components/index.css", + template: '@import "./{{dashCase name}}.css";', + }, + { + type: "append", + path: "packages/react/src/components/index.ts", + template: 'export * from "./{{dashCase name}}";', + }, + ]; +} + function generator(plop: PlopTypes.NodePlopAPI): void { plop.setGenerator("display-component", { description: "Display-only component (native HTML element)", @@ -10,48 +57,7 @@ function generator(plop: PlopTypes.NodePlopAPI): void { message: "Component name (PascalCase, e.g. Surface):", }, ], - actions: [ - { - type: "add", - path: "packages/styles/src/components/{{dashCase name}}.css", - templateFile: "display-component/component.css.hbs", - }, - { - type: "add", - path: "packages/react/src/components/{{dashCase name}}/{{pascalCase name}}.tsx", - templateFile: "display-component/Component.tsx.hbs", - }, - { - type: "add", - path: "packages/react/src/components/{{dashCase name}}/{{dashCase name}}.variants.ts", - templateFile: "display-component/component.variants.ts.hbs", - }, - { - type: "add", - path: "packages/react/src/components/{{dashCase name}}/{{dashCase name}}.test.tsx", - templateFile: "display-component/component.test.tsx.hbs", - }, - { - type: "add", - path: "packages/react/src/components/{{dashCase name}}/{{dashCase name}}.stories.tsx", - templateFile: "display-component/component.stories.tsx.hbs", - }, - { - type: "add", - path: "packages/react/src/components/{{dashCase name}}/index.ts", - templateFile: "display-component/index.ts.hbs", - }, - { - type: "append", - path: "packages/styles/src/components/index.css", - template: '@import "./{{dashCase name}}.css";', - }, - { - type: "append", - path: "packages/react/src/components/index.ts", - template: 'export * from "./{{dashCase name}}";', - }, - ], + actions: createActions("display-component"), }); plop.setGenerator("interactive-component", { @@ -63,48 +69,7 @@ function generator(plop: PlopTypes.NodePlopAPI): void { message: "Component name (PascalCase, e.g. Link):", }, ], - actions: [ - { - type: "add", - path: "packages/styles/src/components/{{dashCase name}}.css", - templateFile: "interactive-component/component.css.hbs", - }, - { - type: "add", - path: "packages/react/src/components/{{dashCase name}}/{{pascalCase name}}.tsx", - templateFile: "interactive-component/Component.tsx.hbs", - }, - { - type: "add", - path: "packages/react/src/components/{{dashCase name}}/{{dashCase name}}.variants.ts", - templateFile: "interactive-component/component.variants.ts.hbs", - }, - { - type: "add", - path: "packages/react/src/components/{{dashCase name}}/{{dashCase name}}.test.tsx", - templateFile: "interactive-component/component.test.tsx.hbs", - }, - { - type: "add", - path: "packages/react/src/components/{{dashCase name}}/{{dashCase name}}.stories.tsx", - templateFile: "interactive-component/component.stories.tsx.hbs", - }, - { - type: "add", - path: "packages/react/src/components/{{dashCase name}}/index.ts", - templateFile: "interactive-component/index.ts.hbs", - }, - { - type: "append", - path: "packages/styles/src/components/index.css", - template: '@import "./{{dashCase name}}.css";', - }, - { - type: "append", - path: "packages/react/src/components/index.ts", - template: 'export * from "./{{dashCase name}}";', - }, - ], + actions: createActions("interactive-component"), }); } From 3232c95c5aa1ee67c7d7e8990c9f1c57d5b529bb Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Mon, 13 Apr 2026 20:36:04 +0700 Subject: [PATCH 057/199] refactor(react): Remove isShadow, vitest package, and clean up repo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove isShadow prop from Badge, Button, Surface (variants, components, CSS, tests, stories) - Remove packages/vitest — inline config into packages/react - Remove packages/mcp — unused - Remove all code comments across styles package - Fix exports in package.json to point to dist/ for npm publish - Fix README with correct exports and usage example - Remove --ease-in-out duplicate token - Remove redundant strictNullChecks from child tsconfigs - Remove duplicate surface export - Remove empty packages/quality/prettier directory - Add icons to tsup.config.ts and package.json exports - Fix translateY(4px) to 0.25rem in motion keyframes - Use calc() with golden ratio formula for spacing - Add Badge and Surface to introduction docs - Update design-tokens docs Co-Authored-By: Claude Opus 4.6 (1M context) --- .mcp.json.example | 46 - apps/docs/public/llm/badge.md | 11 - apps/docs/public/llm/surface.md | 2 - apps/docs/src/app/docs/[[...slug]]/page.tsx | 4 - apps/docs/src/components/MdxComponents.tsx | 30 - .../src/content/docs/components/badge.mdx | 16 - .../src/content/docs/components/surface.mdx | 14 - .../content/docs/overview/introduction.mdx | 2 + .../src/content/docs/styles/design-tokens.mdx | 25 +- apps/docs/tsconfig.json | 1 - apps/storybook/tsconfig.json | 1 - packages/mcp/package.json | 26 - packages/mcp/src/index.ts | 11 - packages/mcp/tsconfig.json | 11 - packages/mcp/tsup.config.ts | 13 - packages/react/README.md | 21 +- packages/react/package.json | 18 +- packages/react/src/components/badge/Badge.tsx | 3 +- .../src/components/badge/badge.stories.tsx | 68 +- .../react/src/components/badge/badge.test.tsx | 10 - .../src/components/badge/badge.variants.ts | 1 - .../react/src/components/button/Button.tsx | 3 +- .../src/components/button/button.stories.tsx | 70 +- .../src/components/button/button.test.tsx | 10 - .../src/components/button/button.variants.ts | 1 - packages/react/src/components/index.ts | 2 - .../react/src/components/surface/Surface.tsx | 3 +- .../components/surface/surface.stories.tsx | 45 +- .../src/components/surface/surface.test.tsx | 10 - .../components/surface/surface.variants.ts | 1 - .../setup.ts => react/src/test-setup.ts} | 0 packages/react/tsconfig.json | 3 +- packages/react/tsup.config.ts | 1 + packages/react/vitest.config.ts | 23 +- packages/styles/src/components/badge.css | 77 +- packages/styles/src/components/button.css | 75 +- packages/styles/src/components/index.css | 12 - packages/styles/src/components/surface.css | 4 - packages/styles/src/index.css | 10 - packages/styles/src/tokens/colors.css | 61 - packages/styles/src/tokens/index.css | 9 - packages/styles/src/tokens/layout.css | 18 - packages/styles/src/tokens/motion.css | 29 +- packages/styles/src/tokens/typography.css | 14 - packages/styles/src/utilities/focus.css | 23 - packages/styles/src/utilities/index.css | 9 - packages/styles/src/utilities/interaction.css | 11 - packages/styles/src/utilities/sizing.css | 16 - packages/styles/src/utilities/status.css | 20 - packages/vitest/configs/base.ts | 18 - packages/vitest/configs/react.ts | 17 - packages/vitest/package.json | 29 - .../vitest/scripts/collect-json-outputs.ts | 24 - packages/vitest/tsconfig.json | 7 - packages/vitest/turbo.json | 14 - pnpm-lock.yaml | 1053 +---------------- .../display-component/component.css.hbs | 2 - .../interactive-component/component.css.hbs | 2 - 58 files changed, 164 insertions(+), 1896 deletions(-) delete mode 100644 .mcp.json.example delete mode 100644 packages/mcp/package.json delete mode 100644 packages/mcp/src/index.ts delete mode 100644 packages/mcp/tsconfig.json delete mode 100644 packages/mcp/tsup.config.ts rename packages/{vitest/setup.ts => react/src/test-setup.ts} (100%) delete mode 100644 packages/vitest/configs/base.ts delete mode 100644 packages/vitest/configs/react.ts delete mode 100644 packages/vitest/package.json delete mode 100644 packages/vitest/scripts/collect-json-outputs.ts delete mode 100644 packages/vitest/tsconfig.json delete mode 100644 packages/vitest/turbo.json diff --git a/.mcp.json.example b/.mcp.json.example deleted file mode 100644 index 78d87fe..0000000 --- a/.mcp.json.example +++ /dev/null @@ -1,46 +0,0 @@ -{ - "mcpServers": { - "package-registry": { - "command": "npx", - "args": ["-y", "package-registry-mcp"] - }, - "chrome-devtools": { - "command": "npx", - "args": ["-y", "chrome-devtools-mcp@latest"] - }, - "eslint": { - "command": "npx", - "args": ["@eslint/mcp@latest"] - }, - "context7": { - "type": "http", - "url": "https://mcp.context7.com/mcp", - "headers": { - "CONTEXT7_API_KEY": "${CONTEXT7_API_KEY}" - } - }, - "sonarqube": { - "command": "docker", - "args": [ - "run", "-i", "--rm", "--init", "--pull=always", - "-e", "SONARQUBE_TOKEN", "-e", "SONARQUBE_ORG", - "mcp/sonarqube" - ], - "env": { - "SONARQUBE_TOKEN": "${SONARQUBE_TOKEN}", - "SONARQUBE_ORG": "${SONARQUBE_ORG}" - } - }, - "fried-ui": { - "command": "node", - "args": ["./packages/mcp/dist/index.mjs"] - }, - "github": { - "command": "npx", - "args": ["-y", "@modelcontextprotocol/server-github"], - "env": { - "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_PERSONAL_ACCESS_TOKEN}" - } - } - } -} diff --git a/apps/docs/public/llm/badge.md b/apps/docs/public/llm/badge.md index 6db426b..c2f557c 100644 --- a/apps/docs/public/llm/badge.md +++ b/apps/docs/public/llm/badge.md @@ -21,7 +21,6 @@ import { Badge } from "@fried-ui/react"; | `variant` | `"primary"` \| `"secondary"` \| `"ghost"` \| `"outline"` \| `"success"` \| `"warning"` \| `"danger"` \| `"info"` \| `"primary-soft"` \| `"secondary-soft"` \| `"success-soft"` \| `"warning-soft"` \| `"danger-soft"` \| `"info-soft"` \| `"primary-outline"` \| `"secondary-outline"` \| `"success-outline"` \| `"warning-outline"` \| `"danger-outline"` \| `"info-outline"` | `"primary"` | Visual variant | | `size` | `"sm"` \| `"md"` \| `"lg"` | `"md"` | Size — controls padding, font | | `isIconOnly` | `boolean` | `false` | Square badge for icon-only indicators | -| `isShadow` | `boolean` | `false` | Show colored drop shadow (em-based) | | `className` | `string` | - | Additional CSS classes | | `ref` | `Ref` | - | Forwarded ref to span element | | `children` | `ReactNode` | - | Badge content | @@ -55,7 +54,6 @@ import { Badge } from "@fried-ui/react"; | `fri-badge--size-md` | Medium size | | `fri-badge--size-lg` | Large size | | `fri-badge--icon-only` | Icon-only square | -| `fri-badge--shadow` | Colored drop shadow | ## CSS Variables @@ -97,15 +95,6 @@ Transparent background with colored border and text. Danger ``` -### Shadow - -Colored drop shadow that scales with font-size (em-based). - -```tsx -Primary -Success -``` - ### Icon Only Square badge for icon-only indicators. Always provide `aria-label`. diff --git a/apps/docs/public/llm/surface.md b/apps/docs/public/llm/surface.md index e5bca29..20d6f7a 100644 --- a/apps/docs/public/llm/surface.md +++ b/apps/docs/public/llm/surface.md @@ -20,7 +20,6 @@ import { Surface } from "@fried-ui/react"; | ----------- | ------------------------------------------------------------ | ----------- | ------------------------ | | `variant` | `"default"` \| `"bordered"` \| `"glass"` \| `"primary"` | `"default"` | The visual variant | | `radius` | `"none"` \| `"sm"` \| `"md"` \| `"lg"` \| `"xl"` \| `"full"` | `"md"` | Border radius | -| `isShadow` | `boolean` | `false` | Show drop shadow | | `className` | `string` | - | Additional CSS classes | | `ref` | `Ref` | - | Forwarded ref to the div | | `children` | `ReactNode` | - | Surface content | @@ -34,7 +33,6 @@ import { Surface } from "@fried-ui/react"; | `fri-surface--bordered` | Bordered variant | | `fri-surface--glass` | Glass variant | | `fri-surface--primary` | Primary variant | -| `fri-surface--shadow` | Drop shadow | | `fri-surface--radius-none` | No border radius | | `fri-surface--radius-sm` | Small border radius | | `fri-surface--radius-md` | Medium border radius | diff --git a/apps/docs/src/app/docs/[[...slug]]/page.tsx b/apps/docs/src/app/docs/[[...slug]]/page.tsx index 5edfdfd..20eb723 100644 --- a/apps/docs/src/app/docs/[[...slug]]/page.tsx +++ b/apps/docs/src/app/docs/[[...slug]]/page.tsx @@ -9,7 +9,6 @@ import { BadgeDemo, BadgeIconOnly, BadgeOutlineVariants, - BadgeShadow, BadgeSizes, BadgeSoftVariants, BadgeVariants, @@ -28,7 +27,6 @@ import { SurfaceBadgeLinks, SurfaceDemo, SurfaceRadii, - SurfaceShadow, SurfaceVariants, } from "@/components/MdxComponents"; import { Preview } from "@/components/Preview"; @@ -48,7 +46,6 @@ const mdxComponents = { BadgeDemo, BadgeIconOnly, BadgeOutlineVariants, - BadgeShadow, BadgeSizes, BadgeSoftVariants, BadgeVariants, @@ -69,7 +66,6 @@ const mdxComponents = { SurfaceBadgeLinks, SurfaceDemo, SurfaceRadii, - SurfaceShadow, SurfaceVariants, }; diff --git a/apps/docs/src/components/MdxComponents.tsx b/apps/docs/src/components/MdxComponents.tsx index 32a44c4..6a25904 100644 --- a/apps/docs/src/components/MdxComponents.tsx +++ b/apps/docs/src/components/MdxComponents.tsx @@ -84,22 +84,6 @@ const BadgeOutlineVariants = (): React.JSX.Element => { ); }; -const BadgeShadow = (): React.JSX.Element => { - return ( -
- Primary - - - Success - - - - Danger - -
- ); -}; - const BadgeIconOnly = (): React.JSX.Element => { return (
@@ -360,24 +344,11 @@ const SurfaceRadii = (): React.JSX.Element => { ); }; -const SurfaceShadow = (): React.JSX.Element => { - return ( -
- No shadow - - - With shadow - -
- ); -}; - export { BadgeBadgeLinks, BadgeDemo, BadgeIconOnly, BadgeOutlineVariants, - BadgeShadow, BadgeSizes, BadgeSoftVariants, BadgeVariants, @@ -396,6 +367,5 @@ export { SurfaceBadgeLinks, SurfaceDemo, SurfaceRadii, - SurfaceShadow, SurfaceVariants, }; diff --git a/apps/docs/src/content/docs/components/badge.mdx b/apps/docs/src/content/docs/components/badge.mdx index 9fbc0a0..fe3b695 100644 --- a/apps/docs/src/content/docs/components/badge.mdx +++ b/apps/docs/src/content/docs/components/badge.mdx @@ -76,20 +76,6 @@ Transparent background with colored border and text. Uses the `{color}-outline` Info ``` -## Shadow - -The `isShadow` prop adds a colored drop shadow that scales with font-size (em-based). - - - - - -```tsx -Primary -Success -Danger -``` - ## Icon Only Square badge for icon-only indicators. Always provide `aria-label`. @@ -127,7 +113,6 @@ The `size` prop controls padding, gap, and font-size. | `variant` | `"primary"` \| `"secondary"` \| `"ghost"` \| `"outline"` \| `"success"` \| `"warning"` \| `"danger"` \| `"info"` \| `"primary-soft"` \| `"secondary-soft"` \| `"success-soft"` \| `"warning-soft"` \| `"danger-soft"` \| `"info-soft"` \| `"primary-outline"` \| `"secondary-outline"` \| `"success-outline"` \| `"warning-outline"` \| `"danger-outline"` \| `"info-outline"` | `"primary"` | The visual variant of the badge | | `size` | `"sm"` \| `"md"` \| `"lg"` | `"md"` | The size of the badge | | `isIconOnly` | `boolean` | `false` | Square badge for icon-only indicators | -| `isShadow` | `boolean` | `false` | Show colored drop shadow (em-based) | | `className` | `string` | - | Additional CSS classes | | `ref` | `Ref` | - | Forwarded ref to the span | | `children` | `ReactNode` | - | Badge content | @@ -161,7 +146,6 @@ The `size` prop controls padding, gap, and font-size. | `fri-badge--size-md` | Medium size | | `fri-badge--size-lg` | Large size | | `fri-badge--icon-only` | Icon-only square | -| `fri-badge--shadow` | Colored drop shadow | ### CSS Variables diff --git a/apps/docs/src/content/docs/components/surface.mdx b/apps/docs/src/content/docs/components/surface.mdx index 11e6fb8..4c1ba42 100644 --- a/apps/docs/src/content/docs/components/surface.mdx +++ b/apps/docs/src/content/docs/components/surface.mdx @@ -56,18 +56,6 @@ The `radius` prop controls border-radius. full ``` -## Shadow - -The `isShadow` prop adds a drop shadow. - - - - - -```tsx -With shadow -``` - ## API Reference ### Surface Props @@ -76,7 +64,6 @@ The `isShadow` prop adds a drop shadow. | ----------- | ------------------------------------------------------------ | ----------- | ------------------------ | | `variant` | `"default"` \| `"bordered"` \| `"glass"` \| `"primary"` | `"default"` | The visual variant | | `radius` | `"none"` \| `"sm"` \| `"md"` \| `"lg"` \| `"xl"` \| `"full"` | `"md"` | Border radius | -| `isShadow` | `boolean` | `false` | Show drop shadow | | `className` | `string` | - | Additional CSS classes | | `ref` | `Ref` | - | Forwarded ref to the div | | `children` | `ReactNode` | - | Surface content | @@ -90,7 +77,6 @@ The `isShadow` prop adds a drop shadow. | `fri-surface--bordered` | Bordered variant | | `fri-surface--glass` | Glass variant | | `fri-surface--primary` | Primary variant | -| `fri-surface--shadow` | Drop shadow | | `fri-surface--radius-none` | No border radius | | `fri-surface--radius-sm` | Small border radius | | `fri-surface--radius-md` | Medium border radius | diff --git a/apps/docs/src/content/docs/overview/introduction.mdx b/apps/docs/src/content/docs/overview/introduction.mdx index 966e037..6688bcf 100644 --- a/apps/docs/src/content/docs/overview/introduction.mdx +++ b/apps/docs/src/content/docs/overview/introduction.mdx @@ -27,4 +27,6 @@ Fried UI is a React component library built on top of [React Aria Components](ht ## Components +- [Badge](/docs/components/badge) — Display label with 20 variants (solid, soft, outline) - [Button](/docs/components/button) — Accessible button with 8 variants, golden ratio spacing, and icon-only mode +- [Surface](/docs/components/surface) — Visual layer component for cards and panels diff --git a/apps/docs/src/content/docs/styles/design-tokens.mdx b/apps/docs/src/content/docs/styles/design-tokens.mdx index 8cc24e1..5c1509c 100644 --- a/apps/docs/src/content/docs/styles/design-tokens.mdx +++ b/apps/docs/src/content/docs/styles/design-tokens.mdx @@ -37,18 +37,8 @@ Available groups: `primary`, `secondary`, `success`, `warning`, `danger`, `info` Each color group has a shadow token using `em` units — shadows scale proportionally with the component's font-size. -| Token | Usage | -| ----------------------- | ------------------------------- | -| `--color-{name}-shadow` | Colored drop shadow per variant | - -Light mode uses a tight two-layer shadow (Fibonacci: 2, 3, 5 → em). Dark mode uses a softer glow with larger blur. - -Applied via the `isShadow` prop on components: - -```tsx - -Larger shadow -``` +| Token | Usage | +| ----- | ----- | ## Layout Elevation @@ -158,12 +148,11 @@ All durations stay under 400ms (Doherty Threshold). Automatically disabled when ### Easing -| Token | Value | Usage | -| --------------- | ---------------------------- | ----------------------- | -| `--ease-smooth` | cubic-bezier(0.4, 0, 0.2, 1) | Default for most | -| `--ease-in` | cubic-bezier(0.4, 0, 1, 1) | Element exiting | -| `--ease-out` | cubic-bezier(0, 0, 0.2, 1) | Element entering | -| `--ease-in-out` | cubic-bezier(0.4, 0, 0.2, 1) | Element moving position | +| Token | Value | Usage | +| --------------- | ---------------------------- | ---------------- | +| `--ease-smooth` | cubic-bezier(0.4, 0, 0.2, 1) | Default for most | +| `--ease-in` | cubic-bezier(0.4, 0, 1, 1) | Element exiting | +| `--ease-out` | cubic-bezier(0, 0, 0.2, 1) | Element entering | ```tsx
Hover me
diff --git a/apps/docs/tsconfig.json b/apps/docs/tsconfig.json index 626efc0..ba2bff8 100644 --- a/apps/docs/tsconfig.json +++ b/apps/docs/tsconfig.json @@ -6,7 +6,6 @@ "name": "next" } ], - "strictNullChecks": true, "paths": { "@/*": ["./src/*"], "@/.source/*": ["./.source/*"], diff --git a/apps/storybook/tsconfig.json b/apps/storybook/tsconfig.json index de50dd3..5cb0a3f 100644 --- a/apps/storybook/tsconfig.json +++ b/apps/storybook/tsconfig.json @@ -3,7 +3,6 @@ "compilerOptions": { "rootDir": ".storybook", "outDir": "dist", - "strictNullChecks": true, "types": ["vite/client"] }, "include": ["**/*.ts", "**/*.tsx", ".storybook/**/*.ts"], diff --git a/packages/mcp/package.json b/packages/mcp/package.json deleted file mode 100644 index 42f8471..0000000 --- a/packages/mcp/package.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "@fried-ui/mcp", - "version": "0.1.0", - "private": true, - "description": "MCP server for fried-ui — query components, design tokens, and API docs", - "type": "module", - "bin": { - "fried-ui-mcp": "./dist/index.mjs" - }, - "scripts": { - "build": "tsup", - "dev": "tsup --watch" - }, - "dependencies": { - "@modelcontextprotocol/sdk": "^1.12.1", - "zod": "^3.25.67" - }, - "devDependencies": { - "@types/node": "^22.15.3", - "tsup": "^8.5.1", - "typescript": "5.9.2" - }, - "engines": { - "node": ">=18" - } -} diff --git a/packages/mcp/src/index.ts b/packages/mcp/src/index.ts deleted file mode 100644 index e90d5df..0000000 --- a/packages/mcp/src/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; - -const server = new McpServer({ - name: "fried-ui", - version: "0.1.0", -}); - -const transport = new StdioServerTransport(); -await server.connect(transport); -console.error("fried-ui MCP server running on stdio"); diff --git a/packages/mcp/tsconfig.json b/packages/mcp/tsconfig.json deleted file mode 100644 index f7e1ec1..0000000 --- a/packages/mcp/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "@repo/quality/tsconfig/base.json", - "compilerOptions": { - "rootDir": "src", - "outDir": "dist", - "strictNullChecks": true, - "types": ["node"] - }, - "include": ["src"], - "exclude": ["node_modules", "dist"] -} diff --git a/packages/mcp/tsup.config.ts b/packages/mcp/tsup.config.ts deleted file mode 100644 index ddf35ce..0000000 --- a/packages/mcp/tsup.config.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineConfig } from "tsup"; - -export default defineConfig({ - entry: { - index: "src/index.ts", - }, - format: ["esm"], - outExtension: () => ({ js: ".mjs" }), - banner: { - js: "#!/usr/bin/env node", - }, - clean: true, -}); diff --git a/packages/react/README.md b/packages/react/README.md index 3d62960..298692c 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -20,21 +20,22 @@ Peer dependencies: `react >=19.0.0`, `react-dom >=19.0.0`, `tailwindcss >=4.0.0` ## Usage ```tsx -import { Box } from "@fried-ui/react/box"; +import { Button } from "@fried-ui/react"; - - Hello -; +; ``` ## Exports -| Path | Description | -| -------------------------- | -------------------------------- | -| `@fried-ui/react` | All components + utilities | -| `@fried-ui/react/box` | Box polymorphic layout primitive | -| `@fried-ui/react/aria` | React Aria Components re-exports | -| `@fried-ui/react/utils/cn` | Tailwind class merge utility | +| Path | Description | +| ------------------------- | --------------------------- | +| `@fried-ui/react` | All components + icons | +| `@fried-ui/react/badge` | Badge component | +| `@fried-ui/react/button` | Button component | +| `@fried-ui/react/icons` | SVG icon components | +| `@fried-ui/react/surface` | Surface container component | ## Documentation diff --git a/packages/react/package.json b/packages/react/package.json index 46eef24..ef08f43 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -24,16 +24,24 @@ "type": "module", "exports": { ".": { - "import": "./src/index.ts" + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs" }, "./badge": { - "import": "./src/components/badge/index.ts" + "types": "./dist/components/badge/index.d.ts", + "import": "./dist/components/badge/index.mjs" }, "./button": { - "import": "./src/components/button/index.ts" + "types": "./dist/components/button/index.d.ts", + "import": "./dist/components/button/index.mjs" + }, + "./icons": { + "types": "./dist/components/icons/index.d.ts", + "import": "./dist/components/icons/index.mjs" }, "./surface": { - "import": "./src/components/surface/index.ts" + "types": "./dist/components/surface/index.d.ts", + "import": "./dist/components/surface/index.mjs" } }, "main": "./dist/index.mjs", @@ -60,7 +68,6 @@ "react-aria-components": "^1.16.0" }, "devDependencies": { - "@fried-ui/vitest": "workspace:*", "@repo/quality": "workspace:*", "@storybook/react": "^10.3.3", "@testing-library/jest-dom": "^6.9.1", @@ -70,6 +77,7 @@ "@types/react": "19.2.2", "@types/react-dom": "19.2.2", "@vitejs/plugin-react": "^6.0.1", + "@vitest/coverage-istanbul": "^4.1.1", "eslint": "^9.39.1", "jsdom": "^29.0.1", "react": "^19.2.0", diff --git a/packages/react/src/components/badge/Badge.tsx b/packages/react/src/components/badge/Badge.tsx index a4b8264..bc3436c 100644 --- a/packages/react/src/components/badge/Badge.tsx +++ b/packages/react/src/components/badge/Badge.tsx @@ -14,7 +14,7 @@ export type BadgeProps = Omit, "className"> & }; const Badge = (props: Readonly) => { - const { children, className, isIconOnly, isShadow, ref, size, variant, ...rest } = props; + const { children, className, isIconOnly, ref, size, variant, ...rest } = props; const badgeClassName = clsx( bem({ @@ -23,7 +23,6 @@ const Badge = (props: Readonly) => { variant, size, "icon-only": isIconOnly, - shadow: isShadow, }, }), className, diff --git a/packages/react/src/components/badge/badge.stories.tsx b/packages/react/src/components/badge/badge.stories.tsx index 124699a..f8fd8fa 100644 --- a/packages/react/src/components/badge/badge.stories.tsx +++ b/packages/react/src/components/badge/badge.stories.tsx @@ -89,19 +89,6 @@ const meta = { category: "Style Variants", }, }, - isShadow: { - control: "boolean", - description: "Whether to show a colored drop shadow", - table: { - type: { - summary: "boolean", - }, - defaultValue: { - summary: "false", - }, - category: "Style Variants", - }, - }, className: { control: "text", description: "Additional CSS classes", @@ -292,7 +279,6 @@ const WithIcon: Story = { source: { code: `import { Badge, SettingsIcon } from "@fried-ui/react"; -// Option 1: slot="icon" (recommended) const WithIcon = () => { return ( @@ -302,7 +288,6 @@ const WithIcon = () => { ); }; -// Option 2: className const WithIconAlt = () => { return ( @@ -317,68 +302,23 @@ const WithIconAlt = () => { render: (args): React.JSX.Element => (
- + Settings - + Active - + Error
), }; -const Shadow: Story = { - parameters: { - docs: { - source: { - code: `import { Badge } from "@fried-ui/react"; - -const Shadow = () => { - return ( -
- Primary - Success - Warning - Danger - Info -
- ); -};`, - }, - }, - }, - render: (args): React.JSX.Element => ( -
- - Primary - - - - Success - - - - Warning - - - - Danger - - - - Info - -
- ), -}; - const IconOnly: Story = { parameters: { docs: { @@ -462,6 +402,6 @@ const SoftVariants = () => { ), }; -export { Default, Variants, SoftVariants, Sizes, Shadow, WithIcon, IconOnly }; +export { Default, Variants, SoftVariants, Sizes, WithIcon, IconOnly }; export default meta; diff --git a/packages/react/src/components/badge/badge.test.tsx b/packages/react/src/components/badge/badge.test.tsx index 68aba4d..9a409e5 100644 --- a/packages/react/src/components/badge/badge.test.tsx +++ b/packages/react/src/components/badge/badge.test.tsx @@ -72,16 +72,6 @@ describe("Badge", () => { expect(screen.getByText("X").className).toContain("fri-badge--icon-only"); }); - it("applies shadow class when isShadow is true", () => { - render(Shadow); - expect(screen.getByText("Shadow").className).toContain("fri-badge--shadow"); - }); - - it("does not apply shadow class when isShadow is false", () => { - render(No shadow); - expect(screen.getByText("No shadow").className).not.toContain("fri-badge--shadow"); - }); - it("merges custom className", () => { render(Styled); expect(screen.getByText("Styled").className).toContain("mt-4"); diff --git a/packages/react/src/components/badge/badge.variants.ts b/packages/react/src/components/badge/badge.variants.ts index d2125c5..ae2a0b5 100644 --- a/packages/react/src/components/badge/badge.variants.ts +++ b/packages/react/src/components/badge/badge.variants.ts @@ -1,6 +1,5 @@ export interface BadgeVariantsProps { isIconOnly?: boolean; - isShadow?: boolean; size?: "sm" | "md" | "lg"; variant?: | "primary" diff --git a/packages/react/src/components/button/Button.tsx b/packages/react/src/components/button/Button.tsx index 8f889e2..a0daf00 100644 --- a/packages/react/src/components/button/Button.tsx +++ b/packages/react/src/components/button/Button.tsx @@ -18,7 +18,7 @@ export type ButtonProps = ButtonVariantsProps & { } & Omit, "className" | "children">; const Button = (props: Readonly) => { - const { children, className, isFullWidth, isIconOnly, isShadow, radius, ref, size, variant, ...rest } = props; + const { children, className, isFullWidth, isIconOnly, radius, ref, size, variant, ...rest } = props; const baseClassName = bem({ block: "fri-button", @@ -28,7 +28,6 @@ const Button = (props: Readonly) => { radius, "full-width": isFullWidth, "icon-only": isIconOnly, - shadow: isShadow, disabled: rest.isDisabled, pending: rest.isPending, }, diff --git a/packages/react/src/components/button/button.stories.tsx b/packages/react/src/components/button/button.stories.tsx index 4e01056..62b8c7c 100644 --- a/packages/react/src/components/button/button.stories.tsx +++ b/packages/react/src/components/button/button.stories.tsx @@ -82,19 +82,6 @@ const meta = { category: "Style Variants", }, }, - isShadow: { - control: "boolean", - description: "Whether to show a colored drop shadow", - table: { - type: { - summary: "boolean", - }, - defaultValue: { - summary: "false", - }, - category: "Style Variants", - }, - }, isFullWidth: { control: "boolean", description: "Whether the button takes full width of its container", @@ -329,7 +316,6 @@ const WithIcon: Story = { source: { code: `import { Button, ArrowRightIcon } from "@fried-ui/react"; -// Option 1: slot="icon" (recommended) const WithIcon = () => { return (
@@ -377,7 +362,6 @@ const IconOnly: Story = { source: { code: `import { Button, SettingsIcon } from "@fried-ui/react"; -// Option 1: slot="icon" (recommended) const IconOnly = () => { return ( - - - - -
- ); -};`, - }, - }, - }, - render: (args): React.JSX.Element => ( -
- - - - - - - - - -
- ), -}; - -export { Default, Variants, Sizes, Radii, Shadow, WithIcon, IconOnly, FullWidth, Disabled, Pending, RenderProps }; +export { Default, Variants, Sizes, Radii, WithIcon, IconOnly, FullWidth, Disabled, Pending, RenderProps }; export default meta; diff --git a/packages/react/src/components/button/button.test.tsx b/packages/react/src/components/button/button.test.tsx index 928ab9c..e800064 100644 --- a/packages/react/src/components/button/button.test.tsx +++ b/packages/react/src/components/button/button.test.tsx @@ -103,16 +103,6 @@ describe("Button", () => { expect(ref.current).toBeInstanceOf(HTMLButtonElement); }); - it("applies shadow class when isShadow is true", () => { - render(); - expect(screen.getByRole("button").className).toContain("fri-button--shadow"); - }); - - it("does not apply shadow class when isShadow is false", () => { - render(); - expect(screen.getByRole("button").className).not.toContain("fri-button--shadow"); - }); - it("applies icon-only class", () => { render(
-
); @@ -314,7 +312,7 @@ const SurfaceVariants = (): React.JSX.Element => { ); }; -const SurfaceRadii = (): React.JSX.Element => { +const SurfaceRadius = (): React.JSX.Element => { return (
@@ -336,10 +334,6 @@ const SurfaceRadii = (): React.JSX.Element => { xl - - - full -
); }; @@ -360,12 +354,12 @@ export { ButtonFullWidth, ButtonIconOnly, ButtonPending, - ButtonRadii, + ButtonRadius, ButtonSizes, ButtonVariants, ButtonWithIcons, SurfaceBadgeLinks, SurfaceDemo, - SurfaceRadii, + SurfaceRadius, SurfaceVariants, }; diff --git a/apps/docs/src/content/docs/components/badge.mdx b/apps/docs/src/content/docs/components/badge.mdx index fe3b695..cc2dc28 100644 --- a/apps/docs/src/content/docs/components/badge.mdx +++ b/apps/docs/src/content/docs/components/badge.mdx @@ -34,7 +34,6 @@ The `variant` prop controls the visual style using semantic color tokens. ```tsx Primary Secondary -Ghost Outline Success Warning @@ -108,14 +107,14 @@ The `size` prop controls padding, gap, and font-size. ### Badge Props -| Prop | Type | Default | Description | -| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------- | ------------------------------------- | -| `variant` | `"primary"` \| `"secondary"` \| `"ghost"` \| `"outline"` \| `"success"` \| `"warning"` \| `"danger"` \| `"info"` \| `"primary-soft"` \| `"secondary-soft"` \| `"success-soft"` \| `"warning-soft"` \| `"danger-soft"` \| `"info-soft"` \| `"primary-outline"` \| `"secondary-outline"` \| `"success-outline"` \| `"warning-outline"` \| `"danger-outline"` \| `"info-outline"` | `"primary"` | The visual variant of the badge | -| `size` | `"sm"` \| `"md"` \| `"lg"` | `"md"` | The size of the badge | -| `isIconOnly` | `boolean` | `false` | Square badge for icon-only indicators | -| `className` | `string` | - | Additional CSS classes | -| `ref` | `Ref` | - | Forwarded ref to the span | -| `children` | `ReactNode` | - | Badge content | +| Prop | Type | Default | Description | +| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | ------------------------------------- | +| `variant` | `"primary"` \| `"secondary"` \| `"outline"` \| `"success"` \| `"warning"` \| `"danger"` \| `"info"` \| `"primary-soft"` \| `"secondary-soft"` \| `"success-soft"` \| `"warning-soft"` \| `"danger-soft"` \| `"info-soft"` \| `"primary-outline"` \| `"secondary-outline"` \| `"success-outline"` \| `"warning-outline"` \| `"danger-outline"` \| `"info-outline"` | `"primary"` | The visual variant of the badge | +| `size` | `"sm"` \| `"md"` \| `"lg"` | `"md"` | The size of the badge | +| `isIconOnly` | `boolean` | `false` | Square badge for icon-only indicators | +| `className` | `string` | - | Additional CSS classes | +| `ref` | `Ref` | - | Forwarded ref to the span | +| `children` | `ReactNode` | - | Badge content | ### CSS Classes @@ -124,7 +123,6 @@ The `size` prop controls padding, gap, and font-size. | `fri-badge` | Base styles | | `fri-badge--primary` | Primary variant | | `fri-badge--secondary` | Secondary variant | -| `fri-badge--ghost` | Ghost variant | | `fri-badge--outline` | Outline variant | | `fri-badge--success` | Success variant | | `fri-badge--warning` | Warning variant | diff --git a/apps/docs/src/content/docs/components/button.mdx b/apps/docs/src/content/docs/components/button.mdx index 327eea3..5019383 100644 --- a/apps/docs/src/content/docs/components/button.mdx +++ b/apps/docs/src/content/docs/components/button.mdx @@ -80,7 +80,7 @@ The `size` prop controls padding, gap, and font-size using golden ratio proporti The `radius` prop controls border-radius independently from size. - + ```tsx @@ -88,7 +88,6 @@ The `radius` prop controls border-radius independently from size. - ``` @@ -235,7 +234,7 @@ Built on [React Aria Button](https://react-spectrum.adobe.com/react-aria/Button. | ------------------ | -------------------------------------------------------------------------------------------------- | ----------- | ------------------------------------------ | | `variant` | `"primary" \| "secondary" \| "ghost" \| "outline" \| "success" \| "warning" \| "danger" \| "info"` | `"primary"` | The visual variant of the button | | `size` | `"sm" \| "md" \| "lg" \| "xl"` | `"md"` | The size of the button | -| `radius` | `"none" \| "sm" \| "md" \| "lg" \| "xl" \| "full"` | `"md"` | The border radius of the button | +| `radius` | `"none" \| "sm" \| "md" \| "lg" \| "full"` | `"md"` | The border radius of the button | | `isIconOnly` | `boolean` | `false` | Whether the button is icon-only (square) | | `isFullWidth` | `boolean` | `false` | Whether the button takes full width | | `isDisabled` | `boolean` | `false` | Whether the button is disabled | @@ -295,7 +294,6 @@ Built on [React Aria Button](https://react-spectrum.adobe.com/react-aria/Button. | `fri-button--radius-sm` | Small border radius | | `fri-button--radius-md` | Medium border radius | | `fri-button--radius-lg` | Large border radius | -| `fri-button--radius-xl` | Extra large border radius | | `fri-button--radius-full` | Full (pill) border radius | | `fri-button--disabled` | Disabled state | | `fri-button--full-width` | Full width button | diff --git a/apps/docs/src/content/docs/components/surface.mdx b/apps/docs/src/content/docs/components/surface.mdx index 4c1ba42..00233b2 100644 --- a/apps/docs/src/content/docs/components/surface.mdx +++ b/apps/docs/src/content/docs/components/surface.mdx @@ -44,7 +44,7 @@ The `variant` prop controls the visual style of the surface. The `radius` prop controls border-radius. - + ```tsx @@ -53,20 +53,19 @@ The `radius` prop controls border-radius. md lg xl -full ``` ## API Reference ### Surface Props -| Prop | Type | Default | Description | -| ----------- | ------------------------------------------------------------ | ----------- | ------------------------ | -| `variant` | `"default"` \| `"bordered"` \| `"glass"` \| `"primary"` | `"default"` | The visual variant | -| `radius` | `"none"` \| `"sm"` \| `"md"` \| `"lg"` \| `"xl"` \| `"full"` | `"md"` | Border radius | -| `className` | `string` | - | Additional CSS classes | -| `ref` | `Ref` | - | Forwarded ref to the div | -| `children` | `ReactNode` | - | Surface content | +| Prop | Type | Default | Description | +| ----------- | ------------------------------------------------------- | ----------- | ------------------------ | +| `variant` | `"default"` \| `"bordered"` \| `"glass"` \| `"primary"` | `"default"` | The visual variant | +| `radius` | `"none"` \| `"sm"` \| `"md"` \| `"lg"` \| `"xl"` | `"md"` | Border radius | +| `className` | `string` | - | Additional CSS classes | +| `ref` | `Ref` | - | Forwarded ref to the div | +| `children` | `ReactNode` | - | Surface content | ### CSS Classes @@ -82,4 +81,3 @@ The `radius` prop controls border-radius. | `fri-surface--radius-md` | Medium border radius | | `fri-surface--radius-lg` | Large border radius | | `fri-surface--radius-xl` | Extra large radius | -| `fri-surface--radius-full` | Full (pill) radius | diff --git a/packages/react/src/components/badge/Badge.tsx b/packages/react/src/components/badge/Badge.tsx index bc3436c..4fbe164 100644 --- a/packages/react/src/components/badge/Badge.tsx +++ b/packages/react/src/components/badge/Badge.tsx @@ -14,7 +14,7 @@ export type BadgeProps = Omit, "className"> & }; const Badge = (props: Readonly) => { - const { children, className, isIconOnly, ref, size, variant, ...rest } = props; + const { children, className, isIconOnly, radius, ref, size, variant, ...rest } = props; const badgeClassName = clsx( bem({ @@ -22,6 +22,7 @@ const Badge = (props: Readonly) => { modifiers: { variant, size, + radius, "icon-only": isIconOnly, }, }), diff --git a/packages/react/src/components/badge/badge.stories.tsx b/packages/react/src/components/badge/badge.stories.tsx index f8fd8fa..6b590c9 100644 --- a/packages/react/src/components/badge/badge.stories.tsx +++ b/packages/react/src/components/badge/badge.stories.tsx @@ -31,7 +31,6 @@ const meta = { options: [ "primary", "secondary", - "ghost", "outline", "success", "warning", @@ -54,7 +53,7 @@ const meta = { table: { type: { summary: - '"primary" | "secondary" | "ghost" | "outline" | "success" | "warning" | "danger" | "info" | "primary-soft" | "secondary-soft" | "success-soft" | "warning-soft" | "danger-soft" | "info-soft" | "primary-outline" | "secondary-outline" | "success-outline" | "warning-outline" | "danger-outline" | "info-outline"', + '"primary" | "secondary" | "outline" | "success" | "warning" | "danger" | "info" | "primary-soft" | "secondary-soft" | "success-soft" | "warning-soft" | "danger-soft" | "info-soft" | "primary-outline" | "secondary-outline" | "success-outline" | "warning-outline" | "danger-outline" | "info-outline"', }, defaultValue: { summary: "primary", @@ -76,6 +75,20 @@ const meta = { category: "Style Variants", }, }, + radius: { + control: "select", + options: ["none", "sm", "md", "lg", "full"], + description: "The border radius of the badge", + table: { + type: { + summary: '"none" | "sm" | "md" | "lg" | "full"', + }, + defaultValue: { + summary: "full", + }, + category: "Style Variants", + }, + }, isIconOnly: { control: "boolean", description: "Whether the badge is icon-only (square)", @@ -126,27 +139,14 @@ const Variants: Story = { const Variants = () => { return ( -
+
Primary Secondary - Ghost Outline Success Warning Danger Info - Primary Soft - Secondary Soft - Success Soft - Warning Soft - Danger Soft - Info Soft - Primary Outline - Secondary Outline - Success Outline - Warning Outline - Danger Outline - Info Outline
); };`, @@ -163,10 +163,6 @@ const Variants = () => { Secondary - - Ghost - - Outline @@ -186,53 +182,150 @@ const Variants = () => { Info +
+ ), +}; + +const SoftVariants: Story = { + parameters: { + docs: { + source: { + code: `import { Badge } from "@fried-ui/react"; +const SoftVariants = () => { + return ( +
+ Primary + Secondary + Success + Warning + Danger + Info +
+ ); +};`, + }, + }, + }, + render: (args): React.JSX.Element => ( +
- Primary Soft + Primary - Secondary Soft + Secondary - Success Soft + Success - Warning Soft + Warning - Danger Soft + Danger - Info Soft + Info +
+ ), +}; +const OutlineVariants: Story = { + parameters: { + docs: { + source: { + code: `import { Badge } from "@fried-ui/react"; + +const OutlineVariants = () => { + return ( +
+ Primary + Secondary + Success + Warning + Danger + Info +
+ ); +};`, + }, + }, + }, + render: (args): React.JSX.Element => ( +
- Primary Outline + Primary - Secondary Outline + Secondary - Success Outline + Success - Warning Outline + Warning - Danger Outline + Danger - Info Outline + Info + +
+ ), +}; + +const Radius: Story = { + parameters: { + docs: { + source: { + code: `import { Badge } from "@fried-ui/react"; + +const Radius = () => { + return ( +
+ None + Small + Medium + Large + Full +
+ ); +};`, + }, + }, + }, + render: (args): React.JSX.Element => ( +
+ + None + + + + Small + + + + Medium + + + + Large + + + + Full
), @@ -281,19 +374,22 @@ const WithIcon: Story = { const WithIcon = () => { return ( - - - Settings - - ); -}; +
+ + + Settings + -const WithIconAlt = () => { - return ( - - - Settings - + + + Active + + + + + Error + +
); };`, }, @@ -327,81 +423,41 @@ const IconOnly: Story = { const IconOnly = () => { return ( - - - - ); -};`, - }, - }, - }, - render: (args): React.JSX.Element => (
- + - + - +
- ), -}; - -const SoftVariants: Story = { - parameters: { - docs: { - source: { - code: `import { Badge } from "@fried-ui/react"; - -const SoftVariants = () => { - return ( -
- Primary - Secondary - Success - Warning - Danger - Info -
); };`, }, }, }, render: (args): React.JSX.Element => ( -
- - Primary - - - - Secondary - - - - Success - - - - Warning +
+ + - - Danger + + - - Info + +
), }; -export { Default, Variants, SoftVariants, Sizes, WithIcon, IconOnly }; +export { Default, Variants, SoftVariants, OutlineVariants, Radius, Sizes, WithIcon, IconOnly }; export default meta; diff --git a/packages/react/src/components/badge/badge.test.tsx b/packages/react/src/components/badge/badge.test.tsx index 9a409e5..06ff58f 100644 --- a/packages/react/src/components/badge/badge.test.tsx +++ b/packages/react/src/components/badge/badge.test.tsx @@ -30,7 +30,6 @@ describe("Badge", () => { const variants = [ "primary", "secondary", - "ghost", "outline", "success", "warning", @@ -67,6 +66,16 @@ describe("Badge", () => { }); }); + it("applies radius class with key-value format", () => { + const radiusValues = ["none", "sm", "md", "lg", "full"] as const; + + radiusValues.forEach((radius) => { + const { unmount } = render({radius}); + expect(screen.getByText(radius).className).toContain(`fri-badge--radius-${radius}`); + unmount(); + }); + }); + it("applies icon-only class", () => { render(X); expect(screen.getByText("X").className).toContain("fri-badge--icon-only"); diff --git a/packages/react/src/components/badge/badge.variants.ts b/packages/react/src/components/badge/badge.variants.ts index ae2a0b5..d77b9ea 100644 --- a/packages/react/src/components/badge/badge.variants.ts +++ b/packages/react/src/components/badge/badge.variants.ts @@ -1,10 +1,10 @@ export interface BadgeVariantsProps { isIconOnly?: boolean; + radius?: "none" | "sm" | "md" | "lg" | "full"; size?: "sm" | "md" | "lg"; variant?: | "primary" | "secondary" - | "ghost" | "outline" | "success" | "warning" diff --git a/packages/react/src/components/button/button.stories.tsx b/packages/react/src/components/button/button.stories.tsx index 62b8c7c..7f02f3d 100644 --- a/packages/react/src/components/button/button.stories.tsx +++ b/packages/react/src/components/button/button.stories.tsx @@ -57,11 +57,11 @@ const meta = { }, radius: { control: "select", - options: ["none", "sm", "md", "lg", "xl", "full"], + options: ["none", "sm", "md", "lg", "full"], description: "The border radius of the button", table: { type: { - summary: '"none" | "sm" | "md" | "lg" | "xl" | "full"', + summary: '"none" | "sm" | "md" | "lg" | "full"', }, defaultValue: { summary: "md", @@ -168,7 +168,7 @@ const Variants: Story = { const Variants = () => { return ( -
+
@@ -260,20 +260,19 @@ const Sizes = () => { ), }; -const Radii: Story = { +const Radius: Story = { parameters: { docs: { source: { code: `import { Button } from "@fried-ui/react"; -const Radii = () => { +const Radius = () => { return (
-
); @@ -299,10 +298,6 @@ const Radii = () => { Large - - @@ -318,19 +313,22 @@ const WithIcon: Story = { const WithIcon = () => { return ( - - ); -}; +
+ -const WithIconAlt = () => { - return ( - + + + +
); };`, }, @@ -360,21 +358,27 @@ const IconOnly: Story = { parameters: { docs: { source: { - code: `import { Button, SettingsIcon } from "@fried-ui/react"; + code: `import { Button, MoreIcon, PlusIcon, SettingsIcon, ShareIcon } from "@fried-ui/react"; const IconOnly = () => { return ( - - ); -}; +
+ -const IconOnlyAlt = () => { - return ( - + + + + + +
); };`, }, @@ -408,7 +412,19 @@ const FullWidth: Story = { code: `import { Button } from "@fried-ui/react"; const FullWidth = () => { - return ; + return ( +
+ + + + + +
+ ); };`, }, }, @@ -487,6 +503,6 @@ const RenderProps = () => { ), }; -export { Default, Variants, Sizes, Radii, WithIcon, IconOnly, FullWidth, Disabled, Pending, RenderProps }; +export { Default, Variants, Sizes, Radius, WithIcon, IconOnly, FullWidth, Disabled, Pending, RenderProps }; export default meta; diff --git a/packages/react/src/components/button/button.test.tsx b/packages/react/src/components/button/button.test.tsx index e800064..c7674a3 100644 --- a/packages/react/src/components/button/button.test.tsx +++ b/packages/react/src/components/button/button.test.tsx @@ -73,9 +73,9 @@ describe("Button", () => { }); it("applies radius class with key-value format", () => { - const radii = ["none", "sm", "md", "lg", "xl", "full"] as const; + const radiusValues = ["none", "sm", "md", "lg", "full"] as const; - radii.forEach((radius) => { + radiusValues.forEach((radius) => { const { unmount } = render(); expect(screen.getByRole("button").className).toContain(`fri-button--radius-${radius}`); unmount(); diff --git a/packages/react/src/components/button/button.variants.ts b/packages/react/src/components/button/button.variants.ts index 528af49..63fe44e 100644 --- a/packages/react/src/components/button/button.variants.ts +++ b/packages/react/src/components/button/button.variants.ts @@ -1,7 +1,7 @@ export interface ButtonVariantsProps { isFullWidth?: boolean; isIconOnly?: boolean; - radius?: "none" | "sm" | "md" | "lg" | "xl" | "full"; + radius?: "none" | "sm" | "md" | "lg" | "full"; size?: "sm" | "md" | "lg" | "xl"; variant?: "primary" | "secondary" | "ghost" | "outline" | "success" | "warning" | "danger" | "info"; } diff --git a/packages/react/src/components/surface/surface.stories.tsx b/packages/react/src/components/surface/surface.stories.tsx index 7187361..1d5587f 100644 --- a/packages/react/src/components/surface/surface.stories.tsx +++ b/packages/react/src/components/surface/surface.stories.tsx @@ -39,11 +39,11 @@ const meta = { }, radius: { control: "select", - options: ["none", "sm", "md", "lg", "xl", "full"], + options: ["none", "sm", "md", "lg", "xl"], description: "Border radius", table: { type: { - summary: '"none" | "sm" | "md" | "lg" | "xl" | "full"', + summary: '"none" | "sm" | "md" | "lg" | "xl"', }, defaultValue: { summary: "md", @@ -74,9 +74,9 @@ const Default: Story = { const Card = () => { return ( - -

Card Title

-

Content goes here.

+ +

Card Title

+

Content goes here.

); };`, @@ -145,7 +145,6 @@ const Radius = () => { md lg xl - full
); };`, @@ -173,10 +172,6 @@ const Radius = () => { xl - - - full -
), }; @@ -189,10 +184,10 @@ const Glass: Story = { const Glass = () => { return ( -
- background - - Content over image +
+ +

Glass Surface

+

Content over a gradient background.

); @@ -201,7 +196,7 @@ const Glass = () => { }, }, render: (args): React.JSX.Element => ( -
+

Glass Surface

Content over a gradient background.

diff --git a/packages/react/src/components/surface/surface.test.tsx b/packages/react/src/components/surface/surface.test.tsx index c2be7bb..f77019f 100644 --- a/packages/react/src/components/surface/surface.test.tsx +++ b/packages/react/src/components/surface/surface.test.tsx @@ -27,9 +27,9 @@ describe("Surface", () => { }); it("applies radius class with key-value format", () => { - const radii = ["none", "sm", "md", "lg", "xl", "full"] as const; + const radiusValues = ["none", "sm", "md", "lg", "xl"] as const; - radii.forEach((radius) => { + radiusValues.forEach((radius) => { const { unmount } = render({radius}); expect(screen.getByText(radius).className).toContain(`fri-surface--radius-${radius}`); unmount(); diff --git a/packages/react/src/components/surface/surface.variants.ts b/packages/react/src/components/surface/surface.variants.ts index 6f6fe1f..310d8f4 100644 --- a/packages/react/src/components/surface/surface.variants.ts +++ b/packages/react/src/components/surface/surface.variants.ts @@ -1,4 +1,4 @@ export interface SurfaceVariantsProps { - radius?: "none" | "sm" | "md" | "lg" | "xl" | "full"; + radius?: "none" | "sm" | "md" | "lg" | "xl"; variant?: "default" | "bordered" | "glass" | "primary"; } diff --git a/packages/styles/src/components/badge.css b/packages/styles/src/components/badge.css index 410a954..42babb0 100644 --- a/packages/styles/src/components/badge.css +++ b/packages/styles/src/components/badge.css @@ -7,7 +7,7 @@ --fri-badge-fg: var(--color-primary-foreground); @apply bg-(--fri-badge-bg) text-(--fri-badge-fg); - @apply gap-[calc(0.875rem/1.618)] px-[calc(0.875rem)] py-[calc(0.875rem/2.058)] text-sm; + @apply gap-[calc(0.875rem/2.058)] px-[calc(0.875rem)] py-[calc(0.875rem/2.058)] text-sm; @apply rounded-full; } @@ -21,11 +21,6 @@ --fri-badge-fg: var(--color-secondary-foreground); } -.fri-badge--ghost { - --fri-badge-bg: transparent; - --fri-badge-fg: var(--color-foreground); -} - .fri-badge--outline { --fri-badge-bg: transparent; --fri-badge-fg: var(--color-foreground); @@ -137,6 +132,26 @@ @apply border border-info; } +.fri-badge--radius-none { + @apply rounded-none; +} + +.fri-badge--radius-sm { + @apply rounded-[calc(1em*1.272/4)]; +} + +.fri-badge--radius-md { + @apply rounded-[calc(1em*1.272*2/4)]; +} + +.fri-badge--radius-lg { + @apply rounded-[calc(1em*1.272*3/4)]; +} + +.fri-badge--radius-full { + @apply rounded-full; +} + .fri-badge [slot="icon"], .fri-badge [slot="icon-start"], .fri-badge [slot="icon-end"] { @@ -152,7 +167,7 @@ } .fri-badge--size-sm { - @apply gap-[calc(0.75rem/1.618)] px-[calc(0.75rem)] py-[calc(0.75rem/2.058)] text-xs; + @apply gap-[calc(0.75rem/2.058)] px-[calc(0.75rem)] py-[calc(0.75rem/2.058)] text-xs; } .fri-badge--size-sm:has([slot="icon-start"]) { @@ -164,7 +179,7 @@ } .fri-badge--size-md { - @apply gap-[calc(0.875rem/1.618)] px-[calc(0.875rem)] py-[calc(0.875rem/2.058)] text-sm; + @apply gap-[calc(0.875rem/2.058)] px-[calc(0.875rem)] py-[calc(0.875rem/2.058)] text-sm; } .fri-badge--size-md:has([slot="icon-start"]) { @@ -176,7 +191,7 @@ } .fri-badge--size-lg { - @apply gap-[calc(1rem/1.618)] px-[calc(1rem)] py-[calc(1rem/2.058)] text-base; + @apply gap-[calc(1rem/2.058)] px-[calc(1rem)] py-[calc(1rem/2.058)] text-base; } .fri-badge--size-lg:has([slot="icon-start"]) { diff --git a/packages/styles/src/components/button.css b/packages/styles/src/components/button.css index b405a46..5b794d5 100644 --- a/packages/styles/src/components/button.css +++ b/packages/styles/src/components/button.css @@ -10,7 +10,7 @@ --fri-button-fg: var(--color-primary-foreground); @apply bg-(--fri-button-bg) text-(--fri-button-fg); - @apply gap-[calc(1rem/1.618)] px-[calc(1rem)] py-[calc(1rem/2.058)] text-base; + @apply gap-[calc(1rem/2.058)] px-[calc(1rem)] py-[calc(1rem/2.058)] text-base; @apply rounded-md; &:focus-visible, @@ -98,19 +98,19 @@ } .fri-button--size-sm { - @apply gap-[calc(0.875rem/1.618)] px-[calc(0.875rem)] py-[calc(0.875rem/2.058)] text-sm; + @apply gap-[calc(0.875rem/2.058)] px-[calc(0.875rem)] py-[calc(0.875rem/2.058)] text-sm; } .fri-button--size-md { - @apply gap-[calc(1rem/1.618)] px-[calc(1rem)] py-[calc(1rem/2.058)] text-base; + @apply gap-[calc(1rem/2.058)] px-[calc(1rem)] py-[calc(1rem/2.058)] text-base; } .fri-button--size-lg { - @apply gap-[calc(1.25rem/1.618)] px-[calc(1.25rem)] py-[calc(1.25rem/2.058)] text-xl; + @apply gap-[calc(1.25rem/2.058)] px-[calc(1.25rem)] py-[calc(1.25rem/2.058)] text-xl; } .fri-button--size-xl { - @apply gap-[calc(1.5rem/1.618)] px-[calc(1.5rem)] py-[calc(1.5rem/2.058)] text-2xl; + @apply gap-[calc(1.5rem/2.058)] px-[calc(1.5rem)] py-[calc(1.5rem/2.058)] text-2xl; } .fri-button--full-width { @@ -138,19 +138,15 @@ } .fri-button--radius-sm { - @apply rounded-sm; + @apply rounded-[calc(1em*1.272/4)]; } .fri-button--radius-md { - @apply rounded-md; + @apply rounded-[calc(1em*1.272*2/4)]; } .fri-button--radius-lg { - @apply rounded-lg; -} - -.fri-button--radius-xl { - @apply rounded-xl; + @apply rounded-[calc(1em*1.272*3/4)]; } .fri-button--radius-full { diff --git a/packages/styles/src/components/surface.css b/packages/styles/src/components/surface.css index d39d3e6..011f355 100644 --- a/packages/styles/src/components/surface.css +++ b/packages/styles/src/components/surface.css @@ -24,21 +24,17 @@ } .fri-surface--radius-sm { - @apply rounded-sm; + @apply rounded-[calc(1em*1.272/4)]; } .fri-surface--radius-md { - @apply rounded-md; + @apply rounded-[calc(1em*1.272*2/4)]; } .fri-surface--radius-lg { - @apply rounded-lg; + @apply rounded-[calc(1em*1.272*3/4)]; } .fri-surface--radius-xl { - @apply rounded-xl; -} - -.fri-surface--radius-full { - @apply rounded-full; + @apply rounded-[calc(1em*1.272)]; } From ceb5ac952bb00b14ea6647f9be6649d33d271e1a Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Sun, 19 Apr 2026 04:01:32 +0700 Subject: [PATCH 064/199] feat(react): Revamp Surface with variants and shadow - Add 7 variants: default, default-bordered, plain, plain-bordered, glass, frost, overlay - Add orthogonal shadow prop (none/sm/md/lg/xl) - Add dedicated surface tokens for designer override (plain, plain-border) - Reserve 1px transparent border on base to prevent height shift - Apply Apple liquid glass blur (blur-xl/2xl + saturate-200) - Sync Badge/Button/Surface glass/frost/overlay gradients - Reorder stories for consistency (declaration === export) - Fix lint errors across icons and Badge role attribute Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/docs/public/llm/badge.md | 113 ++--- apps/docs/public/llm/button.md | 149 ++++-- .../src/content/docs/components/surface.mdx | 47 +- apps/docs/src/content/docs/styles/colors.mdx | 38 +- .../src/content/docs/styles/design-tokens.mdx | 40 +- apps/storybook/.storybook/main.ts | 2 +- apps/storybook/.storybook/preview.ts | 9 + apps/storybook/.storybook/styles.css | 8 + apps/storybook/package.json | 1 + packages/react/src/components/badge/Badge.tsx | 4 +- .../src/components/badge/badge.stories.tsx | 378 +++++++++++--- .../react/src/components/badge/badge.test.tsx | 12 + .../src/components/badge/badge.variants.ts | 13 + .../src/components/button/button.stories.tsx | 423 +++++++++++++++- .../src/components/button/button.test.tsx | 43 +- .../src/components/button/button.variants.ts | 42 +- .../src/components/icons/ArrowRightIcon.tsx | 10 +- .../react/src/components/icons/BellIcon.tsx | 26 + .../src/components/icons/CheckCircleIcon.tsx | 26 + .../src/components/icons/ClipboardIcon.tsx | 19 +- .../icons/ExclamationTriangleIcon.tsx | 27 + .../react/src/components/icons/GitHubIcon.tsx | 16 +- .../react/src/components/icons/HeartIcon.tsx | 25 + .../icons/InformationCircleIcon.tsx | 16 +- .../react/src/components/icons/LockIcon.tsx | 27 + .../react/src/components/icons/MoreIcon.tsx | 16 +- .../react/src/components/icons/PlusIcon.tsx | 11 +- .../src/components/icons/ReactAriaIcon.tsx | 16 +- .../src/components/icons/SettingsIcon.tsx | 17 +- .../react/src/components/icons/ShareIcon.tsx | 18 +- .../react/src/components/icons/Spinner.tsx | 41 +- .../react/src/components/icons/StarIcon.tsx | 25 + .../src/components/icons/StorybookIcon.tsx | 16 +- .../src/components/icons/XCircleIcon.tsx | 26 + packages/react/src/components/icons/index.ts | 7 + .../react/src/components/surface/Surface.tsx | 3 +- .../components/surface/surface.stories.tsx | 283 +++++++++-- .../src/components/surface/surface.test.tsx | 19 +- .../components/surface/surface.variants.ts | 3 +- packages/styles/src/components/badge.css | 167 +++++-- packages/styles/src/components/button.css | 279 ++++++++++- packages/styles/src/components/surface.css | 43 +- packages/styles/src/tokens/colors.css | 460 ++++++++++-------- packages/styles/src/tokens/index.css | 2 +- packages/styles/src/tokens/layout.css | 2 +- packages/styles/src/tokens/motion.css | 18 +- packages/styles/src/tokens/palette.css | 73 +++ packages/styles/src/tokens/typography.css | 8 - pnpm-lock.yaml | 20 + .../interactive-component/component.css.hbs | 2 +- 50 files changed, 2434 insertions(+), 655 deletions(-) create mode 100644 packages/react/src/components/icons/BellIcon.tsx create mode 100644 packages/react/src/components/icons/CheckCircleIcon.tsx create mode 100644 packages/react/src/components/icons/ExclamationTriangleIcon.tsx create mode 100644 packages/react/src/components/icons/HeartIcon.tsx create mode 100644 packages/react/src/components/icons/LockIcon.tsx create mode 100644 packages/react/src/components/icons/StarIcon.tsx create mode 100644 packages/react/src/components/icons/XCircleIcon.tsx create mode 100644 packages/styles/src/tokens/palette.css delete mode 100644 packages/styles/src/tokens/typography.css diff --git a/apps/docs/public/llm/badge.md b/apps/docs/public/llm/badge.md index 5378578..b4a8625 100644 --- a/apps/docs/public/llm/badge.md +++ b/apps/docs/public/llm/badge.md @@ -16,43 +16,26 @@ import { Badge } from "@fried-ui/react"; ## Props -| Prop | Type | Default | Description | -| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | ------------------------------------- | -| `variant` | `"primary"` \| `"secondary"` \| `"outline"` \| `"success"` \| `"warning"` \| `"danger"` \| `"info"` \| `"primary-soft"` \| `"secondary-soft"` \| `"success-soft"` \| `"warning-soft"` \| `"danger-soft"` \| `"info-soft"` \| `"primary-outline"` \| `"secondary-outline"` \| `"success-outline"` \| `"warning-outline"` \| `"danger-outline"` \| `"info-outline"` | `"primary"` | Visual variant | -| `size` | `"sm"` \| `"md"` \| `"lg"` | `"md"` | Size — controls padding, font | -| `isIconOnly` | `boolean` | `false` | Square badge for icon-only indicators | -| `className` | `string` | - | Additional CSS classes | -| `ref` | `Ref` | - | Forwarded ref to span element | -| `children` | `ReactNode` | - | Badge content | +| Prop | Type | Default | Description | +| ------------ | -------------------------------------------------- | ----------- | ------------------------------------- | +| `variant` | `BadgeVariant` (see Variants) | `"primary"` | Visual variant (32 options) | +| `size` | `"sm"` \| `"md"` \| `"lg"` | `"md"` | Size — controls padding, font | +| `radius` | `"none"` \| `"sm"` \| `"md"` \| `"lg"` \| `"full"` | `"full"` | Border radius (pill by default) | +| `isIconOnly` | `boolean` | `false` | Square badge for icon-only indicators | +| `aria-label` | `string` | - | Required for icon-only badges | +| `className` | `string` | - | Additional CSS classes | +| `ref` | `Ref` | - | Forwarded ref to span element | +| `children` | `ReactNode` | - | Badge content | ## CSS Classes -| Class | Description | -| ------------------------------ | ------------------------- | -| `fri-badge` | Base styles | -| `fri-badge--primary` | Primary variant | -| `fri-badge--secondary` | Secondary variant | -| `fri-badge--outline` | Outline variant | -| `fri-badge--success` | Success variant | -| `fri-badge--warning` | Warning variant | -| `fri-badge--danger` | Danger variant | -| `fri-badge--info` | Info variant | -| `fri-badge--primary-soft` | Primary soft variant | -| `fri-badge--secondary-soft` | Secondary soft variant | -| `fri-badge--success-soft` | Success soft variant | -| `fri-badge--warning-soft` | Warning soft variant | -| `fri-badge--danger-soft` | Danger soft variant | -| `fri-badge--info-soft` | Info soft variant | -| `fri-badge--primary-outline` | Primary outline variant | -| `fri-badge--secondary-outline` | Secondary outline variant | -| `fri-badge--success-outline` | Success outline variant | -| `fri-badge--warning-outline` | Warning outline variant | -| `fri-badge--danger-outline` | Danger outline variant | -| `fri-badge--info-outline` | Info outline variant | -| `fri-badge--size-sm` | Small size | -| `fri-badge--size-md` | Medium size | -| `fri-badge--size-lg` | Large size | -| `fri-badge--icon-only` | Icon-only square | +| Class | Description | +| --------------------------- | ----------------------------------------- | +| `fri-badge` | Base styles | +| `fri-badge--{variant}` | Variant class (see 32 variants) | +| `fri-badge--size-{size}` | Size (`sm` \| `md` \| `lg`) | +| `fri-badge--radius-{value}` | Radius (`none \| sm \| md \| lg \| full`) | +| `fri-badge--icon-only` | Icon-only square | ## CSS Variables @@ -63,36 +46,54 @@ import { Badge } from "@fried-ui/react"; ## Examples -### Solid Variants +### Variants + +**32 variants** — 11 base + 21 color-prefixed (3 modifiers × 7 colors). + +**Base variants:** ```tsx -Primary -Secondary -Success -Warning -Danger -Info +Primary {/* dark neutral, main label */} +Secondary {/* light neutral, less emphasis */} +Accent {/* purple, brand highlight */} +Outlined {/* neutral bordered */} +Glass {/* frosted + border (on image) */} +Frost {/* frosted no border (on image) */} +Overlay {/* dark scrim (on media) */} +Success {/* green status */} +Warning {/* yellow status */} +Danger {/* red status */} +Info {/* blue status */} ``` -### Soft Variants - -Pale background with colored text and border. +**Color-prefixed variants** — combine `{color}` with `-soft`, `-flat`, or `-outline`: ```tsx -Primary -Success -Danger +{/* -soft: pale bg + visible border (status tags) */} +Draft +Rejected + +{/* -flat: pale bg no border (minimal subtle) */} +Featured + +{/* -outline: transparent + colored border (emphasized label) */} +Pro +Expired ``` -### Colored Outline Variants +**Colors:** `primary` `secondary` `accent` `success` `warning` `danger` `info` +**Modifiers:** `-soft` `-flat` `-outline` -Transparent background with colored border and text. +**When to use:** -```tsx -Primary -Success -Danger -``` +- `primary` — main label (counts, category) +- `accent` — brand feature (new, pro, featured) +- `outline` — subtle label next to primary content +- `success/warning/danger/info` — status indicators +- `{color}-soft` — status tags with border emphasis +- `{color}-flat` — minimal status tags +- `{color}-outline` — emphasized status labels +- `glass/frost/overlay` — badges on images or media ### Icon Only @@ -128,9 +129,9 @@ Square badge for icon-only indicators. Always provide `aria-label`. ## Constraints -- 20 variants: 8 solid + 6 soft + 6 colored outline +- 32 variants: 11 base (primary, secondary, accent, outline, glass, frost, overlay, success, warning, danger, info) + 21 color-prefixed (7 colors × 3 modifiers: -soft, -flat, -outline; no -ghost — display-only) - 3 sizes: sm, md, lg -- Fixed `rounded-full` border radius (pill shape) +- 5 radius options: `none`, `sm`, `md`, `lg`, `full` (default `full` — pill shape) - Non-interactive — no hover, focus, or click states - BEM classes (`fri-badge`, `fri-badge--primary-soft`, etc.) - Shadow uses em units — scales with component font-size diff --git a/apps/docs/public/llm/button.md b/apps/docs/public/llm/button.md index 02a6701..2bd01d2 100644 --- a/apps/docs/public/llm/button.md +++ b/apps/docs/public/llm/button.md @@ -16,37 +16,65 @@ import { Button } from "@fried-ui/react"; ## Props -| Prop | Type | Default | Description | -| ------------------ | -------------------------------------------------------------------------------------------------- | ----------- | --------------------------------------- | -| `variant` | `"primary" \| "secondary" \| "ghost" \| "outline" \| "success" \| "warning" \| "danger" \| "info"` | `"primary"` | Visual variant | -| `size` | `"sm" \| "md" \| "lg" \| "xl"` | `"md"` | Size — controls padding, gap, font-size | -| `radius` | `"none" \| "sm" \| "md" \| "lg" \| "full"` | `"md"` | Border radius (independent from size) | -| `isIconOnly` | `boolean` | `false` | Icon-only square button | -| `isFullWidth` | `boolean` | `false` | Full width button | -| `isDisabled` | `boolean` | `false` | Disabled state | -| `isPending` | `boolean` | `false` | Loading spinner, disables interaction | -| `onPress` | `(e: PressEvent) => void` | - | Press handler (preferred over onClick) | -| `onPressStart` | `(e: PressEvent) => void` | - | Press start handler | -| `onPressEnd` | `(e: PressEvent) => void` | - | Press end handler | -| `onPressChange` | `(isPressed: boolean) => void` | - | Press state change handler | -| `onHoverStart` | `(e: HoverEvent) => void` | - | Hover start handler | -| `onHoverEnd` | `(e: HoverEvent) => void` | - | Hover end handler | -| `onHoverChange` | `(isHovering: boolean) => void` | - | Hover state change handler | -| `onFocus` | `(e: FocusEvent) => void` | - | Focus handler | -| `onBlur` | `(e: FocusEvent) => void` | - | Blur handler | -| `onFocusChange` | `(isFocused: boolean) => void` | - | Focus state change handler | -| `onKeyDown` | `(e: KeyboardEvent) => void` | - | Key down handler | -| `onKeyUp` | `(e: KeyboardEvent) => void` | - | Key up handler | -| `type` | `"button" \| "submit" \| "reset"` | `"button"` | HTML button type | -| `form` | `string` | - | Associated form ID | -| `formAction` | `string` | - | Form submission URL | -| `autoFocus` | `boolean` | `false` | Auto focus on mount | -| `aria-label` | `string` | - | Accessible label | -| `aria-labelledby` | `string` | - | ID of labelling element | -| `aria-describedby` | `string` | - | ID of describing element | -| `className` | `string` | - | Additional CSS classes | -| `ref` | `Ref` | - | Forwarded ref to button element | -| `children` | `ReactNode \| (renderProps: ButtonRenderProps) => ReactNode` | - | Button content | +| Prop | Type | Default | Description | +| ------------------ | ------------------------------------------------------------ | ----------- | --------------------------------------- | +| `variant` | `ButtonVariant` (see Variants) | `"primary"` | Visual variant (40 options) | +| `size` | `"sm" \| "md" \| "lg" \| "xl"` | `"md"` | Size — controls padding, gap, font-size | +| `radius` | `"none" \| "sm" \| "md" \| "lg" \| "full"` | `"md"` | Border radius (independent from size) | +| `isIconOnly` | `boolean` | `false` | Icon-only square button | +| `isFullWidth` | `boolean` | `false` | Full width button | +| `isDisabled` | `boolean` | `false` | Disabled state | +| `isPending` | `boolean` | `false` | Loading spinner, disables interaction | +| `onPress` | `(e: PressEvent) => void` | - | Press handler (preferred over onClick) | +| `onPressStart` | `(e: PressEvent) => void` | - | Press start handler | +| `onPressEnd` | `(e: PressEvent) => void` | - | Press end handler | +| `onPressChange` | `(isPressed: boolean) => void` | - | Press state change handler | +| `onHoverStart` | `(e: HoverEvent) => void` | - | Hover start handler | +| `onHoverEnd` | `(e: HoverEvent) => void` | - | Hover end handler | +| `onHoverChange` | `(isHovering: boolean) => void` | - | Hover state change handler | +| `onFocus` | `(e: FocusEvent) => void` | - | Focus handler | +| `onBlur` | `(e: FocusEvent) => void` | - | Blur handler | +| `onFocusChange` | `(isFocused: boolean) => void` | - | Focus state change handler | +| `onKeyDown` | `(e: KeyboardEvent) => void` | - | Key down handler | +| `onKeyUp` | `(e: KeyboardEvent) => void` | - | Key up handler | +| `type` | `"button" \| "submit" \| "reset"` | `"button"` | HTML button type | +| `form` | `string` | - | Associated form ID | +| `formAction` | `string` | - | Form submission URL | +| `autoFocus` | `boolean` | `false` | Auto focus on mount | +| `aria-label` | `string` | - | Accessible label | +| `aria-labelledby` | `string` | - | ID of labelling element | +| `aria-describedby` | `string` | - | ID of describing element | +| `className` | `string` | - | Additional CSS classes | +| `ref` | `Ref` | - | Forwarded ref to button element | +| `children` | `ReactNode \| (renderProps: ButtonRenderProps) => ReactNode` | - | Button content | + +## Inherited from React Aria Button + +Additional props forwarded to the underlying React Aria `Button`. Use these when building composite patterns (menu triggers, toggles, form buttons). React DOM events (`onClick`, `onMouseMove`, etc.) are also inherited but omitted here — prefer React Aria handlers like `onPress`. + +| Prop | Type | Default | Description | +| --------------------- | ---------------------------------------------------------------- | ------- | ------------------------------------------------------------------------------- | +| `id` | `string` | - | Unique identifier | +| `name` | `string` | - | Form submission key (paired with `value`) | +| `value` | `string` | - | Form submission value for this button | +| `slot` | `string \| null` | - | React Aria slot name (receive props from parent component) | +| `excludeFromTabOrder` | `boolean` | `false` | Remove from sequential tab order (use only with alternative keyboard access) | +| `preventFocusOnPress` | `boolean` | `false` | Don't move focus on press (ComboBox triggers, NumberField steppers) | +| `onPressUp` | `(e: PressEvent) => void` | - | Called on press release regardless of start target (unlike `onPress`) | +| `formEncType` | `string` | - | Encoding for form data | +| `formMethod` | `string` | - | HTTP method for form submission | +| `formNoValidate` | `boolean` | - | Skip form validation on submit | +| `formTarget` | `string` | - | Override form target attribute | +| `aria-expanded` | `boolean \| "true" \| "false"` | - | Expanded state (menu triggers, accordions) | +| `aria-haspopup` | `boolean \| "menu" \| "listbox" \| "tree" \| "grid" \| "dialog"` | - | Type of popup the button controls | +| `aria-controls` | `string` | - | ID of the element this button controls | +| `aria-pressed` | `boolean \| "true" \| "false" \| "mixed"` | - | Pressed state for toggle buttons | +| `aria-current` | `boolean \| "page" \| "step" \| "location" \| "date" \| "time"` | - | Represents the current item in a set | +| `aria-details` | `string` | - | ID of element providing detailed description | +| `render` | `DOMRenderFunction<"button", ButtonRenderProps>` | - | Override default DOM element (must render ` - - - - - - - + {/* dark neutral, main action */} + {/* light neutral, less emphasis */} + {/* purple, brand highlight */} + {/* transparent, dismissive */} + {/* neutral bordered */} + {/* frosted + border (on image) */} + {/* frosted no border */} + {/* dark scrim (on media) */} + {/* green status */} + {/* yellow status */} + {/* red destructive */} + {/* blue info */} ``` +**Color-prefixed variants** — combine `{color}` with `-soft`, `-flat`, `-outline`, or `-ghost`: + +```tsx +{/* -soft: pale bg + visible border (status tags) */} + + + +{/* -flat: pale bg no border (minimal subtle) */} + + +{/* -outline: transparent + colored border (secondary CTA) */} + + + +{/* -ghost: transparent, reveal on hover (toolbar) */} + + +``` + +**Colors:** `primary` `secondary` `accent` `success` `warning` `danger` `info` +**Modifiers:** `-soft` `-flat` `-outline` `-ghost` + +**When to use:** + +- `primary` — main CTA (save, submit, confirm) +- `accent` — brand feature (upgrade, get started, marketing) +- `ghost` — dismissive (cancel, close, skip) +- `outline` — secondary action next to primary +- `danger` — destructive (delete, remove) +- `{color}-soft/flat` — status labels in content +- `{color}-outline` — secondary variants of colored actions +- `{color}-ghost` — toolbar/icon actions + ### Basic Default button with medium size and medium radius. @@ -282,7 +349,7 @@ Formulas: `padding-inline = x`, `padding-block = x × 0.485`, `gap = x / φ`. ## Constraints -- 8 variants: primary, secondary, ghost, outline, success, warning, danger, info +- 40 variants: 12 base (primary, secondary, accent, ghost, outline, glass, frost, overlay, success, warning, danger, info) + 28 color-prefixed (7 colors × 4 modifiers: -soft, -flat, -outline, -ghost) - `radius` is independent from `size` — they do not affect each other - `isPending` makes text transparent and shows spinner; content stays in DOM for layout - Uses BEM classes (`fri-button`, `fri-button--primary`, etc.) — no inline Tailwind in component diff --git a/apps/docs/src/content/docs/components/surface.mdx b/apps/docs/src/content/docs/components/surface.mdx index 00233b2..0ca0aa4 100644 --- a/apps/docs/src/content/docs/components/surface.mdx +++ b/apps/docs/src/content/docs/components/surface.mdx @@ -59,25 +59,34 @@ The `radius` prop controls border-radius. ### Surface Props -| Prop | Type | Default | Description | -| ----------- | ------------------------------------------------------- | ----------- | ------------------------ | -| `variant` | `"default"` \| `"bordered"` \| `"glass"` \| `"primary"` | `"default"` | The visual variant | -| `radius` | `"none"` \| `"sm"` \| `"md"` \| `"lg"` \| `"xl"` | `"md"` | Border radius | -| `className` | `string` | - | Additional CSS classes | -| `ref` | `Ref` | - | Forwarded ref to the div | -| `children` | `ReactNode` | - | Surface content | +| Prop | Type | Default | Description | +| ----------- | --------------------------------------------------------------------------------------------------------------- | ----------- | ------------------------ | +| `variant` | `"default"` \| `"default-bordered"` \| `"plain"` \| `"plain-bordered"` \| `"glass"` \| `"frost"` \| `"overlay"` | `"default"` | The visual variant | +| `radius` | `"none"` \| `"sm"` \| `"md"` \| `"lg"` \| `"xl"` | `"md"` | Border radius | +| `shadow` | `"none"` \| `"sm"` \| `"md"` \| `"lg"` \| `"xl"` | `"none"` | Elevation shadow | +| `className` | `string` | - | Additional CSS classes | +| `ref` | `Ref` | - | Forwarded ref to the div | +| `children` | `ReactNode` | - | Surface content | ### CSS Classes -| Class | Description | -| -------------------------- | -------------------- | -| `fri-surface` | Base styles | -| `fri-surface--default` | Default variant | -| `fri-surface--bordered` | Bordered variant | -| `fri-surface--glass` | Glass variant | -| `fri-surface--primary` | Primary variant | -| `fri-surface--radius-none` | No border radius | -| `fri-surface--radius-sm` | Small border radius | -| `fri-surface--radius-md` | Medium border radius | -| `fri-surface--radius-lg` | Large border radius | -| `fri-surface--radius-xl` | Extra large radius | +| Class | Description | +| ------------------------------- | -------------------- | +| `fri-surface` | Base styles | +| `fri-surface--default` | Default variant | +| `fri-surface--default-bordered` | Default + border | +| `fri-surface--plain` | Plain variant | +| `fri-surface--plain-bordered` | Plain + border | +| `fri-surface--glass` | Glass variant | +| `fri-surface--frost` | Frost variant | +| `fri-surface--overlay` | Overlay variant | +| `fri-surface--radius-none` | No border radius | +| `fri-surface--radius-sm` | Small border radius | +| `fri-surface--radius-md` | Medium border radius | +| `fri-surface--radius-lg` | Large border radius | +| `fri-surface--radius-xl` | Extra large radius | +| `fri-surface--shadow-none` | No shadow | +| `fri-surface--shadow-sm` | Small shadow | +| `fri-surface--shadow-md` | Medium shadow | +| `fri-surface--shadow-lg` | Large shadow | +| `fri-surface--shadow-xl` | Extra large shadow | diff --git a/apps/docs/src/content/docs/styles/colors.mdx b/apps/docs/src/content/docs/styles/colors.mdx index 8ac3049..eb88b77 100644 --- a/apps/docs/src/content/docs/styles/colors.mdx +++ b/apps/docs/src/content/docs/styles/colors.mdx @@ -13,9 +13,9 @@ Your main brand color used for primary actions and CTAs. { name: "--primary-hover", token: "primary-hover" }, { name: "--primary-active", token: "primary-active" }, { name: "--primary-foreground", token: "primary-foreground" }, - { name: "--primary-pale", token: "primary-pale" }, - { name: "--primary-pale-foreground", token: "primary-pale-foreground" }, - { name: "--primary-pale-border", token: "primary-pale-border" }, + { name: "--primary-soft", token: "primary-soft" }, + { name: "--primary-soft-foreground", token: "primary-soft-foreground" }, + { name: "--primary-soft-border", token: "primary-soft-border" }, ]} /> @@ -29,9 +29,9 @@ For secondary actions and less emphasis. { name: "--secondary-hover", token: "secondary-hover" }, { name: "--secondary-active", token: "secondary-active" }, { name: "--secondary-foreground", token: "secondary-foreground" }, - { name: "--secondary-pale", token: "secondary-pale" }, - { name: "--secondary-pale-foreground", token: "secondary-pale-foreground" }, - { name: "--secondary-pale-border", token: "secondary-pale-border" }, + { name: "--secondary-soft", token: "secondary-soft" }, + { name: "--secondary-soft-foreground", token: "secondary-soft-foreground" }, + { name: "--secondary-soft-border", token: "secondary-soft-border" }, ]} /> @@ -47,9 +47,9 @@ For alerts, validation, and status messages. { name: "--success-hover", token: "success-hover" }, { name: "--success-active", token: "success-active" }, { name: "--success-foreground", token: "success-foreground" }, - { name: "--success-pale", token: "success-pale" }, - { name: "--success-pale-foreground", token: "success-pale-foreground" }, - { name: "--success-pale-border", token: "success-pale-border" }, + { name: "--success-soft", token: "success-soft" }, + { name: "--success-soft-foreground", token: "success-soft-foreground" }, + { name: "--success-soft-border", token: "success-soft-border" }, ]} /> @@ -61,9 +61,9 @@ For alerts, validation, and status messages. { name: "--warning-hover", token: "warning-hover" }, { name: "--warning-active", token: "warning-active" }, { name: "--warning-foreground", token: "warning-foreground" }, - { name: "--warning-pale", token: "warning-pale" }, - { name: "--warning-pale-foreground", token: "warning-pale-foreground" }, - { name: "--warning-pale-border", token: "warning-pale-border" }, + { name: "--warning-soft", token: "warning-soft" }, + { name: "--warning-soft-foreground", token: "warning-soft-foreground" }, + { name: "--warning-soft-border", token: "warning-soft-border" }, ]} /> @@ -75,9 +75,9 @@ For alerts, validation, and status messages. { name: "--danger-hover", token: "danger-hover" }, { name: "--danger-active", token: "danger-active" }, { name: "--danger-foreground", token: "danger-foreground" }, - { name: "--danger-pale", token: "danger-pale" }, - { name: "--danger-pale-foreground", token: "danger-pale-foreground" }, - { name: "--danger-pale-border", token: "danger-pale-border" }, + { name: "--danger-soft", token: "danger-soft" }, + { name: "--danger-soft-foreground", token: "danger-soft-foreground" }, + { name: "--danger-soft-border", token: "danger-soft-border" }, ]} /> @@ -89,9 +89,9 @@ For alerts, validation, and status messages. { name: "--info-hover", token: "info-hover" }, { name: "--info-active", token: "info-active" }, { name: "--info-foreground", token: "info-foreground" }, - { name: "--info-pale", token: "info-pale" }, - { name: "--info-pale-foreground", token: "info-pale-foreground" }, - { name: "--info-pale-border", token: "info-pale-border" }, + { name: "--info-soft", token: "info-soft" }, + { name: "--info-soft-foreground", token: "info-soft-foreground" }, + { name: "--info-soft-border", token: "info-soft-border" }, ]} /> @@ -209,7 +209,7 @@ Two-color focus indicator for keyboard navigation (WCAG AAA). ```tsx -
+
Alert
``` diff --git a/apps/docs/src/content/docs/styles/design-tokens.mdx b/apps/docs/src/content/docs/styles/design-tokens.mdx index 5c1509c..b4482b1 100644 --- a/apps/docs/src/content/docs/styles/design-tokens.mdx +++ b/apps/docs/src/content/docs/styles/design-tokens.mdx @@ -22,15 +22,15 @@ Every semantic color has 7 tokens: | `{name}-hover` | Background on hover | | `{name}-active` | Background on pressed | | `{name}-foreground` | Text on solid background | -| `{name}-pale` | Soft background (alert, soft button) | -| `{name}-pale-foreground` | Text on pale background | -| `{name}-pale-border` | Border on pale background | +| `{name}-soft` | Soft background (alert, soft button) | +| `{name}-soft-foreground` | Text on pale background | +| `{name}-soft-border` | Border on pale background | Available groups: `primary`, `secondary`, `success`, `warning`, `danger`, `info` ```tsx
Solid
-
Soft
+
Soft
``` ## Shadow @@ -76,18 +76,9 @@ Each color group has a shadow token using `em` units — shadows scale proportio Updated 2 hours ago ``` -### Heading Sizes (√φ scale) +### Heading Sizes -Sizes derived from √φ (1.272): - -| Level | Token | Size | -| ----- | -------------- | --------- | -| h1 | `--heading-h1` | 2.0625rem | -| h2 | `--heading-h2` | 1.625rem | -| h3 | `--heading-h3` | 1.25rem | -| h4 | `--heading-h4` | 1.125rem | -| h5 | `--heading-h5` | 1rem | -| h6 | `--heading-h6` | 0.875rem | +Use Tailwind text size utilities directly for headings (`text-4xl`, `text-3xl`, etc.). ## Spacing (Golden Ratio) @@ -113,14 +104,14 @@ All spacing derives from font-size × φ (1.618), rounded to nearest Tailwind st | Token | Value | Usage | | ---------------- | ----- | ---------------------- | -| `--z-floating` | 10 | Dropdown, tooltip | +| `--z-popover` | 10 | Dropdown, tooltip | | `--z-navigation` | 20 | Sticky header, sidebar | | `--z-overlay` | 30 | Backdrop | | `--z-dialog` | 40 | Modal, drawer | | `--z-alert` | 50 | Toast, notification | ```tsx -
Dropdown
+
Dropdown
Modal
``` @@ -139,12 +130,13 @@ All durations stay under 400ms (Doherty Threshold). Automatically disabled when ### Duration -| Token | Value | Usage | -| -------------------- | ----- | -------------------------- | -| `--duration-instant` | 100ms | Toggle, checkbox | -| `--duration-fast` | 150ms | Button hover, color change | -| `--duration-normal` | 200ms | Dropdown open, fade | -| `--duration-slow` | 300ms | Modal enter, slide | +| Token | Value | Usage | +| ------------------- | ----- | ------------------------------ | +| `--duration-press` | 80ms | Button press, tactile feedback | +| `--duration-toggle` | 100ms | Switch, checkbox flip | +| `--duration-hover` | 150ms | Button hover, color change | +| `--duration-reveal` | 200ms | Dropdown, tooltip, fade | +| `--duration-enter` | 300ms | Modal, drawer, slide | ### Easing @@ -155,7 +147,7 @@ All durations stay under 400ms (Doherty Threshold). Automatically disabled when | `--ease-out` | cubic-bezier(0, 0, 0.2, 1) | Element entering | ```tsx -
Hover me
+
Hover me
``` ### Animations diff --git a/apps/storybook/.storybook/main.ts b/apps/storybook/.storybook/main.ts index 261d743..225be4b 100644 --- a/apps/storybook/.storybook/main.ts +++ b/apps/storybook/.storybook/main.ts @@ -11,7 +11,7 @@ const config: StorybookConfig = { return config; }, - addons: ["@storybook/addon-docs"], + addons: ["@storybook/addon-docs", "@storybook/addon-a11y"], }; export default config; diff --git a/apps/storybook/.storybook/preview.ts b/apps/storybook/.storybook/preview.ts index 75eb53e..f3983a2 100644 --- a/apps/storybook/.storybook/preview.ts +++ b/apps/storybook/.storybook/preview.ts @@ -4,6 +4,15 @@ import "./styles.css"; const preview: Preview = { parameters: { + a11y: { + test: "error", + options: { + runOnly: { + type: "tag", + values: ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"], + }, + }, + }, backgrounds: { default: "light", options: { diff --git a/apps/storybook/.storybook/styles.css b/apps/storybook/.storybook/styles.css index a8658a5..a437d33 100644 --- a/apps/storybook/.storybook/styles.css +++ b/apps/storybook/.storybook/styles.css @@ -2,3 +2,11 @@ @import "@fried-ui/styles"; @source "../../../packages/react/src"; + +.sbdocs-preview:has([data-full-bleed]) .docs-story > div { + padding: 0; +} + +.sbdocs-preview:has([data-full-bleed]) .css-w4emi0 { + padding: 0; +} diff --git a/apps/storybook/package.json b/apps/storybook/package.json index 43b9f34..8276dcf 100644 --- a/apps/storybook/package.json +++ b/apps/storybook/package.json @@ -16,6 +16,7 @@ }, "devDependencies": { "@repo/quality": "workspace:*", + "@storybook/addon-a11y": "^10.3.5", "@storybook/addon-docs": "^10.3.3", "@storybook/addon-vitest": "^10.3.3", "@storybook/react": "^10.3.3", diff --git a/packages/react/src/components/badge/Badge.tsx b/packages/react/src/components/badge/Badge.tsx index 4fbe164..39e3e8a 100644 --- a/packages/react/src/components/badge/Badge.tsx +++ b/packages/react/src/components/badge/Badge.tsx @@ -29,8 +29,10 @@ const Badge = (props: Readonly) => { className, ); + const role = isIconOnly ? "img" : undefined; + return ( - + {children} ); diff --git a/packages/react/src/components/badge/badge.stories.tsx b/packages/react/src/components/badge/badge.stories.tsx index 6b590c9..c561a58 100644 --- a/packages/react/src/components/badge/badge.stories.tsx +++ b/packages/react/src/components/badge/badge.stories.tsx @@ -1,6 +1,15 @@ import type { Meta, StoryObj } from "@storybook/react"; -import { InformationCircleIcon, SettingsIcon } from "../icons"; +import { + BellIcon, + CheckCircleIcon, + ExclamationTriangleIcon, + HeartIcon, + InformationCircleIcon, + LockIcon, + StarIcon, + XCircleIcon, +} from "../icons"; import { Badge } from "./Badge"; const meta = { @@ -14,11 +23,13 @@ const meta = { children: "Badge", variant: "primary", size: "md", + radius: "full", + isIconOnly: false, }, argTypes: { children: { control: "text", - description: "Badge content", + description: "Badge content (text or icon)", table: { type: { summary: "ReactNode", @@ -31,29 +42,43 @@ const meta = { options: [ "primary", "secondary", + "accent", "outline", + "glass", + "frost", + "overlay", "success", "warning", "danger", "info", "primary-soft", "secondary-soft", + "accent-soft", "success-soft", "warning-soft", "danger-soft", "info-soft", + "primary-flat", + "secondary-flat", + "accent-flat", + "success-flat", + "warning-flat", + "danger-flat", + "info-flat", "primary-outline", "secondary-outline", + "accent-outline", "success-outline", "warning-outline", "danger-outline", "info-outline", ], - description: "The visual variant of the badge", + description: + "Visual style. **Base colors:** primary (default), secondary (neutral), accent (brand), success/warning/danger/info (status). **Style modifiers:** -soft (pale bg + border), -flat (pale bg no border), -outline (transparent + colored border). **Special:** glass (frosted + border), frost (frosted no border), overlay (dark scrim). Use primary for main labels, success/danger for status, -soft for subtle tags.", table: { type: { summary: - '"primary" | "secondary" | "outline" | "success" | "warning" | "danger" | "info" | "primary-soft" | "secondary-soft" | "success-soft" | "warning-soft" | "danger-soft" | "info-soft" | "primary-outline" | "secondary-outline" | "success-outline" | "warning-outline" | "danger-outline" | "info-outline"', + '"primary" | "secondary" | "accent" | "outline" | "glass" | "frost" | "overlay" | "success" | "warning" | "danger" | "info" | "primary-soft" | "secondary-soft" | "accent-soft" | "success-soft" | "warning-soft" | "danger-soft" | "info-soft" | "primary-flat" | "secondary-flat" | "accent-flat" | "success-flat" | "warning-flat" | "danger-flat" | "info-flat" | "primary-outline" | "secondary-outline" | "accent-outline" | "success-outline" | "warning-outline" | "danger-outline" | "info-outline"', }, defaultValue: { summary: "primary", @@ -142,7 +167,7 @@ const Variants = () => {
Primary Secondary - Outline + Accent Success Warning Danger @@ -163,8 +188,8 @@ const Variants = () => { Secondary - - Outline + + Accent @@ -197,6 +222,7 @@ const SoftVariants = () => {
Primary Secondary + Accent Success Warning Danger @@ -217,6 +243,10 @@ const SoftVariants = () => { Secondary + + Accent + + Success @@ -236,6 +266,61 @@ const SoftVariants = () => { ), }; +const FlatVariants: Story = { + parameters: { + docs: { + source: { + code: `import { Badge } from "@fried-ui/react"; + +const FlatVariants = () => { + return ( +
+ Primary + Secondary + Accent + Success + Warning + Danger + Info +
+ ); +};`, + }, + }, + }, + render: (args): React.JSX.Element => ( +
+ + Primary + + + + Secondary + + + + Accent + + + + Success + + + + Warning + + + + Danger + + + + Info + +
+ ), +}; + const OutlineVariants: Story = { parameters: { docs: { @@ -247,6 +332,7 @@ const OutlineVariants = () => {
Primary Secondary + Accent Success Warning Danger @@ -267,6 +353,10 @@ const OutlineVariants = () => { Secondary + + Accent + + Success @@ -286,20 +376,20 @@ const OutlineVariants = () => { ), }; -const Radius: Story = { +const GlassVariants: Story = { parameters: { docs: { source: { code: `import { Badge } from "@fried-ui/react"; -const Radius = () => { +const GlassVariants = () => { return ( -
- None - Small - Medium - Large - Full +
+
+ Featured + New + Premium +
); };`, @@ -307,26 +397,98 @@ const Radius = () => { }, }, render: (args): React.JSX.Element => ( -
- - None - +
+
+ + Featured + + + + New + + + + Premium + +
+
+ ), +}; - - Small - +const FrostVariants: Story = { + parameters: { + docs: { + source: { + code: `import { Badge } from "@fried-ui/react"; - - Medium - +const FrostVariants = () => { + return ( +
+
+ Featured + New + Premium +
+
+ ); +};`, + }, + }, + }, + render: (args): React.JSX.Element => ( +
+
+ + Featured + + + + New + + + + Premium + +
+
+ ), +}; - - Large - +const OverlayVariants: Story = { + parameters: { + docs: { + source: { + code: `import { Badge } from "@fried-ui/react"; - - Full - +const OverlayVariants = () => { + return ( +
+
+ Live + HD + 4K +
+
+ ); +};`, + }, + }, + }, + render: (args): React.JSX.Element => ( +
+
+ + Live + + + + HD + + + + 4K + +
), }; @@ -366,27 +528,77 @@ const Sizes = () => { ), }; +const Radius: Story = { + parameters: { + docs: { + source: { + code: `import { Badge } from "@fried-ui/react"; + +const Radius = () => { + return ( +
+ None + Small + Medium + Large + Full +
+ ); +};`, + }, + }, + }, + render: (args): React.JSX.Element => ( +
+ + None + + + + Small + + + + Medium + + + + Large + + + + Full + +
+ ), +}; + const WithIcon: Story = { parameters: { docs: { source: { - code: `import { Badge, SettingsIcon } from "@fried-ui/react"; + code: `import { Badge, CheckCircleIcon, ExclamationTriangleIcon, InformationCircleIcon, XCircleIcon } from "@fried-ui/react"; const WithIcon = () => { return (
- - - Settings + + + Info - - Active + + Success + + + + + Warning - + Error
@@ -397,18 +609,23 @@ const WithIcon = () => { }, render: (args): React.JSX.Element => (
- - - Settings + + + Info - - Active + + Success + + + + + Warning - + Error
@@ -419,21 +636,37 @@ const IconOnly: Story = { parameters: { docs: { source: { - code: `import { Badge, InformationCircleIcon } from "@fried-ui/react"; + code: `import { Badge, BellIcon, CheckCircleIcon, HeartIcon, InformationCircleIcon, LockIcon, StarIcon, XCircleIcon } from "@fried-ui/react"; const IconOnly = () => { return ( -
- +
+ - - + + - - + + + + + + + + + + + + + + + + + +
); @@ -442,22 +675,51 @@ const IconOnly = () => { }, }, render: (args): React.JSX.Element => ( -
- +
+ - - + + - - + + + + + + + + + + + + + + + + + +
), }; -export { Default, Variants, SoftVariants, OutlineVariants, Radius, Sizes, WithIcon, IconOnly }; +export { + Default, + Variants, + SoftVariants, + FlatVariants, + OutlineVariants, + GlassVariants, + FrostVariants, + OverlayVariants, + Sizes, + Radius, + WithIcon, + IconOnly, +}; export default meta; diff --git a/packages/react/src/components/badge/badge.test.tsx b/packages/react/src/components/badge/badge.test.tsx index 06ff58f..337c97a 100644 --- a/packages/react/src/components/badge/badge.test.tsx +++ b/packages/react/src/components/badge/badge.test.tsx @@ -30,19 +30,31 @@ describe("Badge", () => { const variants = [ "primary", "secondary", + "accent", "outline", "success", "warning", "danger", "info", + "glass", + "overlay", "primary-soft", "secondary-soft", + "accent-soft", "success-soft", "warning-soft", "danger-soft", "info-soft", + "primary-flat", + "secondary-flat", + "accent-flat", + "success-flat", + "warning-flat", + "danger-flat", + "info-flat", "primary-outline", "secondary-outline", + "accent-outline", "success-outline", "warning-outline", "danger-outline", diff --git a/packages/react/src/components/badge/badge.variants.ts b/packages/react/src/components/badge/badge.variants.ts index d77b9ea..bdd9220 100644 --- a/packages/react/src/components/badge/badge.variants.ts +++ b/packages/react/src/components/badge/badge.variants.ts @@ -5,19 +5,32 @@ export interface BadgeVariantsProps { variant?: | "primary" | "secondary" + | "accent" | "outline" + | "glass" + | "frost" + | "overlay" | "success" | "warning" | "danger" | "info" | "primary-soft" | "secondary-soft" + | "accent-soft" | "success-soft" | "warning-soft" | "danger-soft" | "info-soft" + | "primary-flat" + | "secondary-flat" + | "accent-flat" + | "success-flat" + | "warning-flat" + | "danger-flat" + | "info-flat" | "primary-outline" | "secondary-outline" + | "accent-outline" | "success-outline" | "warning-outline" | "danger-outline" diff --git a/packages/react/src/components/button/button.stories.tsx b/packages/react/src/components/button/button.stories.tsx index 7f02f3d..1996a6e 100644 --- a/packages/react/src/components/button/button.stories.tsx +++ b/packages/react/src/components/button/button.stories.tsx @@ -15,11 +15,15 @@ const meta = { variant: "primary", size: "md", radius: "md", + isIconOnly: false, + isFullWidth: false, + isDisabled: false, + isPending: false, }, argTypes: { children: { control: "text", - description: "Button content", + description: "Button content (text, icons, or both)", table: { type: { summary: "ReactNode", @@ -29,11 +33,54 @@ const meta = { }, variant: { control: "select", - options: ["primary", "secondary", "ghost", "outline", "success", "warning", "danger", "info"], - description: "The visual variant of the button", + options: [ + "primary", + "secondary", + "accent", + "ghost", + "outline", + "glass", + "frost", + "overlay", + "success", + "warning", + "danger", + "info", + "primary-soft", + "secondary-soft", + "accent-soft", + "success-soft", + "warning-soft", + "danger-soft", + "info-soft", + "primary-flat", + "secondary-flat", + "accent-flat", + "success-flat", + "warning-flat", + "danger-flat", + "info-flat", + "primary-outline", + "secondary-outline", + "accent-outline", + "success-outline", + "warning-outline", + "danger-outline", + "info-outline", + "primary-ghost", + "secondary-ghost", + "accent-ghost", + "success-ghost", + "warning-ghost", + "danger-ghost", + "info-ghost", + ], + description: + "Visual style. **Base colors:** primary (main CTA), secondary (neutral action), accent (brand highlight), ghost (dismissive), outline (neutral outlined), success/warning/danger/info (status). **Modifiers:** -soft (pale + border), -flat (pale no border), -outline (transparent + colored border), -ghost (transparent, hover reveal). **Special:** glass (frosted + border), frost (frosted no border), overlay (dark scrim). Use primary for primary CTA, accent for brand CTA, danger for destructive, ghost for cancel/dismiss.", table: { type: { - summary: '"primary" | "secondary" | "ghost" | "outline" | "success" | "warning" | "danger" | "info"', + summary: + '"primary" | "secondary" | "accent" | "ghost" | "outline" | "glass" | "frost" | "overlay" | "success" | "warning" | "danger" | "info" | "primary-soft" | "secondary-soft" | "accent-soft" | "success-soft" | "warning-soft" | "danger-soft" | "info-soft" | "primary-flat" | "secondary-flat" | "accent-flat" | "success-flat" | "warning-flat" | "danger-flat" | "info-flat" | "primary-outline" | "secondary-outline" | "accent-outline" | "success-outline" | "warning-outline" | "danger-outline" | "info-outline" | "primary-ghost" | "secondary-ghost" | "accent-ghost" | "success-ghost" | "warning-ghost" | "danger-ghost" | "info-ghost"', }, defaultValue: { summary: "primary", @@ -171,8 +218,7 @@ const Variants = () => {
- - + @@ -193,12 +239,8 @@ const Variants = () => { Secondary - - - + + + + + + +
+ ); +};`, + }, + }, + }, + render: (args): React.JSX.Element => ( +
+ + + + + + + + + + + + + +
+ ), +}; + +const FlatVariants: Story = { + parameters: { + docs: { + source: { + code: `import { Button } from "@fried-ui/react"; + +const FlatVariants = () => { + return ( +
+ + + + + + + +
+ ); +};`, + }, + }, + }, + render: (args): React.JSX.Element => ( +
+ + + + + + + + + + + + + +
+ ), +}; + +const OutlineVariants: Story = { + parameters: { + docs: { + source: { + code: `import { Button } from "@fried-ui/react"; + +const OutlineVariants = () => { + return ( +
+ + + + + + + +
+ ); +};`, + }, + }, + }, + render: (args): React.JSX.Element => ( +
+ + + + + + + + + + + + + +
+ ), +}; + +const GhostVariants: Story = { + parameters: { + docs: { + source: { + code: `import { Button } from "@fried-ui/react"; + +const GhostVariants = () => { + return ( +
+ + + + + + + +
+ ); +};`, + }, + }, + }, + render: (args): React.JSX.Element => ( +
+ + + + + + + + + + + + + +
+ ), +}; + const Sizes: Story = { parameters: { docs: { @@ -503,6 +765,141 @@ const RenderProps = () => { ), }; -export { Default, Variants, Sizes, Radius, WithIcon, IconOnly, FullWidth, Disabled, Pending, RenderProps }; +const GlassVariants: Story = { + parameters: { + docs: { + source: { + code: `import { Button } from "@fried-ui/react"; + +const GlassVariants = () => { + return ( +
+
+ + + +
+
+ ); +};`, + }, + }, + }, + render: (args): React.JSX.Element => ( +
+
+ + + + + +
+
+ ), +}; + +const FrostVariants: Story = { + parameters: { + docs: { + source: { + code: `import { Button } from "@fried-ui/react"; + +const FrostVariants = () => { + return ( +
+
+ + + +
+
+ ); +};`, + }, + }, + }, + render: (args): React.JSX.Element => ( +
+
+ + + + + +
+
+ ), +}; + +const OverlayVariants: Story = { + parameters: { + docs: { + source: { + code: `import { Button } from "@fried-ui/react"; + +const OverlayVariants = () => { + return ( +
+
+ + + +
+
+ ); +};`, + }, + }, + }, + render: (args): React.JSX.Element => ( +
+
+ + + + + +
+
+ ), +}; + +export { + Default, + Variants, + SoftVariants, + FlatVariants, + OutlineVariants, + GhostVariants, + GlassVariants, + FrostVariants, + OverlayVariants, + Sizes, + Radius, + WithIcon, + IconOnly, + FullWidth, + Disabled, + Pending, + RenderProps, +}; export default meta; diff --git a/packages/react/src/components/button/button.test.tsx b/packages/react/src/components/button/button.test.tsx index c7674a3..8d872f8 100644 --- a/packages/react/src/components/button/button.test.tsx +++ b/packages/react/src/components/button/button.test.tsx @@ -53,7 +53,48 @@ describe("Button", () => { }); it("applies all variant classes", () => { - const variants = ["primary", "secondary", "ghost", "outline", "success", "warning", "danger", "info"] as const; + const variants = [ + "primary", + "secondary", + "accent", + "ghost", + "outline", + "glass", + "frost", + "overlay", + "success", + "warning", + "danger", + "info", + "primary-soft", + "secondary-soft", + "accent-soft", + "success-soft", + "warning-soft", + "danger-soft", + "info-soft", + "primary-flat", + "secondary-flat", + "accent-flat", + "success-flat", + "warning-flat", + "danger-flat", + "info-flat", + "primary-outline", + "secondary-outline", + "accent-outline", + "success-outline", + "warning-outline", + "danger-outline", + "info-outline", + "primary-ghost", + "secondary-ghost", + "accent-ghost", + "success-ghost", + "warning-ghost", + "danger-ghost", + "info-ghost", + ] as const; variants.forEach((variant) => { const { unmount } = render(); diff --git a/packages/react/src/components/button/button.variants.ts b/packages/react/src/components/button/button.variants.ts index 63fe44e..f56d258 100644 --- a/packages/react/src/components/button/button.variants.ts +++ b/packages/react/src/components/button/button.variants.ts @@ -3,5 +3,45 @@ export interface ButtonVariantsProps { isIconOnly?: boolean; radius?: "none" | "sm" | "md" | "lg" | "full"; size?: "sm" | "md" | "lg" | "xl"; - variant?: "primary" | "secondary" | "ghost" | "outline" | "success" | "warning" | "danger" | "info"; + variant?: + | "primary" + | "secondary" + | "accent" + | "ghost" + | "outline" + | "glass" + | "frost" + | "overlay" + | "success" + | "warning" + | "danger" + | "info" + | "primary-soft" + | "secondary-soft" + | "accent-soft" + | "success-soft" + | "warning-soft" + | "danger-soft" + | "info-soft" + | "primary-flat" + | "secondary-flat" + | "accent-flat" + | "success-flat" + | "warning-flat" + | "danger-flat" + | "info-flat" + | "primary-outline" + | "secondary-outline" + | "accent-outline" + | "success-outline" + | "warning-outline" + | "danger-outline" + | "info-outline" + | "primary-ghost" + | "secondary-ghost" + | "accent-ghost" + | "success-ghost" + | "warning-ghost" + | "danger-ghost" + | "info-ghost"; } diff --git a/packages/react/src/components/icons/ArrowRightIcon.tsx b/packages/react/src/components/icons/ArrowRightIcon.tsx index 959e40c..3e4b2e8 100644 --- a/packages/react/src/components/icons/ArrowRightIcon.tsx +++ b/packages/react/src/components/icons/ArrowRightIcon.tsx @@ -1,8 +1,6 @@ -"use client"; - import type { ComponentPropsWithRef } from "react"; -const ArrowRightIcon = (props: Readonly & { slot?: string }>) => { +const ArrowRightIcon = (props: Readonly>) => { const { className, ref, ...rest } = props; return ( @@ -11,11 +9,15 @@ const ArrowRightIcon = (props: Readonly & { slot?: fill="none" stroke="currentColor" strokeWidth="2" + strokeLinecap="round" + strokeLinejoin="round" className={className} ref={ref} {...rest} > - + + + ); }; diff --git a/packages/react/src/components/icons/BellIcon.tsx b/packages/react/src/components/icons/BellIcon.tsx new file mode 100644 index 0000000..04f5f1c --- /dev/null +++ b/packages/react/src/components/icons/BellIcon.tsx @@ -0,0 +1,26 @@ +import type { ComponentPropsWithRef } from "react"; + +const BellIcon = (props: Readonly>) => { + const { className, ref, ...rest } = props; + + return ( + + + + + ); +}; + +BellIcon.displayName = "BellIcon"; + +export { BellIcon }; diff --git a/packages/react/src/components/icons/CheckCircleIcon.tsx b/packages/react/src/components/icons/CheckCircleIcon.tsx new file mode 100644 index 0000000..a2ee5dd --- /dev/null +++ b/packages/react/src/components/icons/CheckCircleIcon.tsx @@ -0,0 +1,26 @@ +import type { ComponentPropsWithRef } from "react"; + +const CheckCircleIcon = (props: Readonly>) => { + const { className, ref, ...rest } = props; + + return ( + + + + + ); +}; + +CheckCircleIcon.displayName = "CheckCircleIcon"; + +export { CheckCircleIcon }; diff --git a/packages/react/src/components/icons/ClipboardIcon.tsx b/packages/react/src/components/icons/ClipboardIcon.tsx index 56aaf5a..558b2d3 100644 --- a/packages/react/src/components/icons/ClipboardIcon.tsx +++ b/packages/react/src/components/icons/ClipboardIcon.tsx @@ -1,13 +1,22 @@ -"use client"; - import type { ComponentPropsWithRef } from "react"; -const ClipboardIcon = (props: Readonly & { slot?: string }>) => { +const ClipboardIcon = (props: Readonly>) => { const { className, ref, ...rest } = props; return ( - - + + + ); }; diff --git a/packages/react/src/components/icons/ExclamationTriangleIcon.tsx b/packages/react/src/components/icons/ExclamationTriangleIcon.tsx new file mode 100644 index 0000000..89a1070 --- /dev/null +++ b/packages/react/src/components/icons/ExclamationTriangleIcon.tsx @@ -0,0 +1,27 @@ +import type { ComponentPropsWithRef } from "react"; + +const ExclamationTriangleIcon = (props: Readonly>) => { + const { className, ref, ...rest } = props; + + return ( + + + + + + ); +}; + +ExclamationTriangleIcon.displayName = "ExclamationTriangleIcon"; + +export { ExclamationTriangleIcon }; diff --git a/packages/react/src/components/icons/GitHubIcon.tsx b/packages/react/src/components/icons/GitHubIcon.tsx index af9bc27..2bcd120 100644 --- a/packages/react/src/components/icons/GitHubIcon.tsx +++ b/packages/react/src/components/icons/GitHubIcon.tsx @@ -1,16 +1,10 @@ -"use client"; - import type { ComponentPropsWithRef } from "react"; -const GitHubIcon = (props: Readonly & { slot?: string }>) => { - const { className, ref, ...rest } = props; - - return ( - - - - ); -}; +const GitHubIcon = (props: Readonly>) => ( + + + +); GitHubIcon.displayName = "GitHubIcon"; diff --git a/packages/react/src/components/icons/HeartIcon.tsx b/packages/react/src/components/icons/HeartIcon.tsx new file mode 100644 index 0000000..9622840 --- /dev/null +++ b/packages/react/src/components/icons/HeartIcon.tsx @@ -0,0 +1,25 @@ +import type { ComponentPropsWithRef } from "react"; + +const HeartIcon = (props: Readonly>) => { + const { className, ref, ...rest } = props; + + return ( + + + + ); +}; + +HeartIcon.displayName = "HeartIcon"; + +export { HeartIcon }; diff --git a/packages/react/src/components/icons/InformationCircleIcon.tsx b/packages/react/src/components/icons/InformationCircleIcon.tsx index 6a91315..9552ca0 100644 --- a/packages/react/src/components/icons/InformationCircleIcon.tsx +++ b/packages/react/src/components/icons/InformationCircleIcon.tsx @@ -1,8 +1,6 @@ -"use client"; - import type { ComponentPropsWithRef } from "react"; -const InformationCircleIcon = (props: Readonly & { slot?: string }>) => { +const InformationCircleIcon = (props: Readonly>) => { const { className, ref, ...rest } = props; return ( @@ -10,16 +8,16 @@ const InformationCircleIcon = (props: Readonly & { viewBox="0 0 24 24" fill="none" stroke="currentColor" - strokeWidth="1.5" + strokeWidth="2" + strokeLinecap="round" + strokeLinejoin="round" className={className} ref={ref} {...rest} > - + + + ); }; diff --git a/packages/react/src/components/icons/LockIcon.tsx b/packages/react/src/components/icons/LockIcon.tsx new file mode 100644 index 0000000..ce74f5e --- /dev/null +++ b/packages/react/src/components/icons/LockIcon.tsx @@ -0,0 +1,27 @@ +import type { ComponentPropsWithRef } from "react"; + +const LockIcon = (props: Readonly>) => { + const { className, ref, ...rest } = props; + + return ( + + + + + + ); +}; + +LockIcon.displayName = "LockIcon"; + +export { LockIcon }; diff --git a/packages/react/src/components/icons/MoreIcon.tsx b/packages/react/src/components/icons/MoreIcon.tsx index 293d706..9618986 100644 --- a/packages/react/src/components/icons/MoreIcon.tsx +++ b/packages/react/src/components/icons/MoreIcon.tsx @@ -1,8 +1,6 @@ -"use client"; - import type { ComponentPropsWithRef } from "react"; -const MoreIcon = (props: Readonly & { slot?: string }>) => { +const MoreIcon = (props: Readonly>) => { const { className, ref, ...rest } = props; return ( @@ -10,16 +8,16 @@ const MoreIcon = (props: Readonly & { slot?: string viewBox="0 0 24 24" fill="none" stroke="currentColor" - strokeWidth="1.5" + strokeWidth="2" + strokeLinecap="round" + strokeLinejoin="round" className={className} ref={ref} {...rest} > - + + + ); }; diff --git a/packages/react/src/components/icons/PlusIcon.tsx b/packages/react/src/components/icons/PlusIcon.tsx index 47be65b..d5cfd27 100644 --- a/packages/react/src/components/icons/PlusIcon.tsx +++ b/packages/react/src/components/icons/PlusIcon.tsx @@ -1,8 +1,6 @@ -"use client"; - import type { ComponentPropsWithRef } from "react"; -const PlusIcon = (props: Readonly & { slot?: string }>) => { +const PlusIcon = (props: Readonly>) => { const { className, ref, ...rest } = props; return ( @@ -10,12 +8,15 @@ const PlusIcon = (props: Readonly & { slot?: string viewBox="0 0 24 24" fill="none" stroke="currentColor" - strokeWidth="1.5" + strokeWidth="2" + strokeLinecap="round" + strokeLinejoin="round" className={className} ref={ref} {...rest} > - + + ); }; diff --git a/packages/react/src/components/icons/ReactAriaIcon.tsx b/packages/react/src/components/icons/ReactAriaIcon.tsx index d74c852..84d829c 100644 --- a/packages/react/src/components/icons/ReactAriaIcon.tsx +++ b/packages/react/src/components/icons/ReactAriaIcon.tsx @@ -1,16 +1,10 @@ -"use client"; - import type { ComponentPropsWithRef } from "react"; -const ReactAriaIcon = (props: Readonly & { slot?: string }>) => { - const { className, ref, ...rest } = props; - - return ( - - - - ); -}; +const ReactAriaIcon = (props: Readonly>) => ( + + + +); ReactAriaIcon.displayName = "ReactAriaIcon"; diff --git a/packages/react/src/components/icons/SettingsIcon.tsx b/packages/react/src/components/icons/SettingsIcon.tsx index 4db0f14..17f98dd 100644 --- a/packages/react/src/components/icons/SettingsIcon.tsx +++ b/packages/react/src/components/icons/SettingsIcon.tsx @@ -1,8 +1,6 @@ -"use client"; - import type { ComponentPropsWithRef } from "react"; -const SettingsIcon = (props: Readonly & { slot?: string }>) => { +const SettingsIcon = (props: Readonly>) => { const { className, ref, ...rest } = props; return ( @@ -10,18 +8,15 @@ const SettingsIcon = (props: Readonly & { slot?: st viewBox="0 0 24 24" fill="none" stroke="currentColor" - strokeWidth="1.5" + strokeWidth="2" + strokeLinecap="round" + strokeLinejoin="round" className={className} ref={ref} {...rest} > - - - + + ); }; diff --git a/packages/react/src/components/icons/ShareIcon.tsx b/packages/react/src/components/icons/ShareIcon.tsx index 394e836..30a0bc2 100644 --- a/packages/react/src/components/icons/ShareIcon.tsx +++ b/packages/react/src/components/icons/ShareIcon.tsx @@ -1,8 +1,6 @@ -"use client"; - import type { ComponentPropsWithRef } from "react"; -const ShareIcon = (props: Readonly & { slot?: string }>) => { +const ShareIcon = (props: Readonly>) => { const { className, ref, ...rest } = props; return ( @@ -10,16 +8,18 @@ const ShareIcon = (props: Readonly & { slot?: strin viewBox="0 0 24 24" fill="none" stroke="currentColor" - strokeWidth="1.5" + strokeWidth="2" + strokeLinecap="round" + strokeLinejoin="round" className={className} ref={ref} {...rest} > - + + + + + ); }; diff --git a/packages/react/src/components/icons/Spinner.tsx b/packages/react/src/components/icons/Spinner.tsx index e6bc575..eb9b96b 100644 --- a/packages/react/src/components/icons/Spinner.tsx +++ b/packages/react/src/components/icons/Spinner.tsx @@ -1,14 +1,39 @@ -"use client"; +import { type ComponentPropsWithRef, useId } from "react"; -import type { ComponentPropsWithRef } from "react"; - -const Spinner = (props: Readonly & { slot?: string }>) => { - const { className, ref, ...rest } = props; +const Spinner = (props: Readonly>) => { + const id = useId(); + const grad1 = `fri-spinner-grad-1-${id}`; + const grad2 = `fri-spinner-grad-2-${id}`; + const fillGrad1 = `url(#${grad1})`; + const fillGrad2 = `url(#${grad2})`; return ( - - - + ); }; diff --git a/packages/react/src/components/icons/StarIcon.tsx b/packages/react/src/components/icons/StarIcon.tsx new file mode 100644 index 0000000..bb8fc80 --- /dev/null +++ b/packages/react/src/components/icons/StarIcon.tsx @@ -0,0 +1,25 @@ +import type { ComponentPropsWithRef } from "react"; + +const StarIcon = (props: Readonly>) => { + const { className, ref, ...rest } = props; + + return ( + + + + ); +}; + +StarIcon.displayName = "StarIcon"; + +export { StarIcon }; diff --git a/packages/react/src/components/icons/StorybookIcon.tsx b/packages/react/src/components/icons/StorybookIcon.tsx index ccd9c6d..b24b314 100644 --- a/packages/react/src/components/icons/StorybookIcon.tsx +++ b/packages/react/src/components/icons/StorybookIcon.tsx @@ -1,16 +1,10 @@ -"use client"; - import type { ComponentPropsWithRef } from "react"; -const StorybookIcon = (props: Readonly & { slot?: string }>) => { - const { className, ref, ...rest } = props; - - return ( - - - - ); -}; +const StorybookIcon = (props: Readonly>) => ( + + + +); StorybookIcon.displayName = "StorybookIcon"; diff --git a/packages/react/src/components/icons/XCircleIcon.tsx b/packages/react/src/components/icons/XCircleIcon.tsx new file mode 100644 index 0000000..a62be74 --- /dev/null +++ b/packages/react/src/components/icons/XCircleIcon.tsx @@ -0,0 +1,26 @@ +import type { ComponentPropsWithRef } from "react"; + +const XCircleIcon = (props: Readonly>) => { + const { className, ref, ...rest } = props; + + return ( + + + + + ); +}; + +XCircleIcon.displayName = "XCircleIcon"; + +export { XCircleIcon }; diff --git a/packages/react/src/components/icons/index.ts b/packages/react/src/components/icons/index.ts index 8358ed9..e55d0d6 100644 --- a/packages/react/src/components/icons/index.ts +++ b/packages/react/src/components/icons/index.ts @@ -1,11 +1,18 @@ export { ArrowRightIcon } from "./ArrowRightIcon"; +export { BellIcon } from "./BellIcon"; +export { CheckCircleIcon } from "./CheckCircleIcon"; export { ClipboardIcon } from "./ClipboardIcon"; +export { ExclamationTriangleIcon } from "./ExclamationTriangleIcon"; export { GitHubIcon } from "./GitHubIcon"; +export { HeartIcon } from "./HeartIcon"; export { InformationCircleIcon } from "./InformationCircleIcon"; +export { LockIcon } from "./LockIcon"; export { MoreIcon } from "./MoreIcon"; export { PlusIcon } from "./PlusIcon"; export { ReactAriaIcon } from "./ReactAriaIcon"; export { SettingsIcon } from "./SettingsIcon"; export { ShareIcon } from "./ShareIcon"; export { Spinner } from "./Spinner"; +export { StarIcon } from "./StarIcon"; export { StorybookIcon } from "./StorybookIcon"; +export { XCircleIcon } from "./XCircleIcon"; diff --git a/packages/react/src/components/surface/Surface.tsx b/packages/react/src/components/surface/Surface.tsx index 1c45c22..20739ce 100644 --- a/packages/react/src/components/surface/Surface.tsx +++ b/packages/react/src/components/surface/Surface.tsx @@ -14,7 +14,7 @@ export type SurfaceProps = Omit, "className"> & }; const Surface = (props: Readonly) => { - const { children, className, radius, ref, variant, ...rest } = props; + const { children, className, radius, ref, shadow, variant, ...rest } = props; const surfaceClassName = clsx( bem({ @@ -22,6 +22,7 @@ const Surface = (props: Readonly) => { modifiers: { variant, radius, + shadow, }, }), className, diff --git a/packages/react/src/components/surface/surface.stories.tsx b/packages/react/src/components/surface/surface.stories.tsx index 1d5587f..813401b 100644 --- a/packages/react/src/components/surface/surface.stories.tsx +++ b/packages/react/src/components/surface/surface.stories.tsx @@ -11,11 +11,14 @@ const meta = { }, args: { children: "Surface content", + variant: "default", + radius: "md", + shadow: "none", }, argTypes: { children: { control: "text", - description: "Surface content", + description: "Surface content (ReactNode)", table: { type: { summary: "ReactNode", @@ -25,11 +28,12 @@ const meta = { }, variant: { control: "select", - options: ["default", "bordered", "glass", "primary"], - description: "The visual style of the surface", + options: ["default", "default-bordered", "plain", "plain-bordered", "glass", "frost", "overlay"], + description: + "Visual style. **Base:** default (subtle gray card), default-bordered (gray + border), plain (pure white — for gray pages), plain-bordered (white + border). **Special:** glass (frosted + border), frost (frosted no border), overlay (dark scrim — on media). Use default on white pages, plain on gray pages, *-bordered for emphasis, glass/frost/overlay for media overlays.", table: { type: { - summary: '"default" | "bordered" | "glass" | "primary"', + summary: '"default" | "default-bordered" | "plain" | "plain-bordered" | "glass" | "frost" | "overlay"', }, defaultValue: { summary: "default", @@ -40,7 +44,7 @@ const meta = { radius: { control: "select", options: ["none", "sm", "md", "lg", "xl"], - description: "Border radius", + description: "The border radius of the surface", table: { type: { summary: '"none" | "sm" | "md" | "lg" | "xl"', @@ -51,6 +55,20 @@ const meta = { category: "Style Variants", }, }, + shadow: { + control: "select", + options: ["none", "sm", "md", "lg", "xl"], + description: "Elevation shadow — orthogonal to variant", + table: { + type: { + summary: '"none" | "sm" | "md" | "lg" | "xl"', + }, + defaultValue: { + summary: "none", + }, + category: "Style Variants", + }, + }, className: { control: "text", description: "Additional CSS classes", @@ -100,10 +118,14 @@ const Variants: Story = { const Variants = () => { return (
- Default - Bordered - Glass - Primary + +

Default

+

Subtle gray card on white.

+
+ +

Default Bordered

+

Gray card with emphasis border.

+
); };`, @@ -112,20 +134,139 @@ const Variants = () => { }, render: (args): React.JSX.Element => (
- - Default + +

Default

+

Subtle gray card on white.

- - Bordered + +

Default Bordered

+

Gray card with emphasis border.

+
+ ), +}; + +const PlainVariants: Story = { + parameters: { + docs: { + source: { + code: `import { Surface } from "@fried-ui/react"; - - Glass +const PlainVariants = () => { + return ( +
+ +

Plain

+

Pure white card on gray page.

+
+ +

Plain Bordered

+

White card with emphasis border.

+
+
+ ); +};`, + }, + }, + }, + render: (args): React.JSX.Element => ( +
+ +

Plain

+

Pure white card on gray page.

- - Primary + +

Plain Bordered

+

White card with emphasis border.

+
+
+ ), +}; + +const GlassVariants: Story = { + parameters: { + docs: { + source: { + code: `import { Surface } from "@fried-ui/react"; + +const GlassVariants = () => { + return ( +
+ +

Glass Surface

+

Content over a gradient background.

+
+
+ ); +};`, + }, + }, + }, + render: (args): React.JSX.Element => ( +
+ +

Glass Surface

+

Content over a gradient background.

+
+
+ ), +}; + +const FrostVariants: Story = { + parameters: { + docs: { + source: { + code: `import { Surface } from "@fried-ui/react"; + +const FrostVariants = () => { + return ( +
+ +

Frost Surface

+

Frosted content without a border.

+
+
+ ); +};`, + }, + }, + }, + render: (args): React.JSX.Element => ( +
+ +

Frost Surface

+

Frosted content without a border.

+
+
+ ), +}; + +const OverlayVariants: Story = { + parameters: { + docs: { + source: { + code: `import { Surface } from "@fried-ui/react"; + +const OverlayVariants = () => { + return ( +
+ +

Overlay Surface

+

Dark scrim over media content.

+
+
+ ); +};`, + }, + }, + }, + render: (args): React.JSX.Element => ( +
+ +

Overlay Surface

+

Dark scrim over media content.

), @@ -140,11 +281,26 @@ const Radius: Story = { const Radius = () => { return (
- none - sm - md - lg - xl + +

None

+

No rounded corners.

+
+ +

Small

+

Subtle rounding.

+
+ +

Medium

+

Balanced rounding.

+
+ +

Large

+

Soft rounding.

+
+ +

Extra Large

+

Very rounded corners.

+
); };`, @@ -153,41 +309,62 @@ const Radius = () => { }, render: (args): React.JSX.Element => (
- - none + +

None

+

No rounded corners.

- - sm + +

Small

+

Subtle rounding.

- - md + +

Medium

+

Balanced rounding.

- - lg + +

Large

+

Soft rounding.

- - xl + +

Extra Large

+

Very rounded corners.

), }; -const Glass: Story = { +const Shadow: Story = { parameters: { docs: { source: { code: `import { Surface } from "@fried-ui/react"; -const Glass = () => { +const Shadow = () => { return ( -
- -

Glass Surface

-

Content over a gradient background.

+
+ +

None

+

No shadow.

+
+ +

Small

+

Subtle lift.

+
+ +

Medium

+

Noticeable elevation.

+
+ +

Large

+

Floating card.

+
+ +

Extra Large

+

Strong elevation.

); @@ -196,15 +373,35 @@ const Glass = () => { }, }, render: (args): React.JSX.Element => ( -
- -

Glass Surface

-

Content over a gradient background.

+
+ +

None

+

No shadow.

+
+ + +

Small

+

Subtle lift.

+
+ + +

Medium

+

Noticeable elevation.

+
+ + +

Large

+

Floating card.

+
+ + +

Extra Large

+

Strong elevation.

), }; -export { Default, Glass, Radius, Variants }; +export { Default, Variants, PlainVariants, GlassVariants, FrostVariants, OverlayVariants, Radius, Shadow }; export default meta; diff --git a/packages/react/src/components/surface/surface.test.tsx b/packages/react/src/components/surface/surface.test.tsx index f77019f..ccbf5de 100644 --- a/packages/react/src/components/surface/surface.test.tsx +++ b/packages/react/src/components/surface/surface.test.tsx @@ -17,7 +17,7 @@ describe("Surface", () => { }); it("applies all variant classes", () => { - const variants = ["default", "bordered", "glass", "primary"] as const; + const variants = ["default", "default-bordered", "plain", "plain-bordered", "glass", "frost", "overlay"] as const; variants.forEach((variant) => { const { unmount } = render({variant}); @@ -36,16 +36,27 @@ describe("Surface", () => { }); }); - it("combines variant + radius", () => { + it("applies shadow class with key-value format", () => { + const shadowValues = ["none", "sm", "md", "lg", "xl"] as const; + + shadowValues.forEach((shadow) => { + const { unmount } = render({shadow}); + expect(screen.getByText(shadow).className).toContain(`fri-surface--shadow-${shadow}`); + unmount(); + }); + }); + + it("combines variant + radius + shadow", () => { render( - + Combined , ); const el = screen.getByText("Combined"); - expect(el.className).toContain("fri-surface--bordered"); + expect(el.className).toContain("fri-surface--plain-bordered"); expect(el.className).toContain("fri-surface--radius-lg"); + expect(el.className).toContain("fri-surface--shadow-md"); }); it("merges custom className", () => { diff --git a/packages/react/src/components/surface/surface.variants.ts b/packages/react/src/components/surface/surface.variants.ts index 310d8f4..9af402e 100644 --- a/packages/react/src/components/surface/surface.variants.ts +++ b/packages/react/src/components/surface/surface.variants.ts @@ -1,4 +1,5 @@ export interface SurfaceVariantsProps { radius?: "none" | "sm" | "md" | "lg" | "xl"; - variant?: "default" | "bordered" | "glass" | "primary"; + shadow?: "none" | "sm" | "md" | "lg" | "xl"; + variant?: "default" | "default-bordered" | "plain" | "plain-bordered" | "glass" | "frost" | "overlay"; } diff --git a/packages/styles/src/components/badge.css b/packages/styles/src/components/badge.css index 42babb0..81a7b92 100644 --- a/packages/styles/src/components/badge.css +++ b/packages/styles/src/components/badge.css @@ -14,122 +14,211 @@ .fri-badge--primary { --fri-badge-bg: var(--color-primary); --fri-badge-fg: var(--color-primary-foreground); + + @apply border border-primary-border; } .fri-badge--secondary { --fri-badge-bg: var(--color-secondary); --fri-badge-fg: var(--color-secondary-foreground); + + @apply border border-secondary-border; +} + +.fri-badge--accent { + --fri-badge-bg: var(--color-accent); + --fri-badge-fg: var(--color-accent-foreground); + + @apply border border-accent-border; } .fri-badge--outline { --fri-badge-bg: transparent; - --fri-badge-fg: var(--color-foreground); + --fri-badge-fg: var(--color-primary); + + @apply border border-primary-border; +} + +.fri-badge--glass { + --fri-badge-bg: var(--color-glass); + --fri-badge-fg: var(--color-glass-foreground); + + @apply border border-glass-border backdrop-blur-xl backdrop-saturate-200; +} + +.fri-badge--frost { + --fri-badge-bg: var(--color-glass); + --fri-badge-fg: var(--color-glass-foreground); - @apply border-border; + @apply backdrop-blur-2xl backdrop-saturate-200; +} + +.fri-badge--overlay { + --fri-badge-bg: var(--color-scrim); + --fri-badge-fg: var(--color-scrim-foreground); + + @apply border border-scrim-border backdrop-blur-xl backdrop-saturate-150; } .fri-badge--success { --fri-badge-bg: var(--color-success); --fri-badge-fg: var(--color-success-foreground); + + @apply border border-success-border; } .fri-badge--warning { --fri-badge-bg: var(--color-warning); --fri-badge-fg: var(--color-warning-foreground); + + @apply border border-warning-border; } .fri-badge--danger { --fri-badge-bg: var(--color-danger); --fri-badge-fg: var(--color-danger-foreground); + + @apply border border-danger-border; } .fri-badge--info { --fri-badge-bg: var(--color-info); --fri-badge-fg: var(--color-info-foreground); + + @apply border border-info-border; } .fri-badge--primary-soft { - --fri-badge-bg: var(--color-primary-pale); - --fri-badge-fg: var(--color-primary-pale-foreground); + --fri-badge-bg: var(--color-primary-soft); + --fri-badge-fg: var(--color-primary-soft-foreground); - @apply border border-primary-pale-border; + @apply border border-primary-soft-border; } .fri-badge--secondary-soft { - --fri-badge-bg: var(--color-secondary-pale); - --fri-badge-fg: var(--color-secondary-pale-foreground); + --fri-badge-bg: var(--color-secondary-soft); + --fri-badge-fg: var(--color-secondary-soft-foreground); + + @apply border border-secondary-soft-border; +} - @apply border border-secondary-pale-border; +.fri-badge--accent-soft { + --fri-badge-bg: var(--color-accent-soft); + --fri-badge-fg: var(--color-accent-soft-foreground); + + @apply border border-accent-soft-border; } .fri-badge--success-soft { - --fri-badge-bg: var(--color-success-pale); - --fri-badge-fg: var(--color-success-pale-foreground); + --fri-badge-bg: var(--color-success-soft); + --fri-badge-fg: var(--color-success-soft-foreground); - @apply border border-success-pale-border; + @apply border border-success-soft-border; } .fri-badge--warning-soft { - --fri-badge-bg: var(--color-warning-pale); - --fri-badge-fg: var(--color-warning-pale-foreground); + --fri-badge-bg: var(--color-warning-soft); + --fri-badge-fg: var(--color-warning-soft-foreground); - @apply border border-warning-pale-border; + @apply border border-warning-soft-border; } .fri-badge--danger-soft { - --fri-badge-bg: var(--color-danger-pale); - --fri-badge-fg: var(--color-danger-pale-foreground); + --fri-badge-bg: var(--color-danger-soft); + --fri-badge-fg: var(--color-danger-soft-foreground); - @apply border border-danger-pale-border; + @apply border border-danger-soft-border; } .fri-badge--info-soft { - --fri-badge-bg: var(--color-info-pale); - --fri-badge-fg: var(--color-info-pale-foreground); + --fri-badge-bg: var(--color-info-soft); + --fri-badge-fg: var(--color-info-soft-foreground); + + @apply border border-info-soft-border; +} - @apply border border-info-pale-border; +.fri-badge--primary-flat { + --fri-badge-bg: var(--color-primary-flat); + --fri-badge-fg: var(--color-primary-flat-foreground); +} + +.fri-badge--secondary-flat { + --fri-badge-bg: var(--color-secondary-flat); + --fri-badge-fg: var(--color-secondary-flat-foreground); +} + +.fri-badge--accent-flat { + --fri-badge-bg: var(--color-accent-flat); + --fri-badge-fg: var(--color-accent-flat-foreground); +} + +.fri-badge--success-flat { + --fri-badge-bg: var(--color-success-flat); + --fri-badge-fg: var(--color-success-flat-foreground); +} + +.fri-badge--warning-flat { + --fri-badge-bg: var(--color-warning-flat); + --fri-badge-fg: var(--color-warning-flat-foreground); +} + +.fri-badge--danger-flat { + --fri-badge-bg: var(--color-danger-flat); + --fri-badge-fg: var(--color-danger-flat-foreground); +} + +.fri-badge--info-flat { + --fri-badge-bg: var(--color-info-flat); + --fri-badge-fg: var(--color-info-flat-foreground); } .fri-badge--primary-outline { - --fri-badge-bg: transparent; - --fri-badge-fg: var(--color-primary); + --fri-badge-bg: var(--color-primary-outline); + --fri-badge-fg: var(--color-primary-outline-foreground); - @apply border border-primary; + @apply border border-primary-outline-border; } .fri-badge--secondary-outline { - --fri-badge-bg: transparent; - --fri-badge-fg: var(--color-secondary-foreground); + --fri-badge-bg: var(--color-secondary-outline); + --fri-badge-fg: var(--color-secondary-outline-foreground); - @apply border border-secondary; + @apply border border-secondary-outline-border; +} + +.fri-badge--accent-outline { + --fri-badge-bg: var(--color-accent-outline); + --fri-badge-fg: var(--color-accent-outline-foreground); + + @apply border border-accent-outline-border; } .fri-badge--success-outline { - --fri-badge-bg: transparent; - --fri-badge-fg: var(--color-success); + --fri-badge-bg: var(--color-success-outline); + --fri-badge-fg: var(--color-success-outline-foreground); - @apply border border-success; + @apply border border-success-outline-border; } .fri-badge--warning-outline { - --fri-badge-bg: transparent; - --fri-badge-fg: var(--color-warning); + --fri-badge-bg: var(--color-warning-outline); + --fri-badge-fg: var(--color-warning-outline-foreground); - @apply border border-warning; + @apply border border-warning-outline-border; } .fri-badge--danger-outline { - --fri-badge-bg: transparent; - --fri-badge-fg: var(--color-danger); + --fri-badge-bg: var(--color-danger-outline); + --fri-badge-fg: var(--color-danger-outline-foreground); - @apply border border-danger; + @apply border border-danger-outline-border; } .fri-badge--info-outline { - --fri-badge-bg: transparent; - --fri-badge-fg: var(--color-info); + --fri-badge-bg: var(--color-info-outline); + --fri-badge-fg: var(--color-info-outline-foreground); - @apply border border-info; + @apply border border-info-outline-border; } .fri-badge--radius-none { @@ -203,7 +292,7 @@ } .fri-badge--icon-only { - @apply aspect-square w-[calc(0.875rem+2*(0.875rem/2.058)+0.125rem)] justify-center gap-0 rounded-full p-0; + @apply aspect-square w-[calc(0.875rem+2*(0.875rem/2.058)+0.125rem)] justify-center gap-0 p-0; } .fri-badge--icon-only.fri-badge--size-sm { diff --git a/packages/styles/src/components/button.css b/packages/styles/src/components/button.css index 5b794d5..ae3821b 100644 --- a/packages/styles/src/components/button.css +++ b/packages/styles/src/components/button.css @@ -2,7 +2,7 @@ @apply relative inline-flex items-center justify-center; @apply leading-none font-medium whitespace-nowrap; @apply cursor-pointer border border-transparent select-none no-highlight; - @apply transition-colors duration-(--duration-fast) ease-smooth motion-reduce:transition-none; + @apply transition-colors duration-(--duration-hover) ease-out motion-reduce:transition-none; --fri-button-bg: var(--color-primary); --fri-button-bg-hover: var(--color-primary-hover); @@ -28,7 +28,7 @@ &:active, &[data-pressed] { - @apply bg-(--fri-button-bg-pressed); + @apply bg-(--fri-button-bg-pressed) duration-(--duration-press); } @media (hover: hover) { @@ -44,6 +44,8 @@ --fri-button-bg-hover: var(--color-primary-hover); --fri-button-bg-pressed: var(--color-primary-active); --fri-button-fg: var(--color-primary-foreground); + + @apply border border-primary-border; } .fri-button--secondary { @@ -51,6 +53,17 @@ --fri-button-bg-hover: var(--color-secondary-hover); --fri-button-bg-pressed: var(--color-secondary-active); --fri-button-fg: var(--color-secondary-foreground); + + @apply border border-secondary-border; +} + +.fri-button--accent { + --fri-button-bg: var(--color-accent); + --fri-button-bg-hover: var(--color-accent-hover); + --fri-button-bg-pressed: var(--color-accent-active); + --fri-button-fg: var(--color-accent-foreground); + + @apply border border-accent-border; } .fri-button--ghost { @@ -62,18 +75,47 @@ .fri-button--outline { --fri-button-bg: transparent; - --fri-button-bg-hover: var(--color-surface-hover); - --fri-button-bg-pressed: var(--color-surface-active); + --fri-button-bg-hover: var(--color-muted); + --fri-button-bg-pressed: var(--color-muted-hover); --fri-button-fg: var(--color-foreground); @apply border-border; } +.fri-button--glass { + --fri-button-bg: var(--color-glass); + --fri-button-bg-hover: var(--color-glass-hover); + --fri-button-bg-pressed: var(--color-glass-active); + --fri-button-fg: var(--color-glass-foreground); + + @apply border-glass-border backdrop-blur-xl backdrop-saturate-200; +} + +.fri-button--frost { + --fri-button-bg: var(--color-glass); + --fri-button-bg-hover: var(--color-glass-hover); + --fri-button-bg-pressed: var(--color-glass-active); + --fri-button-fg: var(--color-glass-foreground); + + @apply backdrop-blur-2xl backdrop-saturate-200; +} + +.fri-button--overlay { + --fri-button-bg: var(--color-scrim); + --fri-button-bg-hover: var(--color-scrim-hover); + --fri-button-bg-pressed: var(--color-scrim-active); + --fri-button-fg: var(--color-scrim-foreground); + + @apply border-scrim-border backdrop-blur-xl backdrop-saturate-150; +} + .fri-button--success { --fri-button-bg: var(--color-success); --fri-button-bg-hover: var(--color-success-hover); --fri-button-bg-pressed: var(--color-success-active); --fri-button-fg: var(--color-success-foreground); + + @apply border border-success-border; } .fri-button--warning { @@ -81,6 +123,8 @@ --fri-button-bg-hover: var(--color-warning-hover); --fri-button-bg-pressed: var(--color-warning-active); --fri-button-fg: var(--color-warning-foreground); + + @apply border border-warning-border; } .fri-button--danger { @@ -88,6 +132,8 @@ --fri-button-bg-hover: var(--color-danger-hover); --fri-button-bg-pressed: var(--color-danger-active); --fri-button-fg: var(--color-danger-foreground); + + @apply border border-danger-border; } .fri-button--info { @@ -95,6 +141,231 @@ --fri-button-bg-hover: var(--color-info-hover); --fri-button-bg-pressed: var(--color-info-active); --fri-button-fg: var(--color-info-foreground); + + @apply border border-info-border; +} +.fri-button--primary-soft { + --fri-button-bg: var(--color-primary-soft); + --fri-button-bg-hover: var(--color-primary-soft-hover); + --fri-button-bg-pressed: var(--color-primary-soft-active); + --fri-button-fg: var(--color-primary-soft-foreground); + + @apply border border-primary-soft-border; +} + +.fri-button--secondary-soft { + --fri-button-bg: var(--color-secondary-soft); + --fri-button-bg-hover: var(--color-secondary-soft-hover); + --fri-button-bg-pressed: var(--color-secondary-soft-active); + --fri-button-fg: var(--color-secondary-soft-foreground); + + @apply border border-secondary-soft-border; +} + +.fri-button--accent-soft { + --fri-button-bg: var(--color-accent-soft); + --fri-button-bg-hover: var(--color-accent-soft-hover); + --fri-button-bg-pressed: var(--color-accent-soft-active); + --fri-button-fg: var(--color-accent-soft-foreground); + + @apply border border-accent-soft-border; +} + +.fri-button--success-soft { + --fri-button-bg: var(--color-success-soft); + --fri-button-bg-hover: var(--color-success-soft-hover); + --fri-button-bg-pressed: var(--color-success-soft-active); + --fri-button-fg: var(--color-success-soft-foreground); + + @apply border border-success-soft-border; +} + +.fri-button--warning-soft { + --fri-button-bg: var(--color-warning-soft); + --fri-button-bg-hover: var(--color-warning-soft-hover); + --fri-button-bg-pressed: var(--color-warning-soft-active); + --fri-button-fg: var(--color-warning-soft-foreground); + + @apply border border-warning-soft-border; +} + +.fri-button--danger-soft { + --fri-button-bg: var(--color-danger-soft); + --fri-button-bg-hover: var(--color-danger-soft-hover); + --fri-button-bg-pressed: var(--color-danger-soft-active); + --fri-button-fg: var(--color-danger-soft-foreground); + + @apply border border-danger-soft-border; +} + +.fri-button--info-soft { + --fri-button-bg: var(--color-info-soft); + --fri-button-bg-hover: var(--color-info-soft-hover); + --fri-button-bg-pressed: var(--color-info-soft-active); + --fri-button-fg: var(--color-info-soft-foreground); + + @apply border border-info-soft-border; +} + +.fri-button--primary-flat { + --fri-button-bg: var(--color-primary-flat); + --fri-button-bg-hover: var(--color-primary-flat-hover); + --fri-button-bg-pressed: var(--color-primary-flat-active); + --fri-button-fg: var(--color-primary-flat-foreground); +} + +.fri-button--secondary-flat { + --fri-button-bg: var(--color-secondary-flat); + --fri-button-bg-hover: var(--color-secondary-flat-hover); + --fri-button-bg-pressed: var(--color-secondary-flat-active); + --fri-button-fg: var(--color-secondary-flat-foreground); +} + +.fri-button--accent-flat { + --fri-button-bg: var(--color-accent-flat); + --fri-button-bg-hover: var(--color-accent-flat-hover); + --fri-button-bg-pressed: var(--color-accent-flat-active); + --fri-button-fg: var(--color-accent-flat-foreground); +} + +.fri-button--success-flat { + --fri-button-bg: var(--color-success-flat); + --fri-button-bg-hover: var(--color-success-flat-hover); + --fri-button-bg-pressed: var(--color-success-flat-active); + --fri-button-fg: var(--color-success-flat-foreground); +} + +.fri-button--warning-flat { + --fri-button-bg: var(--color-warning-flat); + --fri-button-bg-hover: var(--color-warning-flat-hover); + --fri-button-bg-pressed: var(--color-warning-flat-active); + --fri-button-fg: var(--color-warning-flat-foreground); +} + +.fri-button--danger-flat { + --fri-button-bg: var(--color-danger-flat); + --fri-button-bg-hover: var(--color-danger-flat-hover); + --fri-button-bg-pressed: var(--color-danger-flat-active); + --fri-button-fg: var(--color-danger-flat-foreground); +} + +.fri-button--info-flat { + --fri-button-bg: var(--color-info-flat); + --fri-button-bg-hover: var(--color-info-flat-hover); + --fri-button-bg-pressed: var(--color-info-flat-active); + --fri-button-fg: var(--color-info-flat-foreground); +} + +.fri-button--primary-outline { + --fri-button-bg: var(--color-primary-outline); + --fri-button-bg-hover: var(--color-primary-outline-hover); + --fri-button-bg-pressed: var(--color-primary-outline-active); + --fri-button-fg: var(--color-primary-outline-foreground); + + @apply border border-primary-outline-border; +} + +.fri-button--secondary-outline { + --fri-button-bg: var(--color-secondary-outline); + --fri-button-bg-hover: var(--color-secondary-outline-hover); + --fri-button-bg-pressed: var(--color-secondary-outline-active); + --fri-button-fg: var(--color-secondary-outline-foreground); + + @apply border border-secondary-outline-border; +} + +.fri-button--accent-outline { + --fri-button-bg: var(--color-accent-outline); + --fri-button-bg-hover: var(--color-accent-outline-hover); + --fri-button-bg-pressed: var(--color-accent-outline-active); + --fri-button-fg: var(--color-accent-outline-foreground); + + @apply border border-accent-outline-border; +} + +.fri-button--success-outline { + --fri-button-bg: var(--color-success-outline); + --fri-button-bg-hover: var(--color-success-outline-hover); + --fri-button-bg-pressed: var(--color-success-outline-active); + --fri-button-fg: var(--color-success-outline-foreground); + + @apply border border-success-outline-border; +} + +.fri-button--warning-outline { + --fri-button-bg: var(--color-warning-outline); + --fri-button-bg-hover: var(--color-warning-outline-hover); + --fri-button-bg-pressed: var(--color-warning-outline-active); + --fri-button-fg: var(--color-warning-outline-foreground); + + @apply border border-warning-outline-border; +} + +.fri-button--danger-outline { + --fri-button-bg: var(--color-danger-outline); + --fri-button-bg-hover: var(--color-danger-outline-hover); + --fri-button-bg-pressed: var(--color-danger-outline-active); + --fri-button-fg: var(--color-danger-outline-foreground); + + @apply border border-danger-outline-border; +} + +.fri-button--info-outline { + --fri-button-bg: var(--color-info-outline); + --fri-button-bg-hover: var(--color-info-outline-hover); + --fri-button-bg-pressed: var(--color-info-outline-active); + --fri-button-fg: var(--color-info-outline-foreground); + + @apply border border-info-outline-border; +} + +.fri-button--primary-ghost { + --fri-button-bg: var(--color-primary-ghost); + --fri-button-bg-hover: var(--color-primary-ghost-hover); + --fri-button-bg-pressed: var(--color-primary-ghost-active); + --fri-button-fg: var(--color-primary-ghost-foreground); +} + +.fri-button--secondary-ghost { + --fri-button-bg: var(--color-secondary-ghost); + --fri-button-bg-hover: var(--color-secondary-ghost-hover); + --fri-button-bg-pressed: var(--color-secondary-ghost-active); + --fri-button-fg: var(--color-secondary-ghost-foreground); +} + +.fri-button--accent-ghost { + --fri-button-bg: var(--color-accent-ghost); + --fri-button-bg-hover: var(--color-accent-ghost-hover); + --fri-button-bg-pressed: var(--color-accent-ghost-active); + --fri-button-fg: var(--color-accent-ghost-foreground); +} + +.fri-button--success-ghost { + --fri-button-bg: var(--color-success-ghost); + --fri-button-bg-hover: var(--color-success-ghost-hover); + --fri-button-bg-pressed: var(--color-success-ghost-active); + --fri-button-fg: var(--color-success-ghost-foreground); +} + +.fri-button--warning-ghost { + --fri-button-bg: var(--color-warning-ghost); + --fri-button-bg-hover: var(--color-warning-ghost-hover); + --fri-button-bg-pressed: var(--color-warning-ghost-active); + --fri-button-fg: var(--color-warning-ghost-foreground); +} + +.fri-button--danger-ghost { + --fri-button-bg: var(--color-danger-ghost); + --fri-button-bg-hover: var(--color-danger-ghost-hover); + --fri-button-bg-pressed: var(--color-danger-ghost-active); + --fri-button-fg: var(--color-danger-ghost-foreground); +} + +.fri-button--info-ghost { + --fri-button-bg: var(--color-info-ghost); + --fri-button-bg-hover: var(--color-info-ghost-hover); + --fri-button-bg-pressed: var(--color-info-ghost-active); + --fri-button-fg: var(--color-info-ghost-foreground); } .fri-button--size-sm { diff --git a/packages/styles/src/components/surface.css b/packages/styles/src/components/surface.css index 011f355..3fdf9bb 100644 --- a/packages/styles/src/components/surface.css +++ b/packages/styles/src/components/surface.css @@ -1,4 +1,5 @@ .fri-surface { + @apply border border-transparent; @apply bg-(--color-surface) text-(--color-surface-foreground); @apply rounded-md; } @@ -7,16 +8,48 @@ @apply bg-(--color-surface) text-(--color-surface-foreground); } -.fri-surface--bordered { - @apply border border-border bg-transparent; +.fri-surface--default-bordered { + @apply border-(--color-surface-border) bg-(--color-surface) text-(--color-surface-foreground); +} + +.fri-surface--plain { + @apply bg-(--color-surface-plain) text-(--color-surface-plain-foreground); +} + +.fri-surface--plain-bordered { + @apply border-(--color-surface-plain-border) bg-(--color-surface-plain) text-(--color-surface-plain-foreground); } .fri-surface--glass { - @apply bg-surface-glass backdrop-blur-md; + @apply border-glass-border bg-glass text-glass-foreground backdrop-blur-xl backdrop-saturate-200; +} + +.fri-surface--frost { + @apply bg-glass text-glass-foreground backdrop-blur-2xl backdrop-saturate-200; +} + +.fri-surface--overlay { + @apply border-scrim-border bg-scrim text-scrim-foreground backdrop-blur-xl backdrop-saturate-150; +} + +.fri-surface--shadow-none { + @apply shadow-none; +} + +.fri-surface--shadow-sm { + @apply shadow-sm; +} + +.fri-surface--shadow-md { + @apply shadow-md; +} + +.fri-surface--shadow-lg { + @apply shadow-lg; } -.fri-surface--primary { - @apply bg-primary text-primary-foreground; +.fri-surface--shadow-xl { + @apply shadow-xl; } .fri-surface--radius-none { diff --git a/packages/styles/src/tokens/colors.css b/packages/styles/src/tokens/colors.css index 6fbd824..2de06b2 100644 --- a/packages/styles/src/tokens/colors.css +++ b/packages/styles/src/tokens/colors.css @@ -1,224 +1,260 @@ @custom-variant dark (&:is(.dark, [data-theme="dark"]) *); @theme static { - --color-primary: var(--color-neutral-950); - --color-primary-hover: var(--color-neutral-800); - --color-primary-active: var(--color-neutral-700); - --color-primary-foreground: var(--color-white); - --color-primary-pale: var(--color-neutral-100); - --color-primary-pale-foreground: var(--color-neutral-800); - --color-primary-pale-border: var(--color-neutral-200); - --color-primary-shadow: 0 2px 3px 0 rgba(10, 10, 10, 0.25), 0 3px 5px 0 rgba(10, 10, 10, 0.3); - - --color-secondary: var(--color-neutral-100); - --color-secondary-hover: var(--color-neutral-200); - --color-secondary-active: var(--color-neutral-300); - --color-secondary-foreground: var(--color-neutral-900); - --color-secondary-pale: var(--color-neutral-50); - --color-secondary-pale-foreground: var(--color-neutral-700); - --color-secondary-pale-border: var(--color-neutral-200); - --color-secondary-shadow: 0 2px 3px 0 rgba(115, 115, 115, 0.25), 0 3px 5px 0 rgba(115, 115, 115, 0.3); - - --color-success: var(--color-green-600); - --color-success-hover: var(--color-green-700); - --color-success-active: var(--color-green-800); - --color-success-foreground: var(--color-white); - --color-success-pale: var(--color-green-50); - --color-success-pale-foreground: var(--color-green-800); - --color-success-pale-border: var(--color-green-200); - --color-success-shadow: 0 2px 3px 0 rgba(22, 163, 74, 0.25), 0 3px 5px 0 rgba(22, 163, 74, 0.3); - - --color-warning: var(--color-amber-500); - --color-warning-hover: var(--color-amber-600); - --color-warning-active: var(--color-amber-700); - --color-warning-foreground: var(--color-neutral-950); - --color-warning-pale: var(--color-amber-50); - --color-warning-pale-foreground: var(--color-amber-800); - --color-warning-pale-border: var(--color-amber-200); - --color-warning-shadow: 0 2px 3px 0 rgba(245, 158, 11, 0.25), 0 3px 5px 0 rgba(245, 158, 11, 0.3); - - --color-danger: var(--color-rose-600); - --color-danger-hover: var(--color-rose-700); - --color-danger-active: var(--color-rose-800); - --color-danger-foreground: var(--color-white); - --color-danger-pale: var(--color-rose-50); - --color-danger-pale-foreground: var(--color-rose-800); - --color-danger-pale-border: var(--color-rose-200); - --color-danger-shadow: 0 2px 3px 0 rgba(225, 29, 72, 0.25), 0 3px 5px 0 rgba(225, 29, 72, 0.3); - - --color-info: var(--color-blue-600); - --color-info-hover: var(--color-blue-700); - --color-info-active: var(--color-blue-800); - --color-info-foreground: var(--color-white); - --color-info-pale: var(--color-blue-50); - --color-info-pale-foreground: var(--color-blue-800); - --color-info-pale-border: var(--color-blue-200); - --color-info-shadow: 0 2px 3px 0 rgba(37, 99, 235, 0.25), 0 3px 5px 0 rgba(37, 99, 235, 0.3); - - --color-background: var(--color-white); - --color-background-secondary: var(--color-neutral-50); - --color-foreground: var(--color-neutral-950); - - --color-surface: var(--color-neutral-100); - --color-surface-hover: var(--color-neutral-200); - --color-surface-active: var(--color-neutral-300); - --color-surface-foreground: var(--color-neutral-900); - --color-surface-shadow: 0 3px 6px 0 rgba(0, 0, 0, 0.08), 0 0 0 1px rgba(0, 0, 0, 0.05); - --color-surface-glass: rgba(255, 255, 255, 0.8); - - --color-popover: var(--color-white); - --color-popover-foreground: var(--color-neutral-950); - - --color-muted: var(--color-neutral-100); - --color-muted-hover: var(--color-neutral-200); - --color-muted-active: var(--color-neutral-300); - --color-muted-foreground: var(--color-neutral-500); - - --color-separator: var(--color-neutral-200); - --color-skeleton: var(--color-neutral-200); - - --color-disabled: var(--color-neutral-100); - --color-disabled-foreground: var(--color-neutral-400); - - --color-heading: var(--color-neutral-950); - --color-foreground-secondary: var(--color-neutral-600); - --color-foreground-muted: var(--color-neutral-400); - - --color-code: var(--color-neutral-800); - --color-code-background: var(--color-neutral-100); - - --color-mark: var(--color-amber-200); - --color-mark-foreground: var(--color-neutral-950); - - --color-border: var(--color-neutral-200); - --color-border-hover: var(--color-neutral-300); - - --color-field: var(--color-white); - --color-field-hover: var(--color-neutral-50); - --color-field-foreground: var(--color-neutral-950); - --color-field-border: var(--color-neutral-300); - --color-field-border-hover: var(--color-neutral-400); - --color-field-placeholder: var(--color-neutral-400); - - --color-selection: var(--color-neutral-200); - --color-selection-foreground: var(--color-neutral-950); - - --color-link: var(--color-neutral-950); - --color-link-hover: var(--color-neutral-600); - - --color-focus-outer: var(--color-neutral-950); - --color-focus-inner: var(--color-white); + --color-primary: var(--color-fri-neutral-900); + --color-primary-hover: var(--color-fri-neutral-700); + --color-primary-active: var(--color-fri-neutral-950); + --color-primary-border: transparent; + --color-primary-foreground: oklch(100% 0 0); + --color-primary-soft: var(--color-fri-neutral-100); + --color-primary-soft-hover: var(--color-fri-neutral-200); + --color-primary-soft-active: var(--color-fri-neutral-300); + --color-primary-soft-border: var(--color-fri-neutral-300); + --color-primary-soft-foreground: oklch(0% 0 0); + --color-primary-flat: var(--color-fri-neutral-100); + --color-primary-flat-hover: var(--color-fri-neutral-200); + --color-primary-flat-active: var(--color-fri-neutral-300); + --color-primary-flat-border: transparent; + --color-primary-flat-foreground: oklch(0% 0 0); + --color-primary-outline: transparent; + --color-primary-outline-hover: var(--color-fri-neutral-100); + --color-primary-outline-active: var(--color-fri-neutral-200); + --color-primary-outline-border: var(--color-fri-neutral-900); + --color-primary-outline-foreground: oklch(0% 0 0); + --color-primary-ghost: transparent; + --color-primary-ghost-hover: var(--color-fri-neutral-100); + --color-primary-ghost-active: var(--color-fri-neutral-200); + --color-primary-ghost-border: transparent; + --color-primary-ghost-foreground: oklch(0% 0 0); + + --color-secondary: var(--color-fri-neutral-100); + --color-secondary-hover: var(--color-fri-neutral-200); + --color-secondary-active: var(--color-fri-neutral-300); + --color-secondary-border: transparent; + --color-secondary-foreground: var(--color-fri-neutral-950); + --color-secondary-soft: var(--color-fri-neutral-50); + --color-secondary-soft-hover: var(--color-fri-neutral-100); + --color-secondary-soft-active: var(--color-fri-neutral-200); + --color-secondary-soft-border: var(--color-fri-neutral-200); + --color-secondary-soft-foreground: var(--color-fri-neutral-600); + --color-secondary-flat: var(--color-fri-neutral-50); + --color-secondary-flat-hover: var(--color-fri-neutral-100); + --color-secondary-flat-active: var(--color-fri-neutral-200); + --color-secondary-flat-border: transparent; + --color-secondary-flat-foreground: var(--color-fri-neutral-600); + --color-secondary-outline: transparent; + --color-secondary-outline-hover: var(--color-fri-neutral-50); + --color-secondary-outline-active: var(--color-fri-neutral-100); + --color-secondary-outline-border: var(--color-fri-neutral-500); + --color-secondary-outline-foreground: var(--color-fri-neutral-600); + --color-secondary-ghost: transparent; + --color-secondary-ghost-hover: var(--color-fri-neutral-50); + --color-secondary-ghost-active: var(--color-fri-neutral-100); + --color-secondary-ghost-border: transparent; + --color-secondary-ghost-foreground: var(--color-fri-neutral-600); + + --color-accent: var(--color-fri-purple-500); + --color-accent-hover: var(--color-fri-purple-600); + --color-accent-active: var(--color-fri-purple-700); + --color-accent-border: transparent; + --color-accent-foreground: oklch(100% 0 0); + --color-accent-soft: var(--color-fri-purple-50); + --color-accent-soft-hover: var(--color-fri-purple-100); + --color-accent-soft-active: var(--color-fri-purple-200); + --color-accent-soft-border: var(--color-fri-purple-300); + --color-accent-soft-foreground: var(--color-fri-purple-700); + --color-accent-flat: var(--color-fri-purple-100); + --color-accent-flat-hover: var(--color-fri-purple-200); + --color-accent-flat-active: var(--color-fri-purple-300); + --color-accent-flat-border: transparent; + --color-accent-flat-foreground: var(--color-fri-purple-600); + --color-accent-outline: transparent; + --color-accent-outline-hover: var(--color-fri-purple-50); + --color-accent-outline-active: var(--color-fri-purple-100); + --color-accent-outline-border: var(--color-fri-purple-500); + --color-accent-outline-foreground: var(--color-fri-purple-500); + --color-accent-ghost: transparent; + --color-accent-ghost-hover: var(--color-fri-purple-50); + --color-accent-ghost-active: var(--color-fri-purple-100); + --color-accent-ghost-border: transparent; + --color-accent-ghost-foreground: var(--color-fri-purple-500); + + --color-success: var(--color-fri-green-500); + --color-success-hover: var(--color-fri-green-600); + --color-success-active: var(--color-fri-green-700); + --color-success-border: transparent; + --color-success-foreground: oklch(100% 0 0); + --color-success-soft: var(--color-fri-green-50); + --color-success-soft-hover: var(--color-fri-green-100); + --color-success-soft-active: var(--color-fri-green-200); + --color-success-soft-border: var(--color-fri-green-300); + --color-success-soft-foreground: var(--color-fri-green-700); + --color-success-flat: var(--color-fri-green-100); + --color-success-flat-hover: var(--color-fri-green-200); + --color-success-flat-active: var(--color-fri-green-300); + --color-success-flat-border: transparent; + --color-success-flat-foreground: var(--color-fri-green-600); + --color-success-outline: transparent; + --color-success-outline-hover: var(--color-fri-green-50); + --color-success-outline-active: var(--color-fri-green-100); + --color-success-outline-border: var(--color-fri-green-500); + --color-success-outline-foreground: var(--color-fri-green-500); + --color-success-ghost: transparent; + --color-success-ghost-hover: var(--color-fri-green-50); + --color-success-ghost-active: var(--color-fri-green-100); + --color-success-ghost-border: transparent; + --color-success-ghost-foreground: var(--color-fri-green-500); + + --color-warning: var(--color-fri-yellow-500); + --color-warning-hover: var(--color-fri-yellow-600); + --color-warning-active: var(--color-fri-yellow-700); + --color-warning-border: transparent; + --color-warning-foreground: oklch(100% 0 0); + --color-warning-soft: var(--color-fri-yellow-50); + --color-warning-soft-hover: var(--color-fri-yellow-100); + --color-warning-soft-active: var(--color-fri-yellow-200); + --color-warning-soft-border: var(--color-fri-yellow-300); + --color-warning-soft-foreground: var(--color-fri-yellow-700); + --color-warning-flat: var(--color-fri-yellow-100); + --color-warning-flat-hover: var(--color-fri-yellow-200); + --color-warning-flat-active: var(--color-fri-yellow-300); + --color-warning-flat-border: transparent; + --color-warning-flat-foreground: var(--color-fri-yellow-600); + --color-warning-outline: transparent; + --color-warning-outline-hover: var(--color-fri-yellow-50); + --color-warning-outline-active: var(--color-fri-yellow-100); + --color-warning-outline-border: var(--color-fri-yellow-500); + --color-warning-outline-foreground: var(--color-fri-yellow-500); + --color-warning-ghost: transparent; + --color-warning-ghost-hover: var(--color-fri-yellow-50); + --color-warning-ghost-active: var(--color-fri-yellow-100); + --color-warning-ghost-border: transparent; + --color-warning-ghost-foreground: var(--color-fri-yellow-500); + + --color-danger: var(--color-fri-red-500); + --color-danger-hover: var(--color-fri-red-600); + --color-danger-active: var(--color-fri-red-700); + --color-danger-border: transparent; + --color-danger-foreground: oklch(100% 0 0); + --color-danger-soft: var(--color-fri-red-50); + --color-danger-soft-hover: var(--color-fri-red-100); + --color-danger-soft-active: var(--color-fri-red-200); + --color-danger-soft-border: var(--color-fri-red-300); + --color-danger-soft-foreground: var(--color-fri-red-700); + --color-danger-flat: var(--color-fri-red-100); + --color-danger-flat-hover: var(--color-fri-red-200); + --color-danger-flat-active: var(--color-fri-red-300); + --color-danger-flat-border: transparent; + --color-danger-flat-foreground: var(--color-fri-red-600); + --color-danger-outline: transparent; + --color-danger-outline-hover: var(--color-fri-red-50); + --color-danger-outline-active: var(--color-fri-red-100); + --color-danger-outline-border: var(--color-fri-red-500); + --color-danger-outline-foreground: var(--color-fri-red-500); + --color-danger-ghost: transparent; + --color-danger-ghost-hover: var(--color-fri-red-50); + --color-danger-ghost-active: var(--color-fri-red-100); + --color-danger-ghost-border: transparent; + --color-danger-ghost-foreground: var(--color-fri-red-500); + + --color-info: var(--color-fri-blue-500); + --color-info-hover: var(--color-fri-blue-600); + --color-info-active: var(--color-fri-blue-700); + --color-info-border: transparent; + --color-info-foreground: oklch(100% 0 0); + --color-info-soft: var(--color-fri-blue-50); + --color-info-soft-hover: var(--color-fri-blue-100); + --color-info-soft-active: var(--color-fri-blue-200); + --color-info-soft-border: var(--color-fri-blue-300); + --color-info-soft-foreground: var(--color-fri-blue-700); + --color-info-flat: var(--color-fri-blue-100); + --color-info-flat-hover: var(--color-fri-blue-200); + --color-info-flat-active: var(--color-fri-blue-300); + --color-info-flat-border: transparent; + --color-info-flat-foreground: var(--color-fri-blue-600); + --color-info-outline: transparent; + --color-info-outline-hover: var(--color-fri-blue-50); + --color-info-outline-active: var(--color-fri-blue-100); + --color-info-outline-border: var(--color-fri-blue-500); + --color-info-outline-foreground: var(--color-fri-blue-500); + --color-info-ghost: transparent; + --color-info-ghost-hover: var(--color-fri-blue-50); + --color-info-ghost-active: var(--color-fri-blue-100); + --color-info-ghost-border: transparent; + --color-info-ghost-foreground: var(--color-fri-blue-500); + + --color-background: oklch(100% 0 0); + --color-background-secondary: oklch(98.875% 0 0); + --color-foreground: oklch(0% 0 0); + --color-foreground-inverted: oklch(100% 0 0); + + --color-surface: oklch(97.75% 0 0); + --color-surface-border: var(--color-fri-neutral-100); + --color-surface-foreground: oklch(0% 0 0); + --color-surface-plain: oklch(100% 0 0); + --color-surface-plain-border: var(--color-fri-neutral-100); + --color-surface-plain-foreground: oklch(0% 0 0); + + --color-popover: oklch(100% 0 0); + --color-popover-foreground: oklch(0% 0 0); + + --color-muted: var(--color-fri-neutral-100); + --color-muted-hover: var(--color-fri-neutral-200); + --color-muted-active: var(--color-fri-neutral-300); + --color-muted-foreground: var(--color-fri-neutral-500); + + --color-disabled: var(--color-fri-neutral-100); + --color-disabled-foreground: var(--color-fri-neutral-400); + + --color-heading: oklch(0% 0 0); + --color-foreground-secondary: var(--color-fri-neutral-700); + --color-foreground-muted: var(--color-fri-neutral-500); + + --color-code: var(--color-fri-neutral-800); + --color-code-background: var(--color-fri-neutral-100); + + --color-mark: var(--color-fri-yellow-200); + --color-mark-foreground: oklch(0% 0 0); + + --color-border: var(--color-fri-neutral-200); + --color-border-hover: var(--color-fri-neutral-300); + + --color-separator: var(--color-fri-neutral-200); + --color-skeleton: var(--color-fri-neutral-200); + + --color-field: oklch(100% 0 0); + --color-field-hover: var(--color-fri-neutral-50); + --color-field-border: var(--color-fri-neutral-300); + --color-field-border-hover: var(--color-fri-neutral-400); + --color-field-foreground: oklch(0% 0 0); + --color-field-placeholder: var(--color-fri-neutral-400); + + --color-selection: var(--color-fri-neutral-200); + --color-selection-foreground: oklch(0% 0 0); + + --color-link: oklch(0% 0 0); + --color-link-hover: var(--color-fri-neutral-700); + + --color-focus-outer: oklch(0% 0 0); + --color-focus-inner: oklch(100% 0 0); --color-overlay: oklch(0% 0 0 / 50%); + + --color-glass: oklch(100% 0 0 / 0.15); + --color-glass-hover: oklch(100% 0 0 / 0.25); + --color-glass-active: oklch(100% 0 0 / 0.35); + --color-glass-border: oklch(100% 0 0 / 0.2); + --color-glass-foreground: oklch(100% 0 0); + + --color-scrim: oklch(0% 0 0 / 0.6); + --color-scrim-hover: oklch(0% 0 0 / 0.75); + --color-scrim-active: oklch(0% 0 0 / 0.85); + --color-scrim-border: oklch(100% 0 0 / 0.1); + --color-scrim-foreground: oklch(100% 0 0); } @layer base { .dark, [data-theme="dark"] { - --color-primary: var(--color-neutral-50); - --color-primary-hover: var(--color-neutral-200); - --color-primary-active: var(--color-neutral-300); - --color-primary-foreground: var(--color-neutral-950); - --color-primary-pale: var(--color-neutral-900); - --color-primary-pale-foreground: var(--color-neutral-200); - --color-primary-pale-border: var(--color-neutral-800); - --color-primary-shadow: 0 2px 3px 0 rgba(245, 245, 245, 0.25), 0 3px 14px 4px rgba(245, 245, 245, 0.15); - - --color-secondary: var(--color-neutral-800); - --color-secondary-hover: var(--color-neutral-700); - --color-secondary-active: var(--color-neutral-600); - --color-secondary-foreground: var(--color-neutral-50); - --color-secondary-pale: var(--color-neutral-900); - --color-secondary-pale-foreground: var(--color-neutral-300); - --color-secondary-pale-border: var(--color-neutral-700); - --color-secondary-shadow: 0 2px 3px 0 rgba(115, 115, 115, 0.5), 0 3px 14px 4px rgba(115, 115, 115, 0.3); - - --color-success: var(--color-green-500); - --color-success-hover: var(--color-green-600); - --color-success-active: var(--color-green-700); - --color-success-foreground: var(--color-white); - --color-success-pale: var(--color-green-900); - --color-success-pale-foreground: var(--color-green-200); - --color-success-pale-border: var(--color-green-700); - --color-success-shadow: 0 2px 3px 0 rgba(22, 163, 74, 0.5), 0 3px 14px 4px rgba(22, 163, 74, 0.3); - - --color-warning: var(--color-amber-500); - --color-warning-hover: var(--color-amber-600); - --color-warning-active: var(--color-amber-700); - --color-warning-foreground: var(--color-neutral-950); - --color-warning-pale: var(--color-amber-900); - --color-warning-pale-foreground: var(--color-amber-200); - --color-warning-pale-border: var(--color-amber-700); - --color-warning-shadow: 0 2px 3px 0 rgba(245, 158, 11, 0.5), 0 3px 14px 4px rgba(245, 158, 11, 0.3); - - --color-danger: var(--color-rose-500); - --color-danger-hover: var(--color-rose-600); - --color-danger-active: var(--color-rose-700); - --color-danger-foreground: var(--color-white); - --color-danger-pale: var(--color-rose-900); - --color-danger-pale-foreground: var(--color-rose-200); - --color-danger-pale-border: var(--color-rose-700); - --color-danger-shadow: 0 2px 3px 0 rgba(225, 29, 72, 0.5), 0 3px 14px 4px rgba(225, 29, 72, 0.3); - - --color-info: var(--color-blue-500); - --color-info-hover: var(--color-blue-600); - --color-info-active: var(--color-blue-700); - --color-info-foreground: var(--color-white); - --color-info-pale: var(--color-blue-900); - --color-info-pale-foreground: var(--color-blue-200); - --color-info-pale-border: var(--color-blue-700); - --color-info-shadow: 0 2px 3px 0 rgba(37, 99, 235, 0.5), 0 3px 14px 4px rgba(37, 99, 235, 0.3); - - --color-background: var(--color-black); - --color-background-secondary: var(--color-neutral-950); - --color-foreground: var(--color-neutral-50); - - --color-surface: var(--color-neutral-900); - --color-surface-hover: var(--color-neutral-800); - --color-surface-active: var(--color-neutral-700); - --color-surface-foreground: var(--color-neutral-50); - --color-surface-shadow: 0 3px 6px 0 rgba(0, 0, 0, 0.08), inset 0 0 0 1px rgba(255, 255, 255, 0.07); - --color-surface-glass: rgba(23, 23, 23, 0.8); - - --color-popover: var(--color-neutral-800); - --color-popover-foreground: var(--color-neutral-50); - - --color-muted: var(--color-neutral-800); - --color-muted-hover: var(--color-neutral-700); - --color-muted-active: var(--color-neutral-600); - --color-muted-foreground: var(--color-neutral-400); - - --color-separator: var(--color-neutral-700); - --color-skeleton: var(--color-neutral-600); - - --color-disabled: var(--color-neutral-900); - --color-disabled-foreground: var(--color-neutral-500); - - --color-heading: var(--color-neutral-50); - --color-foreground-secondary: var(--color-neutral-400); - --color-foreground-muted: var(--color-neutral-500); - --color-code: var(--color-neutral-200); - --color-code-background: var(--color-neutral-800); - --color-mark: var(--color-amber-800); - --color-mark-foreground: var(--color-amber-100); - - --color-border: var(--color-neutral-800); - --color-border-hover: var(--color-neutral-700); - - --color-field: var(--color-neutral-800); - --color-field-hover: var(--color-neutral-700); - --color-field-foreground: var(--color-neutral-50); - --color-field-border: var(--color-neutral-600); - --color-field-border-hover: var(--color-neutral-500); - --color-field-placeholder: var(--color-neutral-500); - - --color-selection: oklch(from var(--color-blue-500) l c h / 10%); - --color-selection-foreground: var(--color-blue-500); - - --color-link: var(--color-blue-500); - --color-link-hover: var(--color-blue-400); - --color-focus-outer: var(--color-neutral-50); - --color-focus-inner: var(--color-neutral-950); - --color-overlay: oklch(0% 0 0 / 70%); } } diff --git a/packages/styles/src/tokens/index.css b/packages/styles/src/tokens/index.css index 51c5658..45472a4 100644 --- a/packages/styles/src/tokens/index.css +++ b/packages/styles/src/tokens/index.css @@ -1,4 +1,4 @@ +@import "./palette.css"; @import "./colors.css"; -@import "./typography.css"; @import "./layout.css"; @import "./motion.css"; diff --git a/packages/styles/src/tokens/layout.css b/packages/styles/src/tokens/layout.css index 883e086..fa455a2 100644 --- a/packages/styles/src/tokens/layout.css +++ b/packages/styles/src/tokens/layout.css @@ -7,7 +7,7 @@ --cursor-interactive: pointer; --cursor-disabled: not-allowed; - --z-floating: 10; + --z-popover: 10; --z-navigation: 20; --z-overlay: 30; --z-dialog: 40; diff --git a/packages/styles/src/tokens/motion.css b/packages/styles/src/tokens/motion.css index 099d150..34ef32b 100644 --- a/packages/styles/src/tokens/motion.css +++ b/packages/styles/src/tokens/motion.css @@ -1,9 +1,9 @@ @theme { - --duration-instant: 100ms; - --duration-fast: 150ms; - --duration-normal: 200ms; - --duration-slow: 300ms; - + --duration-press: 80ms; + --duration-toggle: 100ms; + --duration-hover: 150ms; + --duration-reveal: 200ms; + --duration-enter: 300ms; --ease-smooth: cubic-bezier(0.4, 0, 0.2, 1); --ease-in: cubic-bezier(0.4, 0, 1, 1); --ease-out: cubic-bezier(0, 0, 0.2, 1); @@ -68,9 +68,11 @@ @media (prefers-reduced-motion: reduce) { :root { - --duration-fast: 0ms; - --duration-normal: 0ms; - --duration-slow: 0ms; + --duration-press: 0ms; + --duration-toggle: 0ms; + --duration-hover: 0ms; + --duration-reveal: 0ms; + --duration-enter: 0ms; --animate-fade-in: none; --animate-fade-out: none; --animate-slide-in-up: none; diff --git a/packages/styles/src/tokens/palette.css b/packages/styles/src/tokens/palette.css new file mode 100644 index 0000000..ca1cd28 --- /dev/null +++ b/packages/styles/src/tokens/palette.css @@ -0,0 +1,73 @@ +@theme { + --color-fri-red-50: oklch(0.958 0.016 12.9); + --color-fri-red-100: oklch(0.926 0.03 15.21); + --color-fri-red-200: oklch(0.843 0.068 16.38); + --color-fri-red-300: oklch(0.772 0.108 17.73); + --color-fri-red-400: oklch(0.691 0.162 19.06); + --color-fri-red-500: oklch(0.62 0.214 22.35); + --color-fri-red-600: oklch(0.522 0.18 22.17); + --color-fri-red-700: oklch(0.433 0.149 22.72); + --color-fri-red-800: oklch(0.332 0.114 21.97); + --color-fri-red-900: oklch(0.243 0.083 22.44); + --color-fri-red-950: oklch(0.184 0.065 22.44); + + --color-fri-yellow-50: oklch(0.983 0.011 63.39); + --color-fri-yellow-100: oklch(0.966 0.023 65.47); + --color-fri-yellow-200: oklch(0.933 0.049 70.1); + --color-fri-yellow-300: oklch(0.899 0.079 72.08); + --color-fri-yellow-400: oklch(0.858 0.125 76.45); + --color-fri-yellow-500: oklch(0.827 0.171 80.47); + --color-fri-yellow-600: oklch(0.685 0.142 80.01); + --color-fri-yellow-700: oklch(0.554 0.115 80.54); + --color-fri-yellow-800: oklch(0.416 0.086 80.63); + --color-fri-yellow-900: oklch(0.279 0.058 80.95); + --color-fri-yellow-950: oklch(0.208 0.043 79.58); + + --color-fri-blue-50: oklch(0.957 0.02 277); + --color-fri-blue-100: oklch(0.925 0.036 273.72); + --color-fri-blue-200: oklch(0.839 0.078 271.81); + --color-fri-blue-300: oklch(0.762 0.119 269.22); + --color-fri-blue-400: oklch(0.678 0.168 264.63); + --color-fri-blue-500: oklch(0.601 0.215 258.43); + --color-fri-blue-600: oklch(0.518 0.187 258.08); + --color-fri-blue-700: oklch(0.428 0.156 258.25); + --color-fri-blue-800: oklch(0.332 0.121 258.18); + --color-fri-blue-900: oklch(0.236 0.084 258.02); + --color-fri-blue-950: oklch(0.18 0.064 257.25); + + --color-fri-green-50: oklch(0.969 0.036 160.6); + --color-fri-green-100: oklch(0.94 0.078 161.58); + --color-fri-green-200: oklch(0.869 0.149 161.59); + --color-fri-green-300: oklch(0.82 0.141 161.54); + --color-fri-green-400: oklch(0.761 0.131 161.57); + --color-fri-green-500: oklch(0.71 0.122 161.37); + --color-fri-green-600: oklch(0.653 0.112 161.43); + --color-fri-green-700: oklch(0.524 0.09 161.2); + --color-fri-green-800: oklch(0.397 0.068 161.34); + --color-fri-green-900: oklch(0.261 0.046 161.11); + --color-fri-green-950: oklch(0.201 0.033 163.31); + + --color-fri-purple-50: oklch(0.949 0.02 292.21); + --color-fri-purple-100: oklch(0.908 0.037 290.79); + --color-fri-purple-200: oklch(0.815 0.077 292.11); + --color-fri-purple-300: oklch(0.716 0.121 291.23); + --color-fri-purple-400: oklch(0.626 0.161 289.72); + --color-fri-purple-500: oklch(0.536 0.197 288.49); + --color-fri-purple-600: oklch(0.462 0.2 287.51); + --color-fri-purple-700: oklch(0.38 0.165 287.65); + --color-fri-purple-800: oklch(0.308 0.134 287.32); + --color-fri-purple-900: oklch(0.226 0.098 287.24); + --color-fri-purple-950: oklch(0.172 0.076 287.56); + + --color-fri-neutral-50: #f2f2f2; + --color-fri-neutral-100: #e2e2e2; + --color-fri-neutral-200: #c6c6c6; + --color-fri-neutral-300: #ababab; + --color-fri-neutral-400: #919191; + --color-fri-neutral-500: #777777; + --color-fri-neutral-600: #5e5e5e; + --color-fri-neutral-700: #474747; + --color-fri-neutral-800: #303030; + --color-fri-neutral-900: #1b1b1b; + --color-fri-neutral-950: #111111; +} diff --git a/packages/styles/src/tokens/typography.css b/packages/styles/src/tokens/typography.css deleted file mode 100644 index 079bb6f..0000000 --- a/packages/styles/src/tokens/typography.css +++ /dev/null @@ -1,8 +0,0 @@ -@theme { - --heading-h1: 2.0625rem; - --heading-h2: 1.625rem; - --heading-h3: 1.25rem; - --heading-h4: 1.125rem; - --heading-h5: 1rem; - --heading-h6: 0.875rem; -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eddc56d..6bef44c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -139,6 +139,9 @@ importers: '@repo/quality': specifier: workspace:* version: link:../../packages/quality + '@storybook/addon-a11y': + specifier: ^10.3.5 + version: 10.3.5(storybook@10.3.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)) '@storybook/addon-docs': specifier: ^10.3.3 version: 10.3.3(@types/react@19.2.2)(esbuild@0.27.4)(rollup@4.60.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@8.0.8(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3)) @@ -2464,6 +2467,11 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@storybook/addon-a11y@10.3.5': + resolution: {integrity: sha512-5k6lpgfIeLxvNhE8v3wEzdiu73ONKjF4gmH1AHvfqYd8kIVzQJai0KCDxgvqNncXHQhIWkaf1fg6+9hKaYJyaw==} + peerDependencies: + storybook: ^10.3.5 + '@storybook/addon-docs@10.3.3': resolution: {integrity: sha512-trJQTpOtuOEuNv1Rn8X2Sopp5hSPpb0u0soEJ71BZAbxe4d2Y1d/1MYcxBdRKwncum6sCTsnxTpqQ/qvSJKlTQ==} peerDependencies: @@ -3075,6 +3083,10 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} + axe-core@4.11.3: + resolution: {integrity: sha512-zBQouZixDTbo3jMGqHKyePxYxr1e5W8UdTmBQ7sNtaA9M2bE32daxxPLS/jojhKOHxQ7LWwPjfiwf/fhaJWzlg==} + engines: {node: '>=4'} + bail@2.0.2: resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} @@ -8640,6 +8652,12 @@ snapshots: '@standard-schema/spec@1.1.0': {} + '@storybook/addon-a11y@10.3.5(storybook@10.3.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))': + dependencies: + '@storybook/global': 5.0.0 + axe-core: 4.11.3 + storybook: 10.3.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@storybook/addon-docs@10.3.3(@types/react@19.2.2)(esbuild@0.27.4)(rollup@4.60.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@8.0.8(@types/node@22.15.3)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@mdx-js/react': 3.1.1(@types/react@19.2.2)(react@19.2.0) @@ -9402,6 +9420,8 @@ snapshots: dependencies: possible-typed-array-names: 1.1.0 + axe-core@4.11.3: {} + bail@2.0.2: {} balanced-match@4.0.4: {} diff --git a/turbo/generators/interactive-component/component.css.hbs b/turbo/generators/interactive-component/component.css.hbs index dbccb4a..abc0731 100644 --- a/turbo/generators/interactive-component/component.css.hbs +++ b/turbo/generators/interactive-component/component.css.hbs @@ -2,7 +2,7 @@ @apply inline-flex items-center justify-center; @apply leading-none font-medium whitespace-nowrap; @apply cursor-pointer border border-transparent select-none no-highlight; - @apply transition-colors duration-(--duration-fast) ease-smooth motion-reduce:transition-none; + @apply transition-colors duration-(--duration-hover) ease-out motion-reduce:transition-none; --fri-{{dashCase name}}-bg: var(--color-primary); --fri-{{dashCase name}}-bg-hover: var(--color-primary-hover); From d37134d7f5b930d66f5681ccf628cdf6c5c72aca Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Sun, 19 Apr 2026 05:29:31 +0700 Subject: [PATCH 065/199] feat(react): Add Avatar component and fix style cascade - Add Avatar with compound API (Avatar.Image, Avatar.Fallback) wrapping @radix-ui/react-avatar - Variants: size (sm/md/lg/xl/2xl), radius (none/sm/md/lg/full), isDisabled - Add --color-avatar tokens for designer override - Add UserIcon for fallback content - Wrap all component CSS in @layer components so className utilities win over component modifiers - Fix type error: extend icon SVG props with slot?: string (SVGAttributes lacks slot) - Register Avatar in tsup + package.json exports - Update .claude/rules/styles.md with token architecture (Layer 1 palette vs Layer 2 semantic) Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/react/package.json | 5 + .../react/src/components/avatar/Avatar.tsx | 74 ++ .../src/components/avatar/avatar.stories.tsx | 407 ++++++++++ .../src/components/avatar/avatar.test.tsx | 134 ++++ .../src/components/avatar/avatar.variants.ts | 5 + packages/react/src/components/avatar/index.ts | 3 + .../src/components/badge/badge.stories.tsx | 12 +- .../src/components/icons/ArrowRightIcon.tsx | 2 +- .../react/src/components/icons/BellIcon.tsx | 2 +- .../src/components/icons/CheckCircleIcon.tsx | 2 +- .../src/components/icons/ClipboardIcon.tsx | 2 +- .../icons/ExclamationTriangleIcon.tsx | 2 +- .../react/src/components/icons/GitHubIcon.tsx | 2 +- .../react/src/components/icons/HeartIcon.tsx | 2 +- .../icons/InformationCircleIcon.tsx | 2 +- .../react/src/components/icons/LockIcon.tsx | 2 +- .../react/src/components/icons/MoreIcon.tsx | 2 +- .../react/src/components/icons/PlusIcon.tsx | 2 +- .../src/components/icons/ReactAriaIcon.tsx | 2 +- .../src/components/icons/SettingsIcon.tsx | 2 +- .../react/src/components/icons/ShareIcon.tsx | 2 +- .../react/src/components/icons/Spinner.tsx | 2 +- .../react/src/components/icons/StarIcon.tsx | 2 +- .../src/components/icons/StorybookIcon.tsx | 2 +- .../react/src/components/icons/UserIcon.tsx | 26 + .../src/components/icons/XCircleIcon.tsx | 2 +- packages/react/src/components/icons/index.ts | 1 + packages/react/src/components/index.ts | 2 + .../components/surface/surface.stories.tsx | 26 +- packages/react/tsup.config.ts | 1 + packages/styles/src/components/avatar.css | 60 ++ packages/styles/src/components/badge.css | 450 +++++------ packages/styles/src/components/button.css | 752 +++++++++--------- packages/styles/src/components/index.css | 2 + packages/styles/src/components/surface.css | 146 ++-- packages/styles/src/tokens/colors.css | 5 +- pnpm-lock.yaml | 82 ++ 37 files changed, 1525 insertions(+), 704 deletions(-) create mode 100644 packages/react/src/components/avatar/Avatar.tsx create mode 100644 packages/react/src/components/avatar/avatar.stories.tsx create mode 100644 packages/react/src/components/avatar/avatar.test.tsx create mode 100644 packages/react/src/components/avatar/avatar.variants.ts create mode 100644 packages/react/src/components/avatar/index.ts create mode 100644 packages/react/src/components/icons/UserIcon.tsx create mode 100644 packages/styles/src/components/avatar.css diff --git a/packages/react/package.json b/packages/react/package.json index ef08f43..24cb6ab 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -27,6 +27,10 @@ "types": "./dist/index.d.ts", "import": "./dist/index.mjs" }, + "./avatar": { + "types": "./dist/components/avatar/index.d.ts", + "import": "./dist/components/avatar/index.mjs" + }, "./badge": { "types": "./dist/components/badge/index.d.ts", "import": "./dist/components/badge/index.mjs" @@ -62,6 +66,7 @@ }, "dependencies": { "@fried-ui/styles": "workspace:^", + "@radix-ui/react-avatar": "^1.1.11", "@react-types/color": "^3.1.4", "@react-types/shared": "^3.33.1", "clsx": "^2.1.1", diff --git a/packages/react/src/components/avatar/Avatar.tsx b/packages/react/src/components/avatar/Avatar.tsx new file mode 100644 index 0000000..62431aa --- /dev/null +++ b/packages/react/src/components/avatar/Avatar.tsx @@ -0,0 +1,74 @@ +"use client"; + +import type { ComponentPropsWithRef } from "react"; + +import * as AvatarPrimitive from "@radix-ui/react-avatar"; + +import { clsx } from "clsx"; + +import { bem } from "../../utils/bem"; + +import type { AvatarVariantsProps } from "./avatar.variants"; + +export type AvatarProps = Omit, "className"> & + AvatarVariantsProps & { + className?: string; + }; + +export type AvatarImageProps = Omit, "className"> & { + className?: string; +}; + +export type AvatarFallbackProps = Omit, "className"> & { + className?: string; +}; + +const AvatarImage = (props: Readonly) => { + const { className, ref, ...rest } = props; + const imageClassName = clsx("fri-avatar__image", className); + + return ; +}; + +AvatarImage.displayName = "Avatar.Image"; + +const AvatarFallback = (props: Readonly) => { + const { children, className, ref, ...rest } = props; + const fallbackClassName = clsx("fri-avatar__fallback", className); + + return ( + + {children} + + ); +}; + +AvatarFallback.displayName = "Avatar.Fallback"; + +const Avatar = (props: Readonly) => { + const { children, className, isDisabled, radius, ref, size, ...rest } = props; + + const avatarClassName = clsx( + bem({ + block: "fri-avatar", + modifiers: { + size, + radius, + disabled: isDisabled, + }, + }), + className, + ); + + return ( + + {children} + + ); +}; + +Avatar.displayName = "Avatar"; +Avatar.Image = AvatarImage; +Avatar.Fallback = AvatarFallback; + +export { Avatar }; diff --git a/packages/react/src/components/avatar/avatar.stories.tsx b/packages/react/src/components/avatar/avatar.stories.tsx new file mode 100644 index 0000000..efa3d70 --- /dev/null +++ b/packages/react/src/components/avatar/avatar.stories.tsx @@ -0,0 +1,407 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import { UserIcon } from "../icons"; +import { Avatar } from "./Avatar"; + +const SAMPLE_SRC_1 = + "https://images.unsplash.com/photo-1729017256081-0271b3fcc08e?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTQ4fHxwcm9maWxlfGVufDB8fDB8fHww"; + +const SAMPLE_SRC_2 = + "https://images.unsplash.com/photo-1569913486515-b74bf7751574?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxzZWFyY2h8Mzh8fHByb2ZpbGUlMjBhdmF0YXJ8ZW58MHx8MHx8fDA%3D"; + +const SAMPLE_SRC_3 = + "https://images.unsplash.com/photo-1576348076752-6085814e5a51?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MjZ8fGF2YXRhciUyMGN1dGUlMjBnaXJsfGVufDB8fDB8fHww"; + +const RADIUS_SRC = + "https://images.unsplash.com/photo-1565972476522-4338522b1929?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTZ8fGNpcmNsZXxlbnwwfHwwfHx8MA%3D%3D"; + +const SAMPLE_SRC_4 = + "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTB8fGZhY2V8ZW58MHx8MHx8fDA%3D"; + +const meta = { + title: "Components/Avatar", + component: Avatar, + tags: ["autodocs"], + parameters: { + layout: "centered", + }, + args: { + size: "md", + radius: "full", + isDisabled: false, + }, + argTypes: { + size: { + control: "select", + options: ["sm", "md", "lg", "xl", "2xl"], + description: "The size of the avatar", + table: { + type: { summary: '"sm" | "md" | "lg" | "xl" | "2xl"' }, + defaultValue: { summary: "md" }, + category: "Style Variants", + }, + }, + radius: { + control: "select", + options: ["none", "sm", "md", "lg", "full"], + description: "The border radius of the avatar", + table: { + type: { summary: '"none" | "sm" | "md" | "lg" | "full"' }, + defaultValue: { summary: "full" }, + category: "Style Variants", + }, + }, + isDisabled: { + control: "boolean", + description: "Dims the avatar and disables pointer events", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "false" }, + category: "Style Variants", + }, + }, + className: { + control: "text", + description: "Additional CSS classes", + table: { + type: { summary: "string" }, + category: "Styling", + }, + }, + }, +} satisfies Meta; + +type Story = StoryObj; + +const Default: Story = { + parameters: { + docs: { + source: { + code: `import { Avatar } from "@fried-ui/react"; + +const Basic = () => { + return ( + + + CT + + ); +};`, + }, + }, + }, + render: (args): React.JSX.Element => ( + + + CT + + ), +}; + +const WithFallback: Story = { + parameters: { + docs: { + source: { + code: `import { Avatar, UserIcon } from "@fried-ui/react"; + +const WithFallback = () => { + return ( +
+ + YK + + + RT + + + + + + +
+ ); +};`, + }, + }, + }, + render: (args): React.JSX.Element => ( +
+ + YK + + + + RT + + + + + + + +
+ ), +}; + +const Sizes: Story = { + parameters: { + docs: { + source: { + code: `import { Avatar } from "@fried-ui/react"; + +const Sizes = () => { + return ( +
+ + + A1 + + + + + A2 + + + + + A3 + + + + + A4 + + + + + A5 + +
+ ); +};`, + }, + }, + }, + render: (args): React.JSX.Element => ( +
+ + + A1 + + + + + A2 + + + + + A3 + + + + + A4 + + + + + A5 + +
+ ), +}; + +const Radius: Story = { + parameters: { + docs: { + source: { + code: `import { Avatar } from "@fried-ui/react"; + +const Radius = () => { + return ( +
+ + + A + + + + + A + + + + + A + + + + + A + + + + + A + +
+ ); +};`, + }, + }, + }, + render: (args): React.JSX.Element => ( +
+ + + A + + + + + A + + + + + A + + + + + A + + + + + A + +
+ ), +}; + +const Disabled: Story = { + parameters: { + docs: { + source: { + code: `import { Avatar } from "@fried-ui/react"; + +const Disabled = () => { + return ( + + + LP + + ); +};`, + }, + }, + }, + args: { + isDisabled: true, + }, + render: (args): React.JSX.Element => ( + + + LP + + ), +}; + +const BrokenImage: Story = { + parameters: { + docs: { + source: { + code: `import { Avatar } from "@fried-ui/react"; + +const BrokenImage = () => { + return ( + + + ZX + + ); +};`, + }, + }, + }, + render: (args): React.JSX.Element => ( + + + ZX + + ), +}; + +const CUSTOM_SRC = + "https://images.unsplash.com/photo-1559828801-04565cd31e27?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTF8fGN5YmVycHVuayUyMGdpcmx8ZW58MHx8MHx8fDA%3D"; + +const CustomStyle: Story = { + parameters: { + docs: { + source: { + code: `import { Avatar } from "@fried-ui/react"; + +const CustomStyle = () => { + return ( +
+ + + 4XL + + +
+ + + ON + + + +
+ +
+ + + ON + + + +
+
+ ); +};`, + }, + }, + }, + render: (args): React.JSX.Element => ( +
+ + + 4XL + + +
+ + + ON + + + +
+ +
+ + + ON + + + +
+
+ ), +}; + +export { Default, WithFallback, Sizes, Radius, Disabled, BrokenImage, CustomStyle }; + +export default meta; diff --git a/packages/react/src/components/avatar/avatar.test.tsx b/packages/react/src/components/avatar/avatar.test.tsx new file mode 100644 index 0000000..40a7fdb --- /dev/null +++ b/packages/react/src/components/avatar/avatar.test.tsx @@ -0,0 +1,134 @@ +import { createRef } from "react"; + +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; + +import { Avatar } from "./Avatar"; + +describe("Avatar", () => { + it("renders with default props", () => { + const { container } = render( + + JD + , + ); + + expect(container.querySelector("[data-slot='avatar']")).toBeInTheDocument(); + }); + + it("renders base class without explicit modifiers", () => { + const { container } = render( + + A + , + ); + + const root = container.querySelector("[data-slot='avatar']"); + expect(root?.className).toBe("fri-avatar"); + }); + + it("applies all size classes", () => { + const sizes = ["sm", "md", "lg", "xl", "2xl"] as const; + + sizes.forEach((size) => { + const { container, unmount } = render( + + A + , + ); + + const root = container.querySelector("[data-slot='avatar']"); + expect(root?.className).toContain(`fri-avatar--size-${size}`); + unmount(); + }); + }); + + it("applies all radius classes", () => { + const radii = ["none", "sm", "md", "lg", "full"] as const; + + radii.forEach((radius) => { + const { container, unmount } = render( + + A + , + ); + + const root = container.querySelector("[data-slot='avatar']"); + expect(root?.className).toContain(`fri-avatar--radius-${radius}`); + unmount(); + }); + }); + + it("applies disabled modifier", () => { + const { container } = render( + + A + , + ); + + const root = container.querySelector("[data-slot='avatar']"); + expect(root?.className).toContain("fri-avatar--disabled"); + }); + + it("renders fallback content", () => { + render( + + XL + , + ); + + expect(screen.getByText("XL")).toBeInTheDocument(); + }); + + it("applies fallback class and data-slot", () => { + const { container } = render( + + A + , + ); + + const fallback = container.querySelector("[data-slot='avatar-fallback']"); + expect(fallback).toBeInTheDocument(); + expect(fallback?.className).toContain("fri-avatar__fallback"); + }); + + it("merges custom className on Root", () => { + const { container } = render( + + A + , + ); + + const root = container.querySelector("[data-slot='avatar']"); + expect(root?.className).toContain("mt-4"); + }); + + it("merges custom className on Fallback", () => { + const { container } = render( + + A + , + ); + + const fallback = container.querySelector("[data-slot='avatar-fallback']"); + expect(fallback?.className).toContain("font-bold"); + }); + + it("forwards ref to span element", () => { + const ref = createRef(); + + render( + + A + , + ); + + expect(ref.current).toBeInstanceOf(HTMLSpanElement); + }); + + it("sets displayName", () => { + expect(Avatar.displayName).toBe("Avatar"); + expect(Avatar.Image.displayName).toBe("Avatar.Image"); + expect(Avatar.Fallback.displayName).toBe("Avatar.Fallback"); + }); +}); diff --git a/packages/react/src/components/avatar/avatar.variants.ts b/packages/react/src/components/avatar/avatar.variants.ts new file mode 100644 index 0000000..2833f70 --- /dev/null +++ b/packages/react/src/components/avatar/avatar.variants.ts @@ -0,0 +1,5 @@ +export interface AvatarVariantsProps { + isDisabled?: boolean; + radius?: "none" | "sm" | "md" | "lg" | "full"; + size?: "sm" | "md" | "lg" | "xl" | "2xl"; +} diff --git a/packages/react/src/components/avatar/index.ts b/packages/react/src/components/avatar/index.ts new file mode 100644 index 0000000..e92a435 --- /dev/null +++ b/packages/react/src/components/avatar/index.ts @@ -0,0 +1,3 @@ +export { Avatar } from "./Avatar"; +export type { AvatarFallbackProps, AvatarImageProps, AvatarProps } from "./Avatar"; +export type { AvatarVariantsProps } from "./avatar.variants"; diff --git a/packages/react/src/components/badge/badge.stories.tsx b/packages/react/src/components/badge/badge.stories.tsx index c561a58..3ab382a 100644 --- a/packages/react/src/components/badge/badge.stories.tsx +++ b/packages/react/src/components/badge/badge.stories.tsx @@ -384,7 +384,7 @@ const GlassVariants: Story = { const GlassVariants = () => { return ( -
+
Featured New @@ -397,7 +397,7 @@ const GlassVariants = () => { }, }, render: (args): React.JSX.Element => ( -
+
Featured @@ -423,7 +423,7 @@ const FrostVariants: Story = { const FrostVariants = () => { return ( -
+
Featured New @@ -436,7 +436,7 @@ const FrostVariants = () => { }, }, render: (args): React.JSX.Element => ( -
+
Featured @@ -462,7 +462,7 @@ const OverlayVariants: Story = { const OverlayVariants = () => { return ( -
+
Live HD @@ -475,7 +475,7 @@ const OverlayVariants = () => { }, }, render: (args): React.JSX.Element => ( -
+
Live diff --git a/packages/react/src/components/icons/ArrowRightIcon.tsx b/packages/react/src/components/icons/ArrowRightIcon.tsx index 3e4b2e8..988dd4c 100644 --- a/packages/react/src/components/icons/ArrowRightIcon.tsx +++ b/packages/react/src/components/icons/ArrowRightIcon.tsx @@ -1,6 +1,6 @@ import type { ComponentPropsWithRef } from "react"; -const ArrowRightIcon = (props: Readonly>) => { +const ArrowRightIcon = (props: Readonly & { slot?: string }>) => { const { className, ref, ...rest } = props; return ( diff --git a/packages/react/src/components/icons/BellIcon.tsx b/packages/react/src/components/icons/BellIcon.tsx index 04f5f1c..0bc625e 100644 --- a/packages/react/src/components/icons/BellIcon.tsx +++ b/packages/react/src/components/icons/BellIcon.tsx @@ -1,6 +1,6 @@ import type { ComponentPropsWithRef } from "react"; -const BellIcon = (props: Readonly>) => { +const BellIcon = (props: Readonly & { slot?: string }>) => { const { className, ref, ...rest } = props; return ( diff --git a/packages/react/src/components/icons/CheckCircleIcon.tsx b/packages/react/src/components/icons/CheckCircleIcon.tsx index a2ee5dd..69b1dec 100644 --- a/packages/react/src/components/icons/CheckCircleIcon.tsx +++ b/packages/react/src/components/icons/CheckCircleIcon.tsx @@ -1,6 +1,6 @@ import type { ComponentPropsWithRef } from "react"; -const CheckCircleIcon = (props: Readonly>) => { +const CheckCircleIcon = (props: Readonly & { slot?: string }>) => { const { className, ref, ...rest } = props; return ( diff --git a/packages/react/src/components/icons/ClipboardIcon.tsx b/packages/react/src/components/icons/ClipboardIcon.tsx index 558b2d3..43d7200 100644 --- a/packages/react/src/components/icons/ClipboardIcon.tsx +++ b/packages/react/src/components/icons/ClipboardIcon.tsx @@ -1,6 +1,6 @@ import type { ComponentPropsWithRef } from "react"; -const ClipboardIcon = (props: Readonly>) => { +const ClipboardIcon = (props: Readonly & { slot?: string }>) => { const { className, ref, ...rest } = props; return ( diff --git a/packages/react/src/components/icons/ExclamationTriangleIcon.tsx b/packages/react/src/components/icons/ExclamationTriangleIcon.tsx index 89a1070..0e4508a 100644 --- a/packages/react/src/components/icons/ExclamationTriangleIcon.tsx +++ b/packages/react/src/components/icons/ExclamationTriangleIcon.tsx @@ -1,6 +1,6 @@ import type { ComponentPropsWithRef } from "react"; -const ExclamationTriangleIcon = (props: Readonly>) => { +const ExclamationTriangleIcon = (props: Readonly & { slot?: string }>) => { const { className, ref, ...rest } = props; return ( diff --git a/packages/react/src/components/icons/GitHubIcon.tsx b/packages/react/src/components/icons/GitHubIcon.tsx index 2bcd120..6e865d4 100644 --- a/packages/react/src/components/icons/GitHubIcon.tsx +++ b/packages/react/src/components/icons/GitHubIcon.tsx @@ -1,6 +1,6 @@ import type { ComponentPropsWithRef } from "react"; -const GitHubIcon = (props: Readonly>) => ( +const GitHubIcon = (props: Readonly & { slot?: string }>) => ( diff --git a/packages/react/src/components/icons/HeartIcon.tsx b/packages/react/src/components/icons/HeartIcon.tsx index 9622840..f5e7c1d 100644 --- a/packages/react/src/components/icons/HeartIcon.tsx +++ b/packages/react/src/components/icons/HeartIcon.tsx @@ -1,6 +1,6 @@ import type { ComponentPropsWithRef } from "react"; -const HeartIcon = (props: Readonly>) => { +const HeartIcon = (props: Readonly & { slot?: string }>) => { const { className, ref, ...rest } = props; return ( diff --git a/packages/react/src/components/icons/InformationCircleIcon.tsx b/packages/react/src/components/icons/InformationCircleIcon.tsx index 9552ca0..6098ef1 100644 --- a/packages/react/src/components/icons/InformationCircleIcon.tsx +++ b/packages/react/src/components/icons/InformationCircleIcon.tsx @@ -1,6 +1,6 @@ import type { ComponentPropsWithRef } from "react"; -const InformationCircleIcon = (props: Readonly>) => { +const InformationCircleIcon = (props: Readonly & { slot?: string }>) => { const { className, ref, ...rest } = props; return ( diff --git a/packages/react/src/components/icons/LockIcon.tsx b/packages/react/src/components/icons/LockIcon.tsx index ce74f5e..c398c57 100644 --- a/packages/react/src/components/icons/LockIcon.tsx +++ b/packages/react/src/components/icons/LockIcon.tsx @@ -1,6 +1,6 @@ import type { ComponentPropsWithRef } from "react"; -const LockIcon = (props: Readonly>) => { +const LockIcon = (props: Readonly & { slot?: string }>) => { const { className, ref, ...rest } = props; return ( diff --git a/packages/react/src/components/icons/MoreIcon.tsx b/packages/react/src/components/icons/MoreIcon.tsx index 9618986..d13d389 100644 --- a/packages/react/src/components/icons/MoreIcon.tsx +++ b/packages/react/src/components/icons/MoreIcon.tsx @@ -1,6 +1,6 @@ import type { ComponentPropsWithRef } from "react"; -const MoreIcon = (props: Readonly>) => { +const MoreIcon = (props: Readonly & { slot?: string }>) => { const { className, ref, ...rest } = props; return ( diff --git a/packages/react/src/components/icons/PlusIcon.tsx b/packages/react/src/components/icons/PlusIcon.tsx index d5cfd27..de92610 100644 --- a/packages/react/src/components/icons/PlusIcon.tsx +++ b/packages/react/src/components/icons/PlusIcon.tsx @@ -1,6 +1,6 @@ import type { ComponentPropsWithRef } from "react"; -const PlusIcon = (props: Readonly>) => { +const PlusIcon = (props: Readonly & { slot?: string }>) => { const { className, ref, ...rest } = props; return ( diff --git a/packages/react/src/components/icons/ReactAriaIcon.tsx b/packages/react/src/components/icons/ReactAriaIcon.tsx index 84d829c..6e7fc94 100644 --- a/packages/react/src/components/icons/ReactAriaIcon.tsx +++ b/packages/react/src/components/icons/ReactAriaIcon.tsx @@ -1,6 +1,6 @@ import type { ComponentPropsWithRef } from "react"; -const ReactAriaIcon = (props: Readonly>) => ( +const ReactAriaIcon = (props: Readonly & { slot?: string }>) => ( diff --git a/packages/react/src/components/icons/SettingsIcon.tsx b/packages/react/src/components/icons/SettingsIcon.tsx index 17f98dd..8af68b0 100644 --- a/packages/react/src/components/icons/SettingsIcon.tsx +++ b/packages/react/src/components/icons/SettingsIcon.tsx @@ -1,6 +1,6 @@ import type { ComponentPropsWithRef } from "react"; -const SettingsIcon = (props: Readonly>) => { +const SettingsIcon = (props: Readonly & { slot?: string }>) => { const { className, ref, ...rest } = props; return ( diff --git a/packages/react/src/components/icons/ShareIcon.tsx b/packages/react/src/components/icons/ShareIcon.tsx index 30a0bc2..7ecd6f8 100644 --- a/packages/react/src/components/icons/ShareIcon.tsx +++ b/packages/react/src/components/icons/ShareIcon.tsx @@ -1,6 +1,6 @@ import type { ComponentPropsWithRef } from "react"; -const ShareIcon = (props: Readonly>) => { +const ShareIcon = (props: Readonly & { slot?: string }>) => { const { className, ref, ...rest } = props; return ( diff --git a/packages/react/src/components/icons/Spinner.tsx b/packages/react/src/components/icons/Spinner.tsx index eb9b96b..acefe61 100644 --- a/packages/react/src/components/icons/Spinner.tsx +++ b/packages/react/src/components/icons/Spinner.tsx @@ -1,6 +1,6 @@ import { type ComponentPropsWithRef, useId } from "react"; -const Spinner = (props: Readonly>) => { +const Spinner = (props: Readonly & { slot?: string }>) => { const id = useId(); const grad1 = `fri-spinner-grad-1-${id}`; const grad2 = `fri-spinner-grad-2-${id}`; diff --git a/packages/react/src/components/icons/StarIcon.tsx b/packages/react/src/components/icons/StarIcon.tsx index bb8fc80..a2a144e 100644 --- a/packages/react/src/components/icons/StarIcon.tsx +++ b/packages/react/src/components/icons/StarIcon.tsx @@ -1,6 +1,6 @@ import type { ComponentPropsWithRef } from "react"; -const StarIcon = (props: Readonly>) => { +const StarIcon = (props: Readonly & { slot?: string }>) => { const { className, ref, ...rest } = props; return ( diff --git a/packages/react/src/components/icons/StorybookIcon.tsx b/packages/react/src/components/icons/StorybookIcon.tsx index b24b314..11c0273 100644 --- a/packages/react/src/components/icons/StorybookIcon.tsx +++ b/packages/react/src/components/icons/StorybookIcon.tsx @@ -1,6 +1,6 @@ import type { ComponentPropsWithRef } from "react"; -const StorybookIcon = (props: Readonly>) => ( +const StorybookIcon = (props: Readonly & { slot?: string }>) => ( diff --git a/packages/react/src/components/icons/UserIcon.tsx b/packages/react/src/components/icons/UserIcon.tsx new file mode 100644 index 0000000..ad3377f --- /dev/null +++ b/packages/react/src/components/icons/UserIcon.tsx @@ -0,0 +1,26 @@ +import type { ComponentPropsWithRef } from "react"; + +const UserIcon = (props: Readonly & { slot?: string }>) => { + const { className, ref, ...rest } = props; + + return ( + + + + + ); +}; + +UserIcon.displayName = "UserIcon"; + +export { UserIcon }; diff --git a/packages/react/src/components/icons/XCircleIcon.tsx b/packages/react/src/components/icons/XCircleIcon.tsx index a62be74..c2e903f 100644 --- a/packages/react/src/components/icons/XCircleIcon.tsx +++ b/packages/react/src/components/icons/XCircleIcon.tsx @@ -1,6 +1,6 @@ import type { ComponentPropsWithRef } from "react"; -const XCircleIcon = (props: Readonly>) => { +const XCircleIcon = (props: Readonly & { slot?: string }>) => { const { className, ref, ...rest } = props; return ( diff --git a/packages/react/src/components/icons/index.ts b/packages/react/src/components/icons/index.ts index e55d0d6..7054551 100644 --- a/packages/react/src/components/icons/index.ts +++ b/packages/react/src/components/icons/index.ts @@ -15,4 +15,5 @@ export { ShareIcon } from "./ShareIcon"; export { Spinner } from "./Spinner"; export { StarIcon } from "./StarIcon"; export { StorybookIcon } from "./StorybookIcon"; +export { UserIcon } from "./UserIcon"; export { XCircleIcon } from "./XCircleIcon"; diff --git a/packages/react/src/components/index.ts b/packages/react/src/components/index.ts index 1dd04dd..d1d0570 100644 --- a/packages/react/src/components/index.ts +++ b/packages/react/src/components/index.ts @@ -5,3 +5,5 @@ export * from "./button"; export * from "./icons"; export * from "./surface"; + +export * from "./avatar"; diff --git a/packages/react/src/components/surface/surface.stories.tsx b/packages/react/src/components/surface/surface.stories.tsx index 813401b..a4a8777 100644 --- a/packages/react/src/components/surface/surface.stories.tsx +++ b/packages/react/src/components/surface/surface.stories.tsx @@ -122,6 +122,7 @@ const Variants = () => {

Default

Subtle gray card on white.

+

Default Bordered

Gray card with emphasis border.

@@ -155,11 +156,12 @@ const PlainVariants: Story = { const PlainVariants = () => { return ( -
+

Plain

Pure white card on gray page.

+

Plain Bordered

White card with emphasis border.

@@ -171,7 +173,7 @@ const PlainVariants = () => { }, }, render: (args): React.JSX.Element => ( -
+

Plain

Pure white card on gray page.

@@ -193,7 +195,7 @@ const GlassVariants: Story = { const GlassVariants = () => { return ( -
+

Glass Surface

Content over a gradient background.

@@ -205,7 +207,7 @@ const GlassVariants = () => { }, }, render: (args): React.JSX.Element => ( -
+

Glass Surface

Content over a gradient background.

@@ -222,7 +224,7 @@ const FrostVariants: Story = { const FrostVariants = () => { return ( -
+

Frost Surface

Frosted content without a border.

@@ -234,7 +236,7 @@ const FrostVariants = () => { }, }, render: (args): React.JSX.Element => ( -
+

Frost Surface

Frosted content without a border.

@@ -251,7 +253,7 @@ const OverlayVariants: Story = { const OverlayVariants = () => { return ( -
+

Overlay Surface

Dark scrim over media content.

@@ -263,7 +265,7 @@ const OverlayVariants = () => { }, }, render: (args): React.JSX.Element => ( -
+

Overlay Surface

Dark scrim over media content.

@@ -285,18 +287,22 @@ const Radius = () => {

None

No rounded corners.

+

Small

Subtle rounding.

+

Medium

Balanced rounding.

+

Large

Soft rounding.

+

Extra Large

Very rounded corners.

@@ -350,18 +356,22 @@ const Shadow = () => {

None

No shadow.

+

Small

Subtle lift.

+

Medium

Noticeable elevation.

+

Large

Floating card.

+

Extra Large

Strong elevation.

diff --git a/packages/react/tsup.config.ts b/packages/react/tsup.config.ts index 655c7a8..ab91953 100644 --- a/packages/react/tsup.config.ts +++ b/packages/react/tsup.config.ts @@ -3,6 +3,7 @@ import { defineConfig } from "tsup"; export default defineConfig({ entry: { index: "src/index.ts", + "components/avatar/index": "src/components/avatar/index.ts", "components/badge/index": "src/components/badge/index.ts", "components/button/index": "src/components/button/index.ts", "components/icons/index": "src/components/icons/index.ts", diff --git a/packages/styles/src/components/avatar.css b/packages/styles/src/components/avatar.css new file mode 100644 index 0000000..c77e7e5 --- /dev/null +++ b/packages/styles/src/components/avatar.css @@ -0,0 +1,60 @@ +@layer components { + .fri-avatar { + @apply relative inline-flex shrink-0 items-center justify-center overflow-hidden select-none; + @apply leading-none font-medium; + @apply size-10 rounded-full text-sm; + @apply bg-(--color-avatar) text-(--color-avatar-foreground); + } + + .fri-avatar__image { + @apply size-full object-cover; + } + + .fri-avatar__fallback { + @apply flex size-full items-center justify-center; + } + + .fri-avatar--size-sm { + @apply size-8 text-xs; + } + + .fri-avatar--size-md { + @apply size-10 text-sm; + } + + .fri-avatar--size-lg { + @apply size-12 text-base; + } + + .fri-avatar--size-xl { + @apply size-14 text-lg; + } + + .fri-avatar--size-2xl { + @apply size-16 text-xl; + } + + .fri-avatar--radius-none { + @apply rounded-none; + } + + .fri-avatar--radius-sm { + @apply rounded-sm; + } + + .fri-avatar--radius-md { + @apply rounded-md; + } + + .fri-avatar--radius-lg { + @apply rounded-lg; + } + + .fri-avatar--radius-full { + @apply rounded-full; + } + + .fri-avatar--disabled { + @apply pointer-events-none opacity-50; + } +} diff --git a/packages/styles/src/components/badge.css b/packages/styles/src/components/badge.css index 81a7b92..76651a8 100644 --- a/packages/styles/src/components/badge.css +++ b/packages/styles/src/components/badge.css @@ -1,304 +1,306 @@ -.fri-badge { - @apply inline-flex items-center justify-center; - @apply leading-none font-medium whitespace-nowrap; - @apply border border-transparent select-none; +@layer components { + .fri-badge { + @apply inline-flex items-center justify-center; + @apply leading-none font-medium whitespace-nowrap; + @apply border border-transparent select-none; - --fri-badge-bg: var(--color-primary); - --fri-badge-fg: var(--color-primary-foreground); + --fri-badge-bg: var(--color-primary); + --fri-badge-fg: var(--color-primary-foreground); - @apply bg-(--fri-badge-bg) text-(--fri-badge-fg); - @apply gap-[calc(0.875rem/2.058)] px-[calc(0.875rem)] py-[calc(0.875rem/2.058)] text-sm; - @apply rounded-full; -} + @apply bg-(--fri-badge-bg) text-(--fri-badge-fg); + @apply gap-[calc(0.875rem/2.058)] px-[calc(0.875rem)] py-[calc(0.875rem/2.058)] text-sm; + @apply rounded-full; + } -.fri-badge--primary { - --fri-badge-bg: var(--color-primary); - --fri-badge-fg: var(--color-primary-foreground); + .fri-badge--primary { + --fri-badge-bg: var(--color-primary); + --fri-badge-fg: var(--color-primary-foreground); - @apply border border-primary-border; -} + @apply border border-primary-border; + } -.fri-badge--secondary { - --fri-badge-bg: var(--color-secondary); - --fri-badge-fg: var(--color-secondary-foreground); + .fri-badge--secondary { + --fri-badge-bg: var(--color-secondary); + --fri-badge-fg: var(--color-secondary-foreground); - @apply border border-secondary-border; -} + @apply border border-secondary-border; + } -.fri-badge--accent { - --fri-badge-bg: var(--color-accent); - --fri-badge-fg: var(--color-accent-foreground); + .fri-badge--accent { + --fri-badge-bg: var(--color-accent); + --fri-badge-fg: var(--color-accent-foreground); - @apply border border-accent-border; -} + @apply border border-accent-border; + } -.fri-badge--outline { - --fri-badge-bg: transparent; - --fri-badge-fg: var(--color-primary); + .fri-badge--outline { + --fri-badge-bg: transparent; + --fri-badge-fg: var(--color-primary); - @apply border border-primary-border; -} + @apply border border-primary-border; + } -.fri-badge--glass { - --fri-badge-bg: var(--color-glass); - --fri-badge-fg: var(--color-glass-foreground); + .fri-badge--glass { + --fri-badge-bg: var(--color-glass); + --fri-badge-fg: var(--color-glass-foreground); - @apply border border-glass-border backdrop-blur-xl backdrop-saturate-200; -} + @apply border border-glass-border backdrop-blur-xl backdrop-saturate-200; + } -.fri-badge--frost { - --fri-badge-bg: var(--color-glass); - --fri-badge-fg: var(--color-glass-foreground); + .fri-badge--frost { + --fri-badge-bg: var(--color-glass); + --fri-badge-fg: var(--color-glass-foreground); - @apply backdrop-blur-2xl backdrop-saturate-200; -} + @apply backdrop-blur-2xl backdrop-saturate-200; + } -.fri-badge--overlay { - --fri-badge-bg: var(--color-scrim); - --fri-badge-fg: var(--color-scrim-foreground); + .fri-badge--overlay { + --fri-badge-bg: var(--color-scrim); + --fri-badge-fg: var(--color-scrim-foreground); - @apply border border-scrim-border backdrop-blur-xl backdrop-saturate-150; -} + @apply border border-scrim-border backdrop-blur-xl backdrop-saturate-150; + } -.fri-badge--success { - --fri-badge-bg: var(--color-success); - --fri-badge-fg: var(--color-success-foreground); + .fri-badge--success { + --fri-badge-bg: var(--color-success); + --fri-badge-fg: var(--color-success-foreground); - @apply border border-success-border; -} + @apply border border-success-border; + } -.fri-badge--warning { - --fri-badge-bg: var(--color-warning); - --fri-badge-fg: var(--color-warning-foreground); + .fri-badge--warning { + --fri-badge-bg: var(--color-warning); + --fri-badge-fg: var(--color-warning-foreground); - @apply border border-warning-border; -} + @apply border border-warning-border; + } -.fri-badge--danger { - --fri-badge-bg: var(--color-danger); - --fri-badge-fg: var(--color-danger-foreground); + .fri-badge--danger { + --fri-badge-bg: var(--color-danger); + --fri-badge-fg: var(--color-danger-foreground); - @apply border border-danger-border; -} + @apply border border-danger-border; + } -.fri-badge--info { - --fri-badge-bg: var(--color-info); - --fri-badge-fg: var(--color-info-foreground); + .fri-badge--info { + --fri-badge-bg: var(--color-info); + --fri-badge-fg: var(--color-info-foreground); - @apply border border-info-border; -} + @apply border border-info-border; + } -.fri-badge--primary-soft { - --fri-badge-bg: var(--color-primary-soft); - --fri-badge-fg: var(--color-primary-soft-foreground); + .fri-badge--primary-soft { + --fri-badge-bg: var(--color-primary-soft); + --fri-badge-fg: var(--color-primary-soft-foreground); - @apply border border-primary-soft-border; -} + @apply border border-primary-soft-border; + } -.fri-badge--secondary-soft { - --fri-badge-bg: var(--color-secondary-soft); - --fri-badge-fg: var(--color-secondary-soft-foreground); + .fri-badge--secondary-soft { + --fri-badge-bg: var(--color-secondary-soft); + --fri-badge-fg: var(--color-secondary-soft-foreground); - @apply border border-secondary-soft-border; -} + @apply border border-secondary-soft-border; + } -.fri-badge--accent-soft { - --fri-badge-bg: var(--color-accent-soft); - --fri-badge-fg: var(--color-accent-soft-foreground); + .fri-badge--accent-soft { + --fri-badge-bg: var(--color-accent-soft); + --fri-badge-fg: var(--color-accent-soft-foreground); - @apply border border-accent-soft-border; -} + @apply border border-accent-soft-border; + } -.fri-badge--success-soft { - --fri-badge-bg: var(--color-success-soft); - --fri-badge-fg: var(--color-success-soft-foreground); + .fri-badge--success-soft { + --fri-badge-bg: var(--color-success-soft); + --fri-badge-fg: var(--color-success-soft-foreground); - @apply border border-success-soft-border; -} + @apply border border-success-soft-border; + } -.fri-badge--warning-soft { - --fri-badge-bg: var(--color-warning-soft); - --fri-badge-fg: var(--color-warning-soft-foreground); + .fri-badge--warning-soft { + --fri-badge-bg: var(--color-warning-soft); + --fri-badge-fg: var(--color-warning-soft-foreground); - @apply border border-warning-soft-border; -} + @apply border border-warning-soft-border; + } -.fri-badge--danger-soft { - --fri-badge-bg: var(--color-danger-soft); - --fri-badge-fg: var(--color-danger-soft-foreground); + .fri-badge--danger-soft { + --fri-badge-bg: var(--color-danger-soft); + --fri-badge-fg: var(--color-danger-soft-foreground); - @apply border border-danger-soft-border; -} + @apply border border-danger-soft-border; + } -.fri-badge--info-soft { - --fri-badge-bg: var(--color-info-soft); - --fri-badge-fg: var(--color-info-soft-foreground); + .fri-badge--info-soft { + --fri-badge-bg: var(--color-info-soft); + --fri-badge-fg: var(--color-info-soft-foreground); - @apply border border-info-soft-border; -} + @apply border border-info-soft-border; + } -.fri-badge--primary-flat { - --fri-badge-bg: var(--color-primary-flat); - --fri-badge-fg: var(--color-primary-flat-foreground); -} + .fri-badge--primary-flat { + --fri-badge-bg: var(--color-primary-flat); + --fri-badge-fg: var(--color-primary-flat-foreground); + } -.fri-badge--secondary-flat { - --fri-badge-bg: var(--color-secondary-flat); - --fri-badge-fg: var(--color-secondary-flat-foreground); -} + .fri-badge--secondary-flat { + --fri-badge-bg: var(--color-secondary-flat); + --fri-badge-fg: var(--color-secondary-flat-foreground); + } -.fri-badge--accent-flat { - --fri-badge-bg: var(--color-accent-flat); - --fri-badge-fg: var(--color-accent-flat-foreground); -} + .fri-badge--accent-flat { + --fri-badge-bg: var(--color-accent-flat); + --fri-badge-fg: var(--color-accent-flat-foreground); + } -.fri-badge--success-flat { - --fri-badge-bg: var(--color-success-flat); - --fri-badge-fg: var(--color-success-flat-foreground); -} + .fri-badge--success-flat { + --fri-badge-bg: var(--color-success-flat); + --fri-badge-fg: var(--color-success-flat-foreground); + } -.fri-badge--warning-flat { - --fri-badge-bg: var(--color-warning-flat); - --fri-badge-fg: var(--color-warning-flat-foreground); -} + .fri-badge--warning-flat { + --fri-badge-bg: var(--color-warning-flat); + --fri-badge-fg: var(--color-warning-flat-foreground); + } -.fri-badge--danger-flat { - --fri-badge-bg: var(--color-danger-flat); - --fri-badge-fg: var(--color-danger-flat-foreground); -} + .fri-badge--danger-flat { + --fri-badge-bg: var(--color-danger-flat); + --fri-badge-fg: var(--color-danger-flat-foreground); + } -.fri-badge--info-flat { - --fri-badge-bg: var(--color-info-flat); - --fri-badge-fg: var(--color-info-flat-foreground); -} + .fri-badge--info-flat { + --fri-badge-bg: var(--color-info-flat); + --fri-badge-fg: var(--color-info-flat-foreground); + } -.fri-badge--primary-outline { - --fri-badge-bg: var(--color-primary-outline); - --fri-badge-fg: var(--color-primary-outline-foreground); + .fri-badge--primary-outline { + --fri-badge-bg: var(--color-primary-outline); + --fri-badge-fg: var(--color-primary-outline-foreground); - @apply border border-primary-outline-border; -} + @apply border border-primary-outline-border; + } -.fri-badge--secondary-outline { - --fri-badge-bg: var(--color-secondary-outline); - --fri-badge-fg: var(--color-secondary-outline-foreground); + .fri-badge--secondary-outline { + --fri-badge-bg: var(--color-secondary-outline); + --fri-badge-fg: var(--color-secondary-outline-foreground); - @apply border border-secondary-outline-border; -} + @apply border border-secondary-outline-border; + } -.fri-badge--accent-outline { - --fri-badge-bg: var(--color-accent-outline); - --fri-badge-fg: var(--color-accent-outline-foreground); + .fri-badge--accent-outline { + --fri-badge-bg: var(--color-accent-outline); + --fri-badge-fg: var(--color-accent-outline-foreground); - @apply border border-accent-outline-border; -} + @apply border border-accent-outline-border; + } -.fri-badge--success-outline { - --fri-badge-bg: var(--color-success-outline); - --fri-badge-fg: var(--color-success-outline-foreground); + .fri-badge--success-outline { + --fri-badge-bg: var(--color-success-outline); + --fri-badge-fg: var(--color-success-outline-foreground); - @apply border border-success-outline-border; -} + @apply border border-success-outline-border; + } -.fri-badge--warning-outline { - --fri-badge-bg: var(--color-warning-outline); - --fri-badge-fg: var(--color-warning-outline-foreground); + .fri-badge--warning-outline { + --fri-badge-bg: var(--color-warning-outline); + --fri-badge-fg: var(--color-warning-outline-foreground); - @apply border border-warning-outline-border; -} + @apply border border-warning-outline-border; + } -.fri-badge--danger-outline { - --fri-badge-bg: var(--color-danger-outline); - --fri-badge-fg: var(--color-danger-outline-foreground); + .fri-badge--danger-outline { + --fri-badge-bg: var(--color-danger-outline); + --fri-badge-fg: var(--color-danger-outline-foreground); - @apply border border-danger-outline-border; -} + @apply border border-danger-outline-border; + } -.fri-badge--info-outline { - --fri-badge-bg: var(--color-info-outline); - --fri-badge-fg: var(--color-info-outline-foreground); + .fri-badge--info-outline { + --fri-badge-bg: var(--color-info-outline); + --fri-badge-fg: var(--color-info-outline-foreground); - @apply border border-info-outline-border; -} + @apply border border-info-outline-border; + } -.fri-badge--radius-none { - @apply rounded-none; -} + .fri-badge--radius-none { + @apply rounded-none; + } -.fri-badge--radius-sm { - @apply rounded-[calc(1em*1.272/4)]; -} + .fri-badge--radius-sm { + @apply rounded-[calc(1em*1.272/4)]; + } -.fri-badge--radius-md { - @apply rounded-[calc(1em*1.272*2/4)]; -} + .fri-badge--radius-md { + @apply rounded-[calc(1em*1.272*2/4)]; + } -.fri-badge--radius-lg { - @apply rounded-[calc(1em*1.272*3/4)]; -} + .fri-badge--radius-lg { + @apply rounded-[calc(1em*1.272*3/4)]; + } -.fri-badge--radius-full { - @apply rounded-full; -} + .fri-badge--radius-full { + @apply rounded-full; + } -.fri-badge [slot="icon"], -.fri-badge [slot="icon-start"], -.fri-badge [slot="icon-end"] { - @apply size-match-font; -} + .fri-badge [slot="icon"], + .fri-badge [slot="icon-start"], + .fri-badge [slot="icon-end"] { + @apply size-match-font; + } -.fri-badge:has([slot="icon-start"]) { - @apply pl-[calc(0.875rem/2.058)]; -} + .fri-badge:has([slot="icon-start"]) { + @apply pl-[calc(0.875rem/2.058)]; + } -.fri-badge:has([slot="icon-end"]) { - @apply pr-[calc(0.875rem/2.058)]; -} + .fri-badge:has([slot="icon-end"]) { + @apply pr-[calc(0.875rem/2.058)]; + } -.fri-badge--size-sm { - @apply gap-[calc(0.75rem/2.058)] px-[calc(0.75rem)] py-[calc(0.75rem/2.058)] text-xs; -} + .fri-badge--size-sm { + @apply gap-[calc(0.75rem/2.058)] px-[calc(0.75rem)] py-[calc(0.75rem/2.058)] text-xs; + } -.fri-badge--size-sm:has([slot="icon-start"]) { - @apply pl-[calc(0.75rem/2.058)]; -} + .fri-badge--size-sm:has([slot="icon-start"]) { + @apply pl-[calc(0.75rem/2.058)]; + } -.fri-badge--size-sm:has([slot="icon-end"]) { - @apply pr-[calc(0.75rem/2.058)]; -} + .fri-badge--size-sm:has([slot="icon-end"]) { + @apply pr-[calc(0.75rem/2.058)]; + } -.fri-badge--size-md { - @apply gap-[calc(0.875rem/2.058)] px-[calc(0.875rem)] py-[calc(0.875rem/2.058)] text-sm; -} + .fri-badge--size-md { + @apply gap-[calc(0.875rem/2.058)] px-[calc(0.875rem)] py-[calc(0.875rem/2.058)] text-sm; + } -.fri-badge--size-md:has([slot="icon-start"]) { - @apply pl-[calc(0.875rem/2.058)]; -} + .fri-badge--size-md:has([slot="icon-start"]) { + @apply pl-[calc(0.875rem/2.058)]; + } -.fri-badge--size-md:has([slot="icon-end"]) { - @apply pr-[calc(0.875rem/2.058)]; -} + .fri-badge--size-md:has([slot="icon-end"]) { + @apply pr-[calc(0.875rem/2.058)]; + } -.fri-badge--size-lg { - @apply gap-[calc(1rem/2.058)] px-[calc(1rem)] py-[calc(1rem/2.058)] text-base; -} + .fri-badge--size-lg { + @apply gap-[calc(1rem/2.058)] px-[calc(1rem)] py-[calc(1rem/2.058)] text-base; + } -.fri-badge--size-lg:has([slot="icon-start"]) { - @apply pl-[calc(1rem/2.058)]; -} + .fri-badge--size-lg:has([slot="icon-start"]) { + @apply pl-[calc(1rem/2.058)]; + } -.fri-badge--size-lg:has([slot="icon-end"]) { - @apply pr-[calc(1rem/2.058)]; -} + .fri-badge--size-lg:has([slot="icon-end"]) { + @apply pr-[calc(1rem/2.058)]; + } -.fri-badge--icon-only { - @apply aspect-square w-[calc(0.875rem+2*(0.875rem/2.058)+0.125rem)] justify-center gap-0 p-0; -} + .fri-badge--icon-only { + @apply aspect-square w-[calc(0.875rem+2*(0.875rem/2.058)+0.125rem)] justify-center gap-0 p-0; + } -.fri-badge--icon-only.fri-badge--size-sm { - @apply w-[calc(0.75rem+2*(0.75rem/2.058)+0.125rem)]; -} + .fri-badge--icon-only.fri-badge--size-sm { + @apply w-[calc(0.75rem+2*(0.75rem/2.058)+0.125rem)]; + } -.fri-badge--icon-only.fri-badge--size-lg { - @apply w-[calc(1rem+2*(1rem/2.058)+0.125rem)]; + .fri-badge--icon-only.fri-badge--size-lg { + @apply w-[calc(1rem+2*(1rem/2.058)+0.125rem)]; + } } diff --git a/packages/styles/src/components/button.css b/packages/styles/src/components/button.css index ae3821b..997f509 100644 --- a/packages/styles/src/components/button.css +++ b/packages/styles/src/components/button.css @@ -1,471 +1,473 @@ -.fri-button { - @apply relative inline-flex items-center justify-center; - @apply leading-none font-medium whitespace-nowrap; - @apply cursor-pointer border border-transparent select-none no-highlight; - @apply transition-colors duration-(--duration-hover) ease-out motion-reduce:transition-none; - - --fri-button-bg: var(--color-primary); - --fri-button-bg-hover: var(--color-primary-hover); - --fri-button-bg-pressed: var(--color-primary-active); - --fri-button-fg: var(--color-primary-foreground); - - @apply bg-(--fri-button-bg) text-(--fri-button-fg); - @apply gap-[calc(1rem/2.058)] px-[calc(1rem)] py-[calc(1rem/2.058)] text-base; - @apply rounded-md; - - &:focus-visible, - &[data-focus-visible] { - @apply focus-ring; - } +@layer components { + .fri-button { + @apply relative inline-flex items-center justify-center; + @apply leading-none font-medium whitespace-nowrap; + @apply cursor-pointer border border-transparent select-none no-highlight; + @apply transition-colors duration-(--duration-hover) ease-out motion-reduce:transition-none; + + --fri-button-bg: var(--color-primary); + --fri-button-bg-hover: var(--color-primary-hover); + --fri-button-bg-pressed: var(--color-primary-active); + --fri-button-fg: var(--color-primary-foreground); + + @apply bg-(--fri-button-bg) text-(--fri-button-fg); + @apply gap-[calc(1rem/2.058)] px-[calc(1rem)] py-[calc(1rem/2.058)] text-base; + @apply rounded-md; + + &:focus-visible, + &[data-focus-visible] { + @apply focus-ring; + } - &.fri-button--disabled { - @apply status-disabled; - } + &.fri-button--disabled { + @apply status-disabled; + } - &[data-pending="true"] { - @apply status-pending; - } + &[data-pending="true"] { + @apply status-pending; + } - &:active, - &[data-pressed] { - @apply bg-(--fri-button-bg-pressed) duration-(--duration-press); - } + &:active, + &[data-pressed] { + @apply bg-(--fri-button-bg-pressed) duration-(--duration-press); + } - @media (hover: hover) { - &:hover, - &[data-hovered] { - @apply bg-(--fri-button-bg-hover); + @media (hover: hover) { + &:hover, + &[data-hovered] { + @apply bg-(--fri-button-bg-hover); + } } } -} -.fri-button--primary { - --fri-button-bg: var(--color-primary); - --fri-button-bg-hover: var(--color-primary-hover); - --fri-button-bg-pressed: var(--color-primary-active); - --fri-button-fg: var(--color-primary-foreground); + .fri-button--primary { + --fri-button-bg: var(--color-primary); + --fri-button-bg-hover: var(--color-primary-hover); + --fri-button-bg-pressed: var(--color-primary-active); + --fri-button-fg: var(--color-primary-foreground); - @apply border border-primary-border; -} + @apply border border-primary-border; + } -.fri-button--secondary { - --fri-button-bg: var(--color-secondary); - --fri-button-bg-hover: var(--color-secondary-hover); - --fri-button-bg-pressed: var(--color-secondary-active); - --fri-button-fg: var(--color-secondary-foreground); + .fri-button--secondary { + --fri-button-bg: var(--color-secondary); + --fri-button-bg-hover: var(--color-secondary-hover); + --fri-button-bg-pressed: var(--color-secondary-active); + --fri-button-fg: var(--color-secondary-foreground); - @apply border border-secondary-border; -} + @apply border border-secondary-border; + } -.fri-button--accent { - --fri-button-bg: var(--color-accent); - --fri-button-bg-hover: var(--color-accent-hover); - --fri-button-bg-pressed: var(--color-accent-active); - --fri-button-fg: var(--color-accent-foreground); + .fri-button--accent { + --fri-button-bg: var(--color-accent); + --fri-button-bg-hover: var(--color-accent-hover); + --fri-button-bg-pressed: var(--color-accent-active); + --fri-button-fg: var(--color-accent-foreground); - @apply border border-accent-border; -} + @apply border border-accent-border; + } -.fri-button--ghost { - --fri-button-bg: transparent; - --fri-button-bg-hover: var(--color-muted); - --fri-button-bg-pressed: var(--color-muted-hover); - --fri-button-fg: var(--color-foreground); -} + .fri-button--ghost { + --fri-button-bg: transparent; + --fri-button-bg-hover: var(--color-muted); + --fri-button-bg-pressed: var(--color-muted-hover); + --fri-button-fg: var(--color-foreground); + } -.fri-button--outline { - --fri-button-bg: transparent; - --fri-button-bg-hover: var(--color-muted); - --fri-button-bg-pressed: var(--color-muted-hover); - --fri-button-fg: var(--color-foreground); + .fri-button--outline { + --fri-button-bg: transparent; + --fri-button-bg-hover: var(--color-muted); + --fri-button-bg-pressed: var(--color-muted-hover); + --fri-button-fg: var(--color-foreground); - @apply border-border; -} + @apply border-border; + } -.fri-button--glass { - --fri-button-bg: var(--color-glass); - --fri-button-bg-hover: var(--color-glass-hover); - --fri-button-bg-pressed: var(--color-glass-active); - --fri-button-fg: var(--color-glass-foreground); + .fri-button--glass { + --fri-button-bg: var(--color-glass); + --fri-button-bg-hover: var(--color-glass-hover); + --fri-button-bg-pressed: var(--color-glass-active); + --fri-button-fg: var(--color-glass-foreground); - @apply border-glass-border backdrop-blur-xl backdrop-saturate-200; -} + @apply border-glass-border backdrop-blur-xl backdrop-saturate-200; + } -.fri-button--frost { - --fri-button-bg: var(--color-glass); - --fri-button-bg-hover: var(--color-glass-hover); - --fri-button-bg-pressed: var(--color-glass-active); - --fri-button-fg: var(--color-glass-foreground); + .fri-button--frost { + --fri-button-bg: var(--color-glass); + --fri-button-bg-hover: var(--color-glass-hover); + --fri-button-bg-pressed: var(--color-glass-active); + --fri-button-fg: var(--color-glass-foreground); - @apply backdrop-blur-2xl backdrop-saturate-200; -} + @apply backdrop-blur-2xl backdrop-saturate-200; + } -.fri-button--overlay { - --fri-button-bg: var(--color-scrim); - --fri-button-bg-hover: var(--color-scrim-hover); - --fri-button-bg-pressed: var(--color-scrim-active); - --fri-button-fg: var(--color-scrim-foreground); + .fri-button--overlay { + --fri-button-bg: var(--color-scrim); + --fri-button-bg-hover: var(--color-scrim-hover); + --fri-button-bg-pressed: var(--color-scrim-active); + --fri-button-fg: var(--color-scrim-foreground); - @apply border-scrim-border backdrop-blur-xl backdrop-saturate-150; -} + @apply border-scrim-border backdrop-blur-xl backdrop-saturate-150; + } -.fri-button--success { - --fri-button-bg: var(--color-success); - --fri-button-bg-hover: var(--color-success-hover); - --fri-button-bg-pressed: var(--color-success-active); - --fri-button-fg: var(--color-success-foreground); + .fri-button--success { + --fri-button-bg: var(--color-success); + --fri-button-bg-hover: var(--color-success-hover); + --fri-button-bg-pressed: var(--color-success-active); + --fri-button-fg: var(--color-success-foreground); - @apply border border-success-border; -} + @apply border border-success-border; + } -.fri-button--warning { - --fri-button-bg: var(--color-warning); - --fri-button-bg-hover: var(--color-warning-hover); - --fri-button-bg-pressed: var(--color-warning-active); - --fri-button-fg: var(--color-warning-foreground); + .fri-button--warning { + --fri-button-bg: var(--color-warning); + --fri-button-bg-hover: var(--color-warning-hover); + --fri-button-bg-pressed: var(--color-warning-active); + --fri-button-fg: var(--color-warning-foreground); - @apply border border-warning-border; -} + @apply border border-warning-border; + } -.fri-button--danger { - --fri-button-bg: var(--color-danger); - --fri-button-bg-hover: var(--color-danger-hover); - --fri-button-bg-pressed: var(--color-danger-active); - --fri-button-fg: var(--color-danger-foreground); + .fri-button--danger { + --fri-button-bg: var(--color-danger); + --fri-button-bg-hover: var(--color-danger-hover); + --fri-button-bg-pressed: var(--color-danger-active); + --fri-button-fg: var(--color-danger-foreground); - @apply border border-danger-border; -} + @apply border border-danger-border; + } -.fri-button--info { - --fri-button-bg: var(--color-info); - --fri-button-bg-hover: var(--color-info-hover); - --fri-button-bg-pressed: var(--color-info-active); - --fri-button-fg: var(--color-info-foreground); + .fri-button--info { + --fri-button-bg: var(--color-info); + --fri-button-bg-hover: var(--color-info-hover); + --fri-button-bg-pressed: var(--color-info-active); + --fri-button-fg: var(--color-info-foreground); - @apply border border-info-border; -} -.fri-button--primary-soft { - --fri-button-bg: var(--color-primary-soft); - --fri-button-bg-hover: var(--color-primary-soft-hover); - --fri-button-bg-pressed: var(--color-primary-soft-active); - --fri-button-fg: var(--color-primary-soft-foreground); + @apply border border-info-border; + } + .fri-button--primary-soft { + --fri-button-bg: var(--color-primary-soft); + --fri-button-bg-hover: var(--color-primary-soft-hover); + --fri-button-bg-pressed: var(--color-primary-soft-active); + --fri-button-fg: var(--color-primary-soft-foreground); - @apply border border-primary-soft-border; -} + @apply border border-primary-soft-border; + } -.fri-button--secondary-soft { - --fri-button-bg: var(--color-secondary-soft); - --fri-button-bg-hover: var(--color-secondary-soft-hover); - --fri-button-bg-pressed: var(--color-secondary-soft-active); - --fri-button-fg: var(--color-secondary-soft-foreground); + .fri-button--secondary-soft { + --fri-button-bg: var(--color-secondary-soft); + --fri-button-bg-hover: var(--color-secondary-soft-hover); + --fri-button-bg-pressed: var(--color-secondary-soft-active); + --fri-button-fg: var(--color-secondary-soft-foreground); - @apply border border-secondary-soft-border; -} + @apply border border-secondary-soft-border; + } -.fri-button--accent-soft { - --fri-button-bg: var(--color-accent-soft); - --fri-button-bg-hover: var(--color-accent-soft-hover); - --fri-button-bg-pressed: var(--color-accent-soft-active); - --fri-button-fg: var(--color-accent-soft-foreground); + .fri-button--accent-soft { + --fri-button-bg: var(--color-accent-soft); + --fri-button-bg-hover: var(--color-accent-soft-hover); + --fri-button-bg-pressed: var(--color-accent-soft-active); + --fri-button-fg: var(--color-accent-soft-foreground); - @apply border border-accent-soft-border; -} + @apply border border-accent-soft-border; + } -.fri-button--success-soft { - --fri-button-bg: var(--color-success-soft); - --fri-button-bg-hover: var(--color-success-soft-hover); - --fri-button-bg-pressed: var(--color-success-soft-active); - --fri-button-fg: var(--color-success-soft-foreground); + .fri-button--success-soft { + --fri-button-bg: var(--color-success-soft); + --fri-button-bg-hover: var(--color-success-soft-hover); + --fri-button-bg-pressed: var(--color-success-soft-active); + --fri-button-fg: var(--color-success-soft-foreground); - @apply border border-success-soft-border; -} + @apply border border-success-soft-border; + } -.fri-button--warning-soft { - --fri-button-bg: var(--color-warning-soft); - --fri-button-bg-hover: var(--color-warning-soft-hover); - --fri-button-bg-pressed: var(--color-warning-soft-active); - --fri-button-fg: var(--color-warning-soft-foreground); + .fri-button--warning-soft { + --fri-button-bg: var(--color-warning-soft); + --fri-button-bg-hover: var(--color-warning-soft-hover); + --fri-button-bg-pressed: var(--color-warning-soft-active); + --fri-button-fg: var(--color-warning-soft-foreground); - @apply border border-warning-soft-border; -} + @apply border border-warning-soft-border; + } -.fri-button--danger-soft { - --fri-button-bg: var(--color-danger-soft); - --fri-button-bg-hover: var(--color-danger-soft-hover); - --fri-button-bg-pressed: var(--color-danger-soft-active); - --fri-button-fg: var(--color-danger-soft-foreground); + .fri-button--danger-soft { + --fri-button-bg: var(--color-danger-soft); + --fri-button-bg-hover: var(--color-danger-soft-hover); + --fri-button-bg-pressed: var(--color-danger-soft-active); + --fri-button-fg: var(--color-danger-soft-foreground); - @apply border border-danger-soft-border; -} + @apply border border-danger-soft-border; + } -.fri-button--info-soft { - --fri-button-bg: var(--color-info-soft); - --fri-button-bg-hover: var(--color-info-soft-hover); - --fri-button-bg-pressed: var(--color-info-soft-active); - --fri-button-fg: var(--color-info-soft-foreground); + .fri-button--info-soft { + --fri-button-bg: var(--color-info-soft); + --fri-button-bg-hover: var(--color-info-soft-hover); + --fri-button-bg-pressed: var(--color-info-soft-active); + --fri-button-fg: var(--color-info-soft-foreground); - @apply border border-info-soft-border; -} + @apply border border-info-soft-border; + } -.fri-button--primary-flat { - --fri-button-bg: var(--color-primary-flat); - --fri-button-bg-hover: var(--color-primary-flat-hover); - --fri-button-bg-pressed: var(--color-primary-flat-active); - --fri-button-fg: var(--color-primary-flat-foreground); -} + .fri-button--primary-flat { + --fri-button-bg: var(--color-primary-flat); + --fri-button-bg-hover: var(--color-primary-flat-hover); + --fri-button-bg-pressed: var(--color-primary-flat-active); + --fri-button-fg: var(--color-primary-flat-foreground); + } -.fri-button--secondary-flat { - --fri-button-bg: var(--color-secondary-flat); - --fri-button-bg-hover: var(--color-secondary-flat-hover); - --fri-button-bg-pressed: var(--color-secondary-flat-active); - --fri-button-fg: var(--color-secondary-flat-foreground); -} + .fri-button--secondary-flat { + --fri-button-bg: var(--color-secondary-flat); + --fri-button-bg-hover: var(--color-secondary-flat-hover); + --fri-button-bg-pressed: var(--color-secondary-flat-active); + --fri-button-fg: var(--color-secondary-flat-foreground); + } -.fri-button--accent-flat { - --fri-button-bg: var(--color-accent-flat); - --fri-button-bg-hover: var(--color-accent-flat-hover); - --fri-button-bg-pressed: var(--color-accent-flat-active); - --fri-button-fg: var(--color-accent-flat-foreground); -} + .fri-button--accent-flat { + --fri-button-bg: var(--color-accent-flat); + --fri-button-bg-hover: var(--color-accent-flat-hover); + --fri-button-bg-pressed: var(--color-accent-flat-active); + --fri-button-fg: var(--color-accent-flat-foreground); + } -.fri-button--success-flat { - --fri-button-bg: var(--color-success-flat); - --fri-button-bg-hover: var(--color-success-flat-hover); - --fri-button-bg-pressed: var(--color-success-flat-active); - --fri-button-fg: var(--color-success-flat-foreground); -} + .fri-button--success-flat { + --fri-button-bg: var(--color-success-flat); + --fri-button-bg-hover: var(--color-success-flat-hover); + --fri-button-bg-pressed: var(--color-success-flat-active); + --fri-button-fg: var(--color-success-flat-foreground); + } -.fri-button--warning-flat { - --fri-button-bg: var(--color-warning-flat); - --fri-button-bg-hover: var(--color-warning-flat-hover); - --fri-button-bg-pressed: var(--color-warning-flat-active); - --fri-button-fg: var(--color-warning-flat-foreground); -} + .fri-button--warning-flat { + --fri-button-bg: var(--color-warning-flat); + --fri-button-bg-hover: var(--color-warning-flat-hover); + --fri-button-bg-pressed: var(--color-warning-flat-active); + --fri-button-fg: var(--color-warning-flat-foreground); + } -.fri-button--danger-flat { - --fri-button-bg: var(--color-danger-flat); - --fri-button-bg-hover: var(--color-danger-flat-hover); - --fri-button-bg-pressed: var(--color-danger-flat-active); - --fri-button-fg: var(--color-danger-flat-foreground); -} + .fri-button--danger-flat { + --fri-button-bg: var(--color-danger-flat); + --fri-button-bg-hover: var(--color-danger-flat-hover); + --fri-button-bg-pressed: var(--color-danger-flat-active); + --fri-button-fg: var(--color-danger-flat-foreground); + } -.fri-button--info-flat { - --fri-button-bg: var(--color-info-flat); - --fri-button-bg-hover: var(--color-info-flat-hover); - --fri-button-bg-pressed: var(--color-info-flat-active); - --fri-button-fg: var(--color-info-flat-foreground); -} + .fri-button--info-flat { + --fri-button-bg: var(--color-info-flat); + --fri-button-bg-hover: var(--color-info-flat-hover); + --fri-button-bg-pressed: var(--color-info-flat-active); + --fri-button-fg: var(--color-info-flat-foreground); + } -.fri-button--primary-outline { - --fri-button-bg: var(--color-primary-outline); - --fri-button-bg-hover: var(--color-primary-outline-hover); - --fri-button-bg-pressed: var(--color-primary-outline-active); - --fri-button-fg: var(--color-primary-outline-foreground); + .fri-button--primary-outline { + --fri-button-bg: var(--color-primary-outline); + --fri-button-bg-hover: var(--color-primary-outline-hover); + --fri-button-bg-pressed: var(--color-primary-outline-active); + --fri-button-fg: var(--color-primary-outline-foreground); - @apply border border-primary-outline-border; -} + @apply border border-primary-outline-border; + } -.fri-button--secondary-outline { - --fri-button-bg: var(--color-secondary-outline); - --fri-button-bg-hover: var(--color-secondary-outline-hover); - --fri-button-bg-pressed: var(--color-secondary-outline-active); - --fri-button-fg: var(--color-secondary-outline-foreground); + .fri-button--secondary-outline { + --fri-button-bg: var(--color-secondary-outline); + --fri-button-bg-hover: var(--color-secondary-outline-hover); + --fri-button-bg-pressed: var(--color-secondary-outline-active); + --fri-button-fg: var(--color-secondary-outline-foreground); - @apply border border-secondary-outline-border; -} + @apply border border-secondary-outline-border; + } -.fri-button--accent-outline { - --fri-button-bg: var(--color-accent-outline); - --fri-button-bg-hover: var(--color-accent-outline-hover); - --fri-button-bg-pressed: var(--color-accent-outline-active); - --fri-button-fg: var(--color-accent-outline-foreground); + .fri-button--accent-outline { + --fri-button-bg: var(--color-accent-outline); + --fri-button-bg-hover: var(--color-accent-outline-hover); + --fri-button-bg-pressed: var(--color-accent-outline-active); + --fri-button-fg: var(--color-accent-outline-foreground); - @apply border border-accent-outline-border; -} + @apply border border-accent-outline-border; + } -.fri-button--success-outline { - --fri-button-bg: var(--color-success-outline); - --fri-button-bg-hover: var(--color-success-outline-hover); - --fri-button-bg-pressed: var(--color-success-outline-active); - --fri-button-fg: var(--color-success-outline-foreground); + .fri-button--success-outline { + --fri-button-bg: var(--color-success-outline); + --fri-button-bg-hover: var(--color-success-outline-hover); + --fri-button-bg-pressed: var(--color-success-outline-active); + --fri-button-fg: var(--color-success-outline-foreground); - @apply border border-success-outline-border; -} + @apply border border-success-outline-border; + } -.fri-button--warning-outline { - --fri-button-bg: var(--color-warning-outline); - --fri-button-bg-hover: var(--color-warning-outline-hover); - --fri-button-bg-pressed: var(--color-warning-outline-active); - --fri-button-fg: var(--color-warning-outline-foreground); + .fri-button--warning-outline { + --fri-button-bg: var(--color-warning-outline); + --fri-button-bg-hover: var(--color-warning-outline-hover); + --fri-button-bg-pressed: var(--color-warning-outline-active); + --fri-button-fg: var(--color-warning-outline-foreground); - @apply border border-warning-outline-border; -} + @apply border border-warning-outline-border; + } -.fri-button--danger-outline { - --fri-button-bg: var(--color-danger-outline); - --fri-button-bg-hover: var(--color-danger-outline-hover); - --fri-button-bg-pressed: var(--color-danger-outline-active); - --fri-button-fg: var(--color-danger-outline-foreground); + .fri-button--danger-outline { + --fri-button-bg: var(--color-danger-outline); + --fri-button-bg-hover: var(--color-danger-outline-hover); + --fri-button-bg-pressed: var(--color-danger-outline-active); + --fri-button-fg: var(--color-danger-outline-foreground); - @apply border border-danger-outline-border; -} + @apply border border-danger-outline-border; + } -.fri-button--info-outline { - --fri-button-bg: var(--color-info-outline); - --fri-button-bg-hover: var(--color-info-outline-hover); - --fri-button-bg-pressed: var(--color-info-outline-active); - --fri-button-fg: var(--color-info-outline-foreground); + .fri-button--info-outline { + --fri-button-bg: var(--color-info-outline); + --fri-button-bg-hover: var(--color-info-outline-hover); + --fri-button-bg-pressed: var(--color-info-outline-active); + --fri-button-fg: var(--color-info-outline-foreground); - @apply border border-info-outline-border; -} + @apply border border-info-outline-border; + } -.fri-button--primary-ghost { - --fri-button-bg: var(--color-primary-ghost); - --fri-button-bg-hover: var(--color-primary-ghost-hover); - --fri-button-bg-pressed: var(--color-primary-ghost-active); - --fri-button-fg: var(--color-primary-ghost-foreground); -} + .fri-button--primary-ghost { + --fri-button-bg: var(--color-primary-ghost); + --fri-button-bg-hover: var(--color-primary-ghost-hover); + --fri-button-bg-pressed: var(--color-primary-ghost-active); + --fri-button-fg: var(--color-primary-ghost-foreground); + } -.fri-button--secondary-ghost { - --fri-button-bg: var(--color-secondary-ghost); - --fri-button-bg-hover: var(--color-secondary-ghost-hover); - --fri-button-bg-pressed: var(--color-secondary-ghost-active); - --fri-button-fg: var(--color-secondary-ghost-foreground); -} + .fri-button--secondary-ghost { + --fri-button-bg: var(--color-secondary-ghost); + --fri-button-bg-hover: var(--color-secondary-ghost-hover); + --fri-button-bg-pressed: var(--color-secondary-ghost-active); + --fri-button-fg: var(--color-secondary-ghost-foreground); + } -.fri-button--accent-ghost { - --fri-button-bg: var(--color-accent-ghost); - --fri-button-bg-hover: var(--color-accent-ghost-hover); - --fri-button-bg-pressed: var(--color-accent-ghost-active); - --fri-button-fg: var(--color-accent-ghost-foreground); -} + .fri-button--accent-ghost { + --fri-button-bg: var(--color-accent-ghost); + --fri-button-bg-hover: var(--color-accent-ghost-hover); + --fri-button-bg-pressed: var(--color-accent-ghost-active); + --fri-button-fg: var(--color-accent-ghost-foreground); + } -.fri-button--success-ghost { - --fri-button-bg: var(--color-success-ghost); - --fri-button-bg-hover: var(--color-success-ghost-hover); - --fri-button-bg-pressed: var(--color-success-ghost-active); - --fri-button-fg: var(--color-success-ghost-foreground); -} + .fri-button--success-ghost { + --fri-button-bg: var(--color-success-ghost); + --fri-button-bg-hover: var(--color-success-ghost-hover); + --fri-button-bg-pressed: var(--color-success-ghost-active); + --fri-button-fg: var(--color-success-ghost-foreground); + } -.fri-button--warning-ghost { - --fri-button-bg: var(--color-warning-ghost); - --fri-button-bg-hover: var(--color-warning-ghost-hover); - --fri-button-bg-pressed: var(--color-warning-ghost-active); - --fri-button-fg: var(--color-warning-ghost-foreground); -} + .fri-button--warning-ghost { + --fri-button-bg: var(--color-warning-ghost); + --fri-button-bg-hover: var(--color-warning-ghost-hover); + --fri-button-bg-pressed: var(--color-warning-ghost-active); + --fri-button-fg: var(--color-warning-ghost-foreground); + } -.fri-button--danger-ghost { - --fri-button-bg: var(--color-danger-ghost); - --fri-button-bg-hover: var(--color-danger-ghost-hover); - --fri-button-bg-pressed: var(--color-danger-ghost-active); - --fri-button-fg: var(--color-danger-ghost-foreground); -} + .fri-button--danger-ghost { + --fri-button-bg: var(--color-danger-ghost); + --fri-button-bg-hover: var(--color-danger-ghost-hover); + --fri-button-bg-pressed: var(--color-danger-ghost-active); + --fri-button-fg: var(--color-danger-ghost-foreground); + } -.fri-button--info-ghost { - --fri-button-bg: var(--color-info-ghost); - --fri-button-bg-hover: var(--color-info-ghost-hover); - --fri-button-bg-pressed: var(--color-info-ghost-active); - --fri-button-fg: var(--color-info-ghost-foreground); -} + .fri-button--info-ghost { + --fri-button-bg: var(--color-info-ghost); + --fri-button-bg-hover: var(--color-info-ghost-hover); + --fri-button-bg-pressed: var(--color-info-ghost-active); + --fri-button-fg: var(--color-info-ghost-foreground); + } -.fri-button--size-sm { - @apply gap-[calc(0.875rem/2.058)] px-[calc(0.875rem)] py-[calc(0.875rem/2.058)] text-sm; -} + .fri-button--size-sm { + @apply gap-[calc(0.875rem/2.058)] px-[calc(0.875rem)] py-[calc(0.875rem/2.058)] text-sm; + } -.fri-button--size-md { - @apply gap-[calc(1rem/2.058)] px-[calc(1rem)] py-[calc(1rem/2.058)] text-base; -} + .fri-button--size-md { + @apply gap-[calc(1rem/2.058)] px-[calc(1rem)] py-[calc(1rem/2.058)] text-base; + } -.fri-button--size-lg { - @apply gap-[calc(1.25rem/2.058)] px-[calc(1.25rem)] py-[calc(1.25rem/2.058)] text-xl; -} + .fri-button--size-lg { + @apply gap-[calc(1.25rem/2.058)] px-[calc(1.25rem)] py-[calc(1.25rem/2.058)] text-xl; + } -.fri-button--size-xl { - @apply gap-[calc(1.5rem/2.058)] px-[calc(1.5rem)] py-[calc(1.5rem/2.058)] text-2xl; -} + .fri-button--size-xl { + @apply gap-[calc(1.5rem/2.058)] px-[calc(1.5rem)] py-[calc(1.5rem/2.058)] text-2xl; + } -.fri-button--full-width { - @apply w-full; -} + .fri-button--full-width { + @apply w-full; + } -.fri-button--icon-only { - @apply aspect-square w-[calc(1rem+2*(1rem/2.058)+0.125rem)] justify-center gap-0 p-0; -} + .fri-button--icon-only { + @apply aspect-square w-[calc(1rem+2*(1rem/2.058)+0.125rem)] justify-center gap-0 p-0; + } -.fri-button--icon-only.fri-button--size-sm { - @apply w-[calc(0.875rem+2*(0.875rem/2.058)+0.125rem)]; -} + .fri-button--icon-only.fri-button--size-sm { + @apply w-[calc(0.875rem+2*(0.875rem/2.058)+0.125rem)]; + } -.fri-button--icon-only.fri-button--size-lg { - @apply w-[calc(1.25rem+2*(1.25rem/2.058)+0.125rem)]; -} + .fri-button--icon-only.fri-button--size-lg { + @apply w-[calc(1.25rem+2*(1.25rem/2.058)+0.125rem)]; + } -.fri-button--icon-only.fri-button--size-xl { - @apply w-[calc(1.5rem+2*(1.5rem/2.058)+0.125rem)]; -} + .fri-button--icon-only.fri-button--size-xl { + @apply w-[calc(1.5rem+2*(1.5rem/2.058)+0.125rem)]; + } -.fri-button--radius-none { - @apply rounded-none; -} + .fri-button--radius-none { + @apply rounded-none; + } -.fri-button--radius-sm { - @apply rounded-[calc(1em*1.272/4)]; -} + .fri-button--radius-sm { + @apply rounded-[calc(1em*1.272/4)]; + } -.fri-button--radius-md { - @apply rounded-[calc(1em*1.272*2/4)]; -} + .fri-button--radius-md { + @apply rounded-[calc(1em*1.272*2/4)]; + } -.fri-button--radius-lg { - @apply rounded-[calc(1em*1.272*3/4)]; -} + .fri-button--radius-lg { + @apply rounded-[calc(1em*1.272*3/4)]; + } -.fri-button--radius-full { - @apply rounded-full; -} + .fri-button--radius-full { + @apply rounded-full; + } -.fri-button--pending { - @apply text-transparent; -} + .fri-button--pending { + @apply text-transparent; + } -.fri-button [slot="icon"], -.fri-button [slot="icon-start"], -.fri-button [slot="icon-end"] { - @apply size-match-font; -} + .fri-button [slot="icon"], + .fri-button [slot="icon-start"], + .fri-button [slot="icon-end"] { + @apply size-match-font; + } -.fri-button:has([slot="icon-start"]) { - @apply pl-[calc(1rem/2.058)]; -} + .fri-button:has([slot="icon-start"]) { + @apply pl-[calc(1rem/2.058)]; + } -.fri-button:has([slot="icon-end"]) { - @apply pr-[calc(1rem/2.058)]; -} + .fri-button:has([slot="icon-end"]) { + @apply pr-[calc(1rem/2.058)]; + } -.fri-button--size-sm:has([slot="icon-start"]) { - @apply pl-[calc(0.875rem/2.058)]; -} + .fri-button--size-sm:has([slot="icon-start"]) { + @apply pl-[calc(0.875rem/2.058)]; + } -.fri-button--size-sm:has([slot="icon-end"]) { - @apply pr-[calc(0.875rem/2.058)]; -} + .fri-button--size-sm:has([slot="icon-end"]) { + @apply pr-[calc(0.875rem/2.058)]; + } -.fri-button--size-lg:has([slot="icon-start"]) { - @apply pl-[calc(1.25rem/2.058)]; -} + .fri-button--size-lg:has([slot="icon-start"]) { + @apply pl-[calc(1.25rem/2.058)]; + } -.fri-button--size-lg:has([slot="icon-end"]) { - @apply pr-[calc(1.25rem/2.058)]; -} + .fri-button--size-lg:has([slot="icon-end"]) { + @apply pr-[calc(1.25rem/2.058)]; + } -.fri-button--size-xl:has([slot="icon-start"]) { - @apply pl-[calc(1.5rem/2.058)]; -} + .fri-button--size-xl:has([slot="icon-start"]) { + @apply pl-[calc(1.5rem/2.058)]; + } -.fri-button--size-xl:has([slot="icon-end"]) { - @apply pr-[calc(1.5rem/2.058)]; -} + .fri-button--size-xl:has([slot="icon-end"]) { + @apply pr-[calc(1.5rem/2.058)]; + } -.fri-button__spinner { - @apply absolute inset-0 m-auto size-match-font animate-spin text-(--fri-button-fg); + .fri-button__spinner { + @apply absolute inset-0 m-auto size-match-font animate-spin text-(--fri-button-fg); + } } diff --git a/packages/styles/src/components/index.css b/packages/styles/src/components/index.css index e31d03f..acfe99f 100644 --- a/packages/styles/src/components/index.css +++ b/packages/styles/src/components/index.css @@ -2,3 +2,5 @@ @import "./button.css"; @import "./surface.css"; + +@import "./avatar.css"; diff --git a/packages/styles/src/components/surface.css b/packages/styles/src/components/surface.css index 3fdf9bb..12d80cd 100644 --- a/packages/styles/src/components/surface.css +++ b/packages/styles/src/components/surface.css @@ -1,73 +1,75 @@ -.fri-surface { - @apply border border-transparent; - @apply bg-(--color-surface) text-(--color-surface-foreground); - @apply rounded-md; -} - -.fri-surface--default { - @apply bg-(--color-surface) text-(--color-surface-foreground); -} - -.fri-surface--default-bordered { - @apply border-(--color-surface-border) bg-(--color-surface) text-(--color-surface-foreground); -} - -.fri-surface--plain { - @apply bg-(--color-surface-plain) text-(--color-surface-plain-foreground); -} - -.fri-surface--plain-bordered { - @apply border-(--color-surface-plain-border) bg-(--color-surface-plain) text-(--color-surface-plain-foreground); -} - -.fri-surface--glass { - @apply border-glass-border bg-glass text-glass-foreground backdrop-blur-xl backdrop-saturate-200; -} - -.fri-surface--frost { - @apply bg-glass text-glass-foreground backdrop-blur-2xl backdrop-saturate-200; -} - -.fri-surface--overlay { - @apply border-scrim-border bg-scrim text-scrim-foreground backdrop-blur-xl backdrop-saturate-150; -} - -.fri-surface--shadow-none { - @apply shadow-none; -} - -.fri-surface--shadow-sm { - @apply shadow-sm; -} - -.fri-surface--shadow-md { - @apply shadow-md; -} - -.fri-surface--shadow-lg { - @apply shadow-lg; -} - -.fri-surface--shadow-xl { - @apply shadow-xl; -} - -.fri-surface--radius-none { - @apply rounded-none; -} - -.fri-surface--radius-sm { - @apply rounded-[calc(1em*1.272/4)]; -} - -.fri-surface--radius-md { - @apply rounded-[calc(1em*1.272*2/4)]; -} - -.fri-surface--radius-lg { - @apply rounded-[calc(1em*1.272*3/4)]; -} - -.fri-surface--radius-xl { - @apply rounded-[calc(1em*1.272)]; +@layer components { + .fri-surface { + @apply border border-transparent; + @apply bg-(--color-surface) text-(--color-surface-foreground); + @apply rounded-md; + } + + .fri-surface--default { + @apply bg-(--color-surface) text-(--color-surface-foreground); + } + + .fri-surface--default-bordered { + @apply border-(--color-surface-border) bg-(--color-surface) text-(--color-surface-foreground); + } + + .fri-surface--plain { + @apply bg-(--color-surface-plain) text-(--color-surface-plain-foreground); + } + + .fri-surface--plain-bordered { + @apply border-(--color-surface-plain-border) bg-(--color-surface-plain) text-(--color-surface-plain-foreground); + } + + .fri-surface--glass { + @apply border-glass-border bg-glass text-glass-foreground backdrop-blur-xl backdrop-saturate-200; + } + + .fri-surface--frost { + @apply bg-glass text-glass-foreground backdrop-blur-2xl backdrop-saturate-200; + } + + .fri-surface--overlay { + @apply border-scrim-border bg-scrim text-scrim-foreground backdrop-blur-xl backdrop-saturate-150; + } + + .fri-surface--shadow-none { + @apply shadow-none; + } + + .fri-surface--shadow-sm { + @apply shadow-sm; + } + + .fri-surface--shadow-md { + @apply shadow-md; + } + + .fri-surface--shadow-lg { + @apply shadow-lg; + } + + .fri-surface--shadow-xl { + @apply shadow-xl; + } + + .fri-surface--radius-none { + @apply rounded-none; + } + + .fri-surface--radius-sm { + @apply rounded-[calc(1em*1.272/4)]; + } + + .fri-surface--radius-md { + @apply rounded-[calc(1em*1.272*2/4)]; + } + + .fri-surface--radius-lg { + @apply rounded-[calc(1em*1.272*3/4)]; + } + + .fri-surface--radius-xl { + @apply rounded-[calc(1em*1.272)]; + } } diff --git a/packages/styles/src/tokens/colors.css b/packages/styles/src/tokens/colors.css index 2de06b2..6f55450 100644 --- a/packages/styles/src/tokens/colors.css +++ b/packages/styles/src/tokens/colors.css @@ -192,9 +192,12 @@ --color-surface-border: var(--color-fri-neutral-100); --color-surface-foreground: oklch(0% 0 0); --color-surface-plain: oklch(100% 0 0); - --color-surface-plain-border: var(--color-fri-neutral-100); + --color-surface-plain-border: var(--color-fri-neutral-950); --color-surface-plain-foreground: oklch(0% 0 0); + --color-avatar: var(--color-fri-neutral-50); + --color-avatar-foreground: var(--color-fri-neutral-700); + --color-popover: oklch(100% 0 0); --color-popover-foreground: oklch(0% 0 0); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6bef44c..f607fea 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -226,6 +226,9 @@ importers: '@fried-ui/styles': specifier: workspace:^ version: link:../styles + '@radix-ui/react-avatar': + specifier: ^1.1.11 + version: 1.1.11(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@react-types/color': specifier: ^3.1.4 version: 3.1.4(react@19.2.0) @@ -1269,6 +1272,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-avatar@1.1.11': + resolution: {integrity: sha512-0Qk603AHGV28BOBO34p7IgD5m+V5Sg/YovfayABkoDDBM5d3NCx0Mp4gGrjzLGes1jV5eNOE1r3itqOR33VC6Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-collapsible@1.1.12': resolution: {integrity: sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==} peerDependencies: @@ -1313,6 +1329,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-context@1.1.3': + resolution: {integrity: sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-dialog@1.1.15': resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==} peerDependencies: @@ -1457,6 +1482,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-primitive@2.1.4': + resolution: {integrity: sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-roving-focus@1.1.11': resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==} peerDependencies: @@ -1550,6 +1588,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-is-hydrated@0.1.0': + resolution: {integrity: sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-layout-effect@1.1.1': resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} peerDependencies: @@ -7083,6 +7130,19 @@ snapshots: '@types/react': 19.2.2 '@types/react-dom': 19.2.2(@types/react@19.2.2) + '@radix-ui/react-avatar@1.1.11(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@radix-ui/react-context': 1.1.3(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.2 + '@types/react-dom': 19.2.2(@types/react@19.2.2) + '@radix-ui/react-collapsible@1.1.12(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@radix-ui/primitive': 1.1.3 @@ -7123,6 +7183,12 @@ snapshots: optionalDependencies: '@types/react': 19.2.2 + '@radix-ui/react-context@1.1.3(@types/react@19.2.2)(react@19.2.0)': + dependencies: + react: 19.2.0 + optionalDependencies: + '@types/react': 19.2.2 + '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@radix-ui/primitive': 1.1.3 @@ -7280,6 +7346,15 @@ snapshots: '@types/react': 19.2.2 '@types/react-dom': 19.2.2(@types/react@19.2.2) + '@radix-ui/react-primitive@2.1.4(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@radix-ui/react-slot': 1.2.4(@types/react@19.2.2)(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.2 + '@types/react-dom': 19.2.2(@types/react@19.2.2) + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@radix-ui/primitive': 1.1.3 @@ -7372,6 +7447,13 @@ snapshots: optionalDependencies: '@types/react': 19.2.2 + '@radix-ui/react-use-is-hydrated@0.1.0(@types/react@19.2.2)(react@19.2.0)': + dependencies: + react: 19.2.0 + use-sync-external-store: 1.6.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.2 + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.2)(react@19.2.0)': dependencies: react: 19.2.0 From 791f3a39e0a44229bdc5cc4224e175001e4574da Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Sun, 19 Apr 2026 05:33:00 +0700 Subject: [PATCH 066/199] fix(docs): Update Surface variants to match current API Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/docs/src/components/MdxComponents.tsx | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/apps/docs/src/components/MdxComponents.tsx b/apps/docs/src/components/MdxComponents.tsx index 4e711c0..86e1773 100644 --- a/apps/docs/src/components/MdxComponents.tsx +++ b/apps/docs/src/components/MdxComponents.tsx @@ -297,16 +297,28 @@ const SurfaceVariants = (): React.JSX.Element => { Default
- - Bordered + + Default Bordered + + + + Plain + + + + Plain Bordered Glass - - Primary + + Frost + + + + Overlay
); From 6776cfe952ce9f6274782dc148c56d1919c2d955 Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Sun, 19 Apr 2026 06:51:55 +0700 Subject: [PATCH 067/199] feat(react): Add Avatar ring and CustomStyle - Avatar: ring prop (7 colors) using -outline-border tokens - Avatar: add 2xl size + rich argType description - Avatar: switch inline-flex to flex (fixes baseline gap) - Surface: 8 status variants (success/warning/danger/info) - Surface/Badge/Button/Avatar: CustomStyle story for className - Rules: argType 4-part description pattern, post-scaffold audit - Generator: TODO template for variant description Co-Authored-By: Claude Opus 4.7 (1M context) --- .../react/src/components/avatar/Avatar.tsx | 3 +- .../src/components/avatar/avatar.stories.tsx | 150 ++++++++++++++++-- .../src/components/avatar/avatar.test.tsx | 16 ++ .../src/components/avatar/avatar.variants.ts | 1 + .../src/components/badge/badge.stories.tsx | 24 +++ .../src/components/button/button.stories.tsx | 24 +++ .../components/surface/surface.stories.tsx | 117 +++++++++++++- .../src/components/surface/surface.test.tsx | 18 ++- .../components/surface/surface.variants.ts | 17 +- packages/styles/src/components/avatar.css | 32 +++- packages/styles/src/components/surface.css | 32 ++++ packages/styles/src/tokens/colors.css | 2 +- .../component.stories.tsx.hbs | 4 +- .../component.stories.tsx.hbs | 4 +- 14 files changed, 422 insertions(+), 22 deletions(-) diff --git a/packages/react/src/components/avatar/Avatar.tsx b/packages/react/src/components/avatar/Avatar.tsx index 62431aa..3d62a52 100644 --- a/packages/react/src/components/avatar/Avatar.tsx +++ b/packages/react/src/components/avatar/Avatar.tsx @@ -46,7 +46,7 @@ const AvatarFallback = (props: Readonly) => { AvatarFallback.displayName = "Avatar.Fallback"; const Avatar = (props: Readonly) => { - const { children, className, isDisabled, radius, ref, size, ...rest } = props; + const { children, className, isDisabled, radius, ref, ring, size, ...rest } = props; const avatarClassName = clsx( bem({ @@ -54,6 +54,7 @@ const Avatar = (props: Readonly) => { modifiers: { size, radius, + ring, disabled: isDisabled, }, }), diff --git a/packages/react/src/components/avatar/avatar.stories.tsx b/packages/react/src/components/avatar/avatar.stories.tsx index efa3d70..f5c10c9 100644 --- a/packages/react/src/components/avatar/avatar.stories.tsx +++ b/packages/react/src/components/avatar/avatar.stories.tsx @@ -13,11 +13,20 @@ const SAMPLE_SRC_3 = "https://images.unsplash.com/photo-1576348076752-6085814e5a51?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MjZ8fGF2YXRhciUyMGN1dGUlMjBnaXJsfGVufDB8fDB8fHww"; const RADIUS_SRC = - "https://images.unsplash.com/photo-1565972476522-4338522b1929?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTZ8fGNpcmNsZXxlbnwwfHwwfHx8MA%3D%3D"; + "https://images.unsplash.com/photo-1656919380078-f0ff7bcd1d92?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MjE5fHxwYXN0ZWx8ZW58MHx8MHx8fDA%3D"; const SAMPLE_SRC_4 = "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTB8fGZhY2V8ZW58MHx8MHx8fDA%3D"; +const SAMPLE_SRC_5 = + "https://images.unsplash.com/photo-1724690336308-02af024b76ab?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTB8fHdvbWVuJTIwY3V0ZXxlbnwwfHwwfHx8MA%3D%3D"; + +const SAMPLE_SRC_6 = + "https://images.unsplash.com/photo-1596554002543-83af3d801c6f?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxzZWFyY2h8ODd8fHdvbWVuJTIwY3V0ZXxlbnwwfHwwfHx8MA%3D%3D"; + +const SAMPLE_SRC_7 = + "https://images.unsplash.com/flagged/photo-1557610650-841aa71a5c3d?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTA5fHx3b21lbiUyMGN1dGV8ZW58MHx8MHx8fDA%3D"; + const meta = { title: "Components/Avatar", component: Avatar, @@ -31,6 +40,14 @@ const meta = { isDisabled: false, }, argTypes: { + children: { + control: false, + description: "Compound children — `` and ``", + table: { + type: { summary: "ReactNode" }, + category: "Children", + }, + }, size: { control: "select", options: ["sm", "md", "lg", "xl", "2xl"], @@ -51,6 +68,18 @@ const meta = { category: "Style Variants", }, }, + ring: { + control: "select", + options: [undefined, "primary", "secondary", "accent", "success", "warning", "danger", "info"], + description: + "Colored ring around the avatar (status indicator). **Brand:** primary (main focus), secondary (neutral gray), accent (brand highlight). **Status:** success (online), warning (away), danger (busy / do-not-disturb), info (notification). Use success for online, danger for busy, warning for away, primary for selected, accent for featured/pro user.", + table: { + type: { + summary: '"primary" | "secondary" | "accent" | "success" | "warning" | "danger" | "info"', + }, + category: "Style Variants", + }, + }, isDisabled: { control: "boolean", description: "Dims the avatar and disables pointer events", @@ -168,12 +197,12 @@ const Sizes = () => { - + A4 - + A5
@@ -200,12 +229,12 @@ const Sizes = () => { - + A4 - + A5
@@ -281,6 +310,95 @@ const Radius = () => { ), }; +const Ring: Story = { + parameters: { + docs: { + source: { + code: `import { Avatar } from "@fried-ui/react"; + +const Ring = () => { + return ( +
+ + + CT + + + + + RT + + + + + YK + + + + + NV + + + + + IO + + + + + ZX + + + + + AP + +
+ ); +};`, + }, + }, + }, + render: (args): React.JSX.Element => ( +
+ + + CT + + + + + RT + + + + + YK + + + + + NV + + + + + IO + + + + + ZX + + + + + AP + +
+ ), +}; + const Disabled: Story = { parameters: { docs: { @@ -345,7 +463,7 @@ const CustomStyle: Story = { const CustomStyle = () => { return ( -
+
4XL @@ -366,8 +484,14 @@ const CustomStyle = () => { ON - +
+ + + + CG + +
); };`, @@ -375,7 +499,7 @@ const CustomStyle = () => { }, }, render: (args): React.JSX.Element => ( -
+
4XL @@ -396,12 +520,18 @@ const CustomStyle = () => { ON - +
+ + + + CG + +
), }; -export { Default, WithFallback, Sizes, Radius, Disabled, BrokenImage, CustomStyle }; +export { Default, WithFallback, Sizes, Radius, Ring, Disabled, BrokenImage, CustomStyle }; export default meta; diff --git a/packages/react/src/components/avatar/avatar.test.tsx b/packages/react/src/components/avatar/avatar.test.tsx index 40a7fdb..03a54a8 100644 --- a/packages/react/src/components/avatar/avatar.test.tsx +++ b/packages/react/src/components/avatar/avatar.test.tsx @@ -43,6 +43,22 @@ describe("Avatar", () => { }); }); + it("applies all ring classes", () => { + const rings = ["primary", "secondary", "accent", "success", "warning", "danger", "info"] as const; + + rings.forEach((ring) => { + const { container, unmount } = render( + + A + , + ); + + const root = container.querySelector("[data-slot='avatar']"); + expect(root?.className).toContain(`fri-avatar--ring-${ring}`); + unmount(); + }); + }); + it("applies all radius classes", () => { const radii = ["none", "sm", "md", "lg", "full"] as const; diff --git a/packages/react/src/components/avatar/avatar.variants.ts b/packages/react/src/components/avatar/avatar.variants.ts index 2833f70..48fd51d 100644 --- a/packages/react/src/components/avatar/avatar.variants.ts +++ b/packages/react/src/components/avatar/avatar.variants.ts @@ -1,5 +1,6 @@ export interface AvatarVariantsProps { isDisabled?: boolean; radius?: "none" | "sm" | "md" | "lg" | "full"; + ring?: "primary" | "secondary" | "accent" | "success" | "warning" | "danger" | "info"; size?: "sm" | "md" | "lg" | "xl" | "2xl"; } diff --git a/packages/react/src/components/badge/badge.stories.tsx b/packages/react/src/components/badge/badge.stories.tsx index 3ab382a..47817e4 100644 --- a/packages/react/src/components/badge/badge.stories.tsx +++ b/packages/react/src/components/badge/badge.stories.tsx @@ -707,6 +707,29 @@ const IconOnly = () => { ), }; +const CustomStyle: Story = { + parameters: { + docs: { + source: { + code: `import { Badge } from "@fried-ui/react"; + +const CustomStyle = () => { + return ( + + Gradient + + ); +};`, + }, + }, + }, + render: (args): React.JSX.Element => ( + + Gradient + + ), +}; + export { Default, Variants, @@ -720,6 +743,7 @@ export { Radius, WithIcon, IconOnly, + CustomStyle, }; export default meta; diff --git a/packages/react/src/components/button/button.stories.tsx b/packages/react/src/components/button/button.stories.tsx index 1996a6e..cc7f561 100644 --- a/packages/react/src/components/button/button.stories.tsx +++ b/packages/react/src/components/button/button.stories.tsx @@ -882,6 +882,29 @@ const OverlayVariants = () => { ), }; +const CustomStyle: Story = { + parameters: { + docs: { + source: { + code: `import { Button } from "@fried-ui/react"; + +const CustomStyle = () => { + return ( + + ); +};`, + }, + }, + }, + render: (args): React.JSX.Element => ( + + ), +}; + export { Default, Variants, @@ -900,6 +923,7 @@ export { Disabled, Pending, RenderProps, + CustomStyle, }; export default meta; diff --git a/packages/react/src/components/surface/surface.stories.tsx b/packages/react/src/components/surface/surface.stories.tsx index a4a8777..57a8fd6 100644 --- a/packages/react/src/components/surface/surface.stories.tsx +++ b/packages/react/src/components/surface/surface.stories.tsx @@ -28,12 +28,29 @@ const meta = { }, variant: { control: "select", - options: ["default", "default-bordered", "plain", "plain-bordered", "glass", "frost", "overlay"], + options: [ + "default", + "default-bordered", + "plain", + "plain-bordered", + "success", + "success-bordered", + "warning", + "warning-bordered", + "danger", + "danger-bordered", + "info", + "info-bordered", + "glass", + "frost", + "overlay", + ], description: - "Visual style. **Base:** default (subtle gray card), default-bordered (gray + border), plain (pure white — for gray pages), plain-bordered (white + border). **Special:** glass (frosted + border), frost (frosted no border), overlay (dark scrim — on media). Use default on white pages, plain on gray pages, *-bordered for emphasis, glass/frost/overlay for media overlays.", + "Visual style. **Base:** default/plain (neutral cards). **Status:** success/warning/danger/info (pale bg — alert/callout boxes). **Special:** glass/frost/overlay (media overlays). Add `-bordered` suffix for emphasis border. Use default on white pages, plain on gray pages, status for alerts.", table: { type: { - summary: '"default" | "default-bordered" | "plain" | "plain-bordered" | "glass" | "frost" | "overlay"', + summary: + '"default" | "default-bordered" | "plain" | "plain-bordered" | "success" | "success-bordered" | "warning" | "warning-bordered" | "danger" | "danger-bordered" | "info" | "info-bordered" | "glass" | "frost" | "overlay"', }, defaultValue: { summary: "default", @@ -148,6 +165,62 @@ const Variants = () => { ), }; +const StatusVariants: Story = { + parameters: { + docs: { + source: { + code: `import { Surface } from "@fried-ui/react"; + +const StatusVariants = () => { + return ( +
+ +

Success

+

Your changes have been saved.

+
+ +

Warning

+

Your session expires in 5 minutes.

+
+ +

Error

+

Unable to connect to the server.

+
+ +

Info

+

New feature available — check it out!

+
+
+ ); +};`, + }, + }, + }, + render: (args): React.JSX.Element => ( +
+ +

Success

+

Your changes have been saved.

+
+ + +

Warning

+

Your session expires in 5 minutes.

+
+ + +

Error

+

Unable to connect to the server.

+
+ + +

Info

+

New feature available — check it out!

+
+
+ ), +}; + const PlainVariants: Story = { parameters: { docs: { @@ -412,6 +485,42 @@ const Shadow = () => { ), }; -export { Default, Variants, PlainVariants, GlassVariants, FrostVariants, OverlayVariants, Radius, Shadow }; +const CustomStyle: Story = { + parameters: { + docs: { + source: { + code: `import { Surface } from "@fried-ui/react"; + +const CustomStyle = () => { + return ( + +

Gradient Card

+

Pastel gradient via className.

+
+ ); +};`, + }, + }, + }, + render: (args): React.JSX.Element => ( + +

Gradient Card

+

Pastel gradient via className.

+
+ ), +}; + +export { + Default, + Variants, + StatusVariants, + PlainVariants, + GlassVariants, + FrostVariants, + OverlayVariants, + Radius, + Shadow, + CustomStyle, +}; export default meta; diff --git a/packages/react/src/components/surface/surface.test.tsx b/packages/react/src/components/surface/surface.test.tsx index ccbf5de..5c3e9f5 100644 --- a/packages/react/src/components/surface/surface.test.tsx +++ b/packages/react/src/components/surface/surface.test.tsx @@ -17,7 +17,23 @@ describe("Surface", () => { }); it("applies all variant classes", () => { - const variants = ["default", "default-bordered", "plain", "plain-bordered", "glass", "frost", "overlay"] as const; + const variants = [ + "default", + "default-bordered", + "plain", + "plain-bordered", + "success", + "success-bordered", + "warning", + "warning-bordered", + "danger", + "danger-bordered", + "info", + "info-bordered", + "glass", + "frost", + "overlay", + ] as const; variants.forEach((variant) => { const { unmount } = render({variant}); diff --git a/packages/react/src/components/surface/surface.variants.ts b/packages/react/src/components/surface/surface.variants.ts index 9af402e..727572d 100644 --- a/packages/react/src/components/surface/surface.variants.ts +++ b/packages/react/src/components/surface/surface.variants.ts @@ -1,5 +1,20 @@ export interface SurfaceVariantsProps { radius?: "none" | "sm" | "md" | "lg" | "xl"; shadow?: "none" | "sm" | "md" | "lg" | "xl"; - variant?: "default" | "default-bordered" | "plain" | "plain-bordered" | "glass" | "frost" | "overlay"; + variant?: + | "default" + | "default-bordered" + | "plain" + | "plain-bordered" + | "success" + | "success-bordered" + | "warning" + | "warning-bordered" + | "danger" + | "danger-bordered" + | "info" + | "info-bordered" + | "glass" + | "frost" + | "overlay"; } diff --git a/packages/styles/src/components/avatar.css b/packages/styles/src/components/avatar.css index c77e7e5..4918fea 100644 --- a/packages/styles/src/components/avatar.css +++ b/packages/styles/src/components/avatar.css @@ -1,13 +1,13 @@ @layer components { .fri-avatar { - @apply relative inline-flex shrink-0 items-center justify-center overflow-hidden select-none; + @apply relative flex shrink-0 items-center justify-center overflow-hidden select-none; @apply leading-none font-medium; @apply size-10 rounded-full text-sm; @apply bg-(--color-avatar) text-(--color-avatar-foreground); } .fri-avatar__image { - @apply size-full object-cover; + @apply block size-full object-cover; } .fri-avatar__fallback { @@ -57,4 +57,32 @@ .fri-avatar--disabled { @apply pointer-events-none opacity-50; } + + .fri-avatar--ring-primary { + @apply ring-2 ring-(--color-primary-outline-border) ring-offset-2 ring-offset-(--color-background); + } + + .fri-avatar--ring-secondary { + @apply ring-2 ring-(--color-secondary-outline-border) ring-offset-2 ring-offset-(--color-background); + } + + .fri-avatar--ring-accent { + @apply ring-2 ring-(--color-accent-outline-border) ring-offset-2 ring-offset-(--color-background); + } + + .fri-avatar--ring-success { + @apply ring-2 ring-(--color-success-outline-border) ring-offset-2 ring-offset-(--color-background); + } + + .fri-avatar--ring-warning { + @apply ring-2 ring-(--color-warning-outline-border) ring-offset-2 ring-offset-(--color-background); + } + + .fri-avatar--ring-danger { + @apply ring-2 ring-(--color-danger-outline-border) ring-offset-2 ring-offset-(--color-background); + } + + .fri-avatar--ring-info { + @apply ring-2 ring-(--color-info-outline-border) ring-offset-2 ring-offset-(--color-background); + } } diff --git a/packages/styles/src/components/surface.css b/packages/styles/src/components/surface.css index 12d80cd..939b1a0 100644 --- a/packages/styles/src/components/surface.css +++ b/packages/styles/src/components/surface.css @@ -21,6 +21,38 @@ @apply border-(--color-surface-plain-border) bg-(--color-surface-plain) text-(--color-surface-plain-foreground); } + .fri-surface--success { + @apply bg-(--color-success-soft) text-(--color-success-soft-foreground); + } + + .fri-surface--success-bordered { + @apply border-(--color-success-soft-border) bg-(--color-success-soft) text-(--color-success-soft-foreground); + } + + .fri-surface--warning { + @apply bg-(--color-warning-soft) text-(--color-warning-soft-foreground); + } + + .fri-surface--warning-bordered { + @apply border-(--color-warning-soft-border) bg-(--color-warning-soft) text-(--color-warning-soft-foreground); + } + + .fri-surface--danger { + @apply bg-(--color-danger-soft) text-(--color-danger-soft-foreground); + } + + .fri-surface--danger-bordered { + @apply border-(--color-danger-soft-border) bg-(--color-danger-soft) text-(--color-danger-soft-foreground); + } + + .fri-surface--info { + @apply bg-(--color-info-soft) text-(--color-info-soft-foreground); + } + + .fri-surface--info-bordered { + @apply border-(--color-info-soft-border) bg-(--color-info-soft) text-(--color-info-soft-foreground); + } + .fri-surface--glass { @apply border-glass-border bg-glass text-glass-foreground backdrop-blur-xl backdrop-saturate-200; } diff --git a/packages/styles/src/tokens/colors.css b/packages/styles/src/tokens/colors.css index 6f55450..c87837a 100644 --- a/packages/styles/src/tokens/colors.css +++ b/packages/styles/src/tokens/colors.css @@ -192,7 +192,7 @@ --color-surface-border: var(--color-fri-neutral-100); --color-surface-foreground: oklch(0% 0 0); --color-surface-plain: oklch(100% 0 0); - --color-surface-plain-border: var(--color-fri-neutral-950); + --color-surface-plain-border: var(--color-fri-neutral-100); --color-surface-plain-foreground: oklch(0% 0 0); --color-avatar: var(--color-fri-neutral-50); diff --git a/turbo/generators/display-component/component.stories.tsx.hbs b/turbo/generators/display-component/component.stories.tsx.hbs index 25d720e..9180ca0 100644 --- a/turbo/generators/display-component/component.stories.tsx.hbs +++ b/turbo/generators/display-component/component.stories.tsx.hbs @@ -26,7 +26,9 @@ const meta = { variant: { control: "select", options: ["primary", "secondary", "ghost", "outline", "success", "warning", "danger", "info"], - description: "The visual variant of the {{dashCase name}}", + // TODO: rewrite this description following the 4-part pattern in .claude/rules/storybook.md (intent + groups + modifiers + usage). + // Length: 50-120 words. Bold headings. End with "Use X for Y" guidance. See Badge/Surface for reference. + description: "Visual style. **Base:** TODO. **Status:** TODO. Use primary for TODO, success/danger for TODO.", table: { type: { summary: '"primary" | "secondary" | "ghost" | "outline" | "success" | "warning" | "danger" | "info"' }, defaultValue: { summary: "primary" }, diff --git a/turbo/generators/interactive-component/component.stories.tsx.hbs b/turbo/generators/interactive-component/component.stories.tsx.hbs index 0f33b5f..13f11d5 100644 --- a/turbo/generators/interactive-component/component.stories.tsx.hbs +++ b/turbo/generators/interactive-component/component.stories.tsx.hbs @@ -26,7 +26,9 @@ const meta = { variant: { control: "select", options: ["primary", "secondary", "ghost", "outline", "success", "warning", "danger", "info"], - description: "The visual variant of the {{dashCase name}}", + // TODO: rewrite this description following the 4-part pattern in .claude/rules/storybook.md (intent + groups + modifiers + usage). + // Length: 50-120 words. Bold headings. End with "Use X for Y" guidance. See Button/Badge for reference. + description: "Visual style. **Base:** TODO. **Status:** TODO. Use primary for TODO, danger for TODO.", table: { type: { summary: '"primary" | "secondary" | "ghost" | "outline" | "success" | "warning" | "danger" | "info"' }, defaultValue: { summary: "primary" }, From 3e12c02db955694bfd5e07b5a739e84f0c3a6b2b Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Sun, 19 Apr 2026 06:56:48 +0700 Subject: [PATCH 068/199] fix(ci): Build docs via turbo on Vercel Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/docs/vercel.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/docs/vercel.json b/apps/docs/vercel.json index 47dc242..b2e861e 100644 --- a/apps/docs/vercel.json +++ b/apps/docs/vercel.json @@ -1,6 +1,6 @@ { - "buildCommand": "pnpm build", - "installCommand": "pnpm install", + "buildCommand": "cd ../.. && pnpm turbo run build --filter=docs", + "installCommand": "cd ../.. && pnpm install", "framework": "nextjs", "ignoreCommand": "npx turbo-ignore" } From 0b6fad2a849addcaab45ae68fc948684d8805307 Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Sun, 19 Apr 2026 07:16:49 +0700 Subject: [PATCH 069/199] feat(gitignore): Add AI-related files to .gitignore --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index ba0d155..0d60f67 100644 --- a/.gitignore +++ b/.gitignore @@ -52,6 +52,10 @@ pnpm-debug.log* # MCP config (each dev manages their own) .mcp.json +# AI +.claude/ +CLAUDE.md + # Misc .DS_Store *.pem From f87782a40ee9af22ba013596c0bb017c9c60479f Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Sun, 19 Apr 2026 07:43:07 +0700 Subject: [PATCH 070/199] feat(react): Update Avatar, Badge, and Button components --- .../src/components/avatar/avatar.stories.tsx | 4 +- .../src/components/badge/badge.stories.tsx | 4 +- .../src/components/button/button.stories.tsx | 4 +- packages/react/src/utils/css-audit.test.ts | 117 ++++++++++++++++++ packages/styles/src/components/badge.css | 16 +-- packages/styles/src/components/button.css | 14 --- 6 files changed, 124 insertions(+), 35 deletions(-) create mode 100644 packages/react/src/utils/css-audit.test.ts diff --git a/packages/react/src/components/avatar/avatar.stories.tsx b/packages/react/src/components/avatar/avatar.stories.tsx index f5c10c9..0eb1d15 100644 --- a/packages/react/src/components/avatar/avatar.stories.tsx +++ b/packages/react/src/components/avatar/avatar.stories.tsx @@ -488,7 +488,7 @@ const CustomStyle = () => {
- + CG @@ -524,7 +524,7 @@ const CustomStyle = () => {
- + CG diff --git a/packages/react/src/components/badge/badge.stories.tsx b/packages/react/src/components/badge/badge.stories.tsx index 47817e4..1915630 100644 --- a/packages/react/src/components/badge/badge.stories.tsx +++ b/packages/react/src/components/badge/badge.stories.tsx @@ -715,7 +715,7 @@ const CustomStyle: Story = { const CustomStyle = () => { return ( - + Gradient ); @@ -724,7 +724,7 @@ const CustomStyle = () => { }, }, render: (args): React.JSX.Element => ( - + Gradient ), diff --git a/packages/react/src/components/button/button.stories.tsx b/packages/react/src/components/button/button.stories.tsx index cc7f561..4847d17 100644 --- a/packages/react/src/components/button/button.stories.tsx +++ b/packages/react/src/components/button/button.stories.tsx @@ -890,7 +890,7 @@ const CustomStyle: Story = { const CustomStyle = () => { return ( - ); @@ -899,7 +899,7 @@ const CustomStyle = () => { }, }, render: (args): React.JSX.Element => ( - ), diff --git a/packages/react/src/utils/css-audit.test.ts b/packages/react/src/utils/css-audit.test.ts new file mode 100644 index 0000000..0604001 --- /dev/null +++ b/packages/react/src/utils/css-audit.test.ts @@ -0,0 +1,117 @@ +import fs from "node:fs"; +import path from "node:path"; + +import { describe, expect, it } from "vitest"; + +const stylesComponentsDir = path.resolve(import.meta.dirname, "..", "..", "..", "styles", "src", "components"); + +interface ReadCssParams { + file: string; +} + +interface ExtractRulesParams { + classPattern: RegExp; + content: string; +} + +function readCss({ file }: ReadCssParams): string { + return fs.readFileSync(path.join(stylesComponentsDir, file), "utf-8"); +} + +function extractRules({ content, classPattern }: ExtractRulesParams): string[] { + const rules: string[] = []; + const lines = content.split("\n"); + let isCapturing = false; + let depth = 0; + let current = ""; + + for (const line of lines) { + if (!isCapturing && classPattern.test(line) && line.includes("{")) { + isCapturing = true; + depth = 1; + current = line + "\n"; + continue; + } + + if (isCapturing) { + current += line + "\n"; + depth += (line.match(/\{/g) ?? []).length; + depth -= (line.match(/\}/g) ?? []).length; + + if (depth === 0) { + rules.push(current); + isCapturing = false; + current = ""; + } + } + } + + return rules; +} + +describe("CSS dead class audit", () => { + it("outline variants must use -outline-border token (not transparent -border)", () => { + const files = fs.readdirSync(stylesComponentsDir).filter((file) => file.endsWith(".css")); + const errors: string[] = []; + + for (const file of files) { + const content = readCss({ file }); + const outlineRules = extractRules({ content, classPattern: /\.fri-[a-z]+--[a-z-]*outline\s*\{/ }); + + for (const rule of outlineRules) { + const classMatch = rule.match(/\.fri-[a-z-]+/); + const className = classMatch ? classMatch[0] : "unknown"; + const badBorderPattern = /border-[a-z]+-border\b(?!-outline)/; + + if (badBorderPattern.test(rule) && !rule.includes("-outline-border")) { + errors.push(`${file} ${className}: outline variant uses transparent -border token`); + } + } + } + + expect(errors).toEqual([]); + }); + + it("soft variants must use -soft-border token", () => { + const files = fs.readdirSync(stylesComponentsDir).filter((file) => file.endsWith(".css")); + const errors: string[] = []; + + for (const file of files) { + const content = readCss({ file }); + const softRules = extractRules({ content, classPattern: /\.fri-[a-z]+--[a-z-]+-soft\s*\{/ }); + + for (const rule of softRules) { + const classMatch = rule.match(/\.fri-[a-z-]+/); + const className = classMatch ? classMatch[0] : "unknown"; + const hasBorder = /@apply[^;]*\bborder-/.test(rule); + + if (hasBorder && !rule.includes("-soft-border")) { + errors.push(`${file} ${className}: soft variant should use -soft-border token`); + } + } + } + + expect(errors).toEqual([]); + }); + + it("ghost variants must not apply visible border", () => { + const files = fs.readdirSync(stylesComponentsDir).filter((file) => file.endsWith(".css")); + const errors: string[] = []; + + for (const file of files) { + const content = readCss({ file }); + const ghostRules = extractRules({ content, classPattern: /\.fri-[a-z]+--[a-z-]+-ghost\s*\{/ }); + + for (const rule of ghostRules) { + const classMatch = rule.match(/\.fri-[a-z-]+/); + const className = classMatch ? classMatch[0] : "unknown"; + + if (/@apply[^;]*\bborder-[a-z]+-outline-border/.test(rule)) { + errors.push(`${file} ${className}: ghost variant should not apply colored border`); + } + } + } + + expect(errors).toEqual([]); + }); +}); diff --git a/packages/styles/src/components/badge.css b/packages/styles/src/components/badge.css index 76651a8..b0a29e3 100644 --- a/packages/styles/src/components/badge.css +++ b/packages/styles/src/components/badge.css @@ -15,29 +15,23 @@ .fri-badge--primary { --fri-badge-bg: var(--color-primary); --fri-badge-fg: var(--color-primary-foreground); - - @apply border border-primary-border; } .fri-badge--secondary { --fri-badge-bg: var(--color-secondary); --fri-badge-fg: var(--color-secondary-foreground); - - @apply border border-secondary-border; } .fri-badge--accent { --fri-badge-bg: var(--color-accent); --fri-badge-fg: var(--color-accent-foreground); - - @apply border border-accent-border; } .fri-badge--outline { --fri-badge-bg: transparent; --fri-badge-fg: var(--color-primary); - @apply border border-primary-border; + @apply border border-primary-outline-border; } .fri-badge--glass { @@ -64,29 +58,21 @@ .fri-badge--success { --fri-badge-bg: var(--color-success); --fri-badge-fg: var(--color-success-foreground); - - @apply border border-success-border; } .fri-badge--warning { --fri-badge-bg: var(--color-warning); --fri-badge-fg: var(--color-warning-foreground); - - @apply border border-warning-border; } .fri-badge--danger { --fri-badge-bg: var(--color-danger); --fri-badge-fg: var(--color-danger-foreground); - - @apply border border-danger-border; } .fri-badge--info { --fri-badge-bg: var(--color-info); --fri-badge-fg: var(--color-info-foreground); - - @apply border border-info-border; } .fri-badge--primary-soft { diff --git a/packages/styles/src/components/button.css b/packages/styles/src/components/button.css index 997f509..2c992a2 100644 --- a/packages/styles/src/components/button.css +++ b/packages/styles/src/components/button.css @@ -45,8 +45,6 @@ --fri-button-bg-hover: var(--color-primary-hover); --fri-button-bg-pressed: var(--color-primary-active); --fri-button-fg: var(--color-primary-foreground); - - @apply border border-primary-border; } .fri-button--secondary { @@ -54,8 +52,6 @@ --fri-button-bg-hover: var(--color-secondary-hover); --fri-button-bg-pressed: var(--color-secondary-active); --fri-button-fg: var(--color-secondary-foreground); - - @apply border border-secondary-border; } .fri-button--accent { @@ -63,8 +59,6 @@ --fri-button-bg-hover: var(--color-accent-hover); --fri-button-bg-pressed: var(--color-accent-active); --fri-button-fg: var(--color-accent-foreground); - - @apply border border-accent-border; } .fri-button--ghost { @@ -115,8 +109,6 @@ --fri-button-bg-hover: var(--color-success-hover); --fri-button-bg-pressed: var(--color-success-active); --fri-button-fg: var(--color-success-foreground); - - @apply border border-success-border; } .fri-button--warning { @@ -124,8 +116,6 @@ --fri-button-bg-hover: var(--color-warning-hover); --fri-button-bg-pressed: var(--color-warning-active); --fri-button-fg: var(--color-warning-foreground); - - @apply border border-warning-border; } .fri-button--danger { @@ -133,8 +123,6 @@ --fri-button-bg-hover: var(--color-danger-hover); --fri-button-bg-pressed: var(--color-danger-active); --fri-button-fg: var(--color-danger-foreground); - - @apply border border-danger-border; } .fri-button--info { @@ -142,8 +130,6 @@ --fri-button-bg-hover: var(--color-info-hover); --fri-button-bg-pressed: var(--color-info-active); --fri-button-fg: var(--color-info-foreground); - - @apply border border-info-border; } .fri-button--primary-soft { --fri-button-bg: var(--color-primary-soft); From 343ac7ed544ca6f7c7b8ae91f3347a5659aefb5f Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Sun, 19 Apr 2026 08:54:07 +0700 Subject: [PATCH 071/199] refactor(react): Rename imports from headless component --- .../react/src/components/avatar/Avatar.tsx | 18 +++++++++--------- .../react/src/components/button/Button.tsx | 8 ++++---- .../interactive-component/Component.tsx.hbs | 8 ++++---- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/react/src/components/avatar/Avatar.tsx b/packages/react/src/components/avatar/Avatar.tsx index 3d62a52..d952eb7 100644 --- a/packages/react/src/components/avatar/Avatar.tsx +++ b/packages/react/src/components/avatar/Avatar.tsx @@ -2,7 +2,7 @@ import type { ComponentPropsWithRef } from "react"; -import * as AvatarPrimitive from "@radix-ui/react-avatar"; +import * as RadixAvatar from "@radix-ui/react-avatar"; import { clsx } from "clsx"; @@ -10,16 +10,16 @@ import { bem } from "../../utils/bem"; import type { AvatarVariantsProps } from "./avatar.variants"; -export type AvatarProps = Omit, "className"> & +export type AvatarProps = Omit, "className"> & AvatarVariantsProps & { className?: string; }; -export type AvatarImageProps = Omit, "className"> & { +export type AvatarImageProps = Omit, "className"> & { className?: string; }; -export type AvatarFallbackProps = Omit, "className"> & { +export type AvatarFallbackProps = Omit, "className"> & { className?: string; }; @@ -27,7 +27,7 @@ const AvatarImage = (props: Readonly) => { const { className, ref, ...rest } = props; const imageClassName = clsx("fri-avatar__image", className); - return ; + return ; }; AvatarImage.displayName = "Avatar.Image"; @@ -37,9 +37,9 @@ const AvatarFallback = (props: Readonly) => { const fallbackClassName = clsx("fri-avatar__fallback", className); return ( - + {children} - + ); }; @@ -62,9 +62,9 @@ const Avatar = (props: Readonly) => { ); return ( - + {children} - + ); }; diff --git a/packages/react/src/components/button/Button.tsx b/packages/react/src/components/button/Button.tsx index a0daf00..bb3c171 100644 --- a/packages/react/src/components/button/Button.tsx +++ b/packages/react/src/components/button/Button.tsx @@ -2,7 +2,7 @@ import type { ComponentPropsWithRef, ReactNode } from "react"; -import { Button as RacButton, composeRenderProps } from "react-aria-components"; +import { Button as AriaButton, composeRenderProps } from "react-aria-components"; import type { ButtonRenderProps } from "react-aria-components"; import { clsx } from "clsx"; @@ -15,7 +15,7 @@ import type { ButtonVariantsProps } from "./button.variants"; export type ButtonProps = ButtonVariantsProps & { children?: ReactNode | ((renderProps: ButtonRenderProps) => ReactNode); className?: string | ((renderProps: ButtonRenderProps) => string); -} & Omit, "className" | "children">; +} & Omit, "className" | "children">; const Button = (props: Readonly) => { const { children, className, isFullWidth, isIconOnly, radius, ref, size, variant, ...rest } = props; @@ -36,14 +36,14 @@ const Button = (props: Readonly) => { const buttonClassName = composeRenderProps(className, (consumerClassName) => clsx(baseClassName, consumerClassName)); return ( - + {composeRenderProps(children, (child, { isPending }) => ( <> {child} {isPending && + ); }; diff --git a/turbo/generators/interactive-component/Component.tsx.hbs b/turbo/generators/interactive-component/Component.tsx.hbs index 83369ec..cd21198 100644 --- a/turbo/generators/interactive-component/Component.tsx.hbs +++ b/turbo/generators/interactive-component/Component.tsx.hbs @@ -2,7 +2,7 @@ import type { ComponentPropsWithRef, ReactNode } from "react"; -import { Button as RacComponent, composeRenderProps } from "react-aria-components"; +import { Button as AriaComponent, composeRenderProps } from "react-aria-components"; import type { ButtonRenderProps } from "react-aria-components"; import { clsx } from "clsx"; @@ -14,7 +14,7 @@ import type { {{pascalCase name}}VariantsProps } from "./{{dashCase name}}.varia export type {{pascalCase name}}Props = {{pascalCase name}}VariantsProps & { children?: ReactNode | ((renderProps: ButtonRenderProps) => ReactNode); className?: string | ((renderProps: ButtonRenderProps) => string); -} & Omit, "className" | "children">; +} & Omit, "className" | "children">; const {{pascalCase name}} = (props: Readonly<{{pascalCase name}}Props>) => { const { children, className, ref, size, variant, ...rest } = props; @@ -31,9 +31,9 @@ const {{pascalCase name}} = (props: Readonly<{{pascalCase name}}Props>) => { const componentClassName = composeRenderProps(className, (consumerClassName) => clsx(baseClassName, consumerClassName)); return ( - + {children} - + ); }; From 4cf390476ed4573500434b5e5d6f92afe0e0e3de Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Sun, 19 Apr 2026 09:37:29 +0700 Subject: [PATCH 072/199] feat(react): Imprement Label component --- packages/react/package.json | 4 + packages/react/src/components/label/Label.tsx | 51 ++++ packages/react/src/components/label/index.ts | 3 + .../src/components/label/label.stories.tsx | 266 ++++++++++++++++++ .../react/src/components/label/label.test.tsx | 102 +++++++ .../src/components/label/label.variants.ts | 8 + packages/react/tsup.config.ts | 1 + packages/styles/src/components/index.css | 2 + packages/styles/src/components/label.css | 48 ++++ 9 files changed, 485 insertions(+) create mode 100644 packages/react/src/components/label/Label.tsx create mode 100644 packages/react/src/components/label/index.ts create mode 100644 packages/react/src/components/label/label.stories.tsx create mode 100644 packages/react/src/components/label/label.test.tsx create mode 100644 packages/react/src/components/label/label.variants.ts create mode 100644 packages/styles/src/components/label.css diff --git a/packages/react/package.json b/packages/react/package.json index 24cb6ab..7f14d4b 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -43,6 +43,10 @@ "types": "./dist/components/icons/index.d.ts", "import": "./dist/components/icons/index.mjs" }, + "./label": { + "types": "./dist/components/label/index.d.ts", + "import": "./dist/components/label/index.mjs" + }, "./surface": { "types": "./dist/components/surface/index.d.ts", "import": "./dist/components/surface/index.mjs" diff --git a/packages/react/src/components/label/Label.tsx b/packages/react/src/components/label/Label.tsx new file mode 100644 index 0000000..9803337 --- /dev/null +++ b/packages/react/src/components/label/Label.tsx @@ -0,0 +1,51 @@ +"use client"; + +import type { ComponentPropsWithRef } from "react"; + +import { Label as AriaLabel } from "react-aria-components"; + +import { clsx } from "clsx"; + +import { bem } from "../../utils/bem"; + +import type { LabelVariantsProps } from "./label.variants"; + +export type LabelProps = LabelVariantsProps & { + className?: string; +} & Omit, "className">; + +const Label = (props: Readonly) => { + const { children, className, isDisabled, isInvalid, isRequired, optionalMessage, ref, size, weight, ...rest } = props; + + const labelClassName = clsx( + bem({ + block: "fri-label", + modifiers: { + size, + weight, + required: isRequired, + invalid: isInvalid, + disabled: isDisabled, + }, + }), + className, + ); + + return ( + + {children} + + {optionalMessage && {optionalMessage}} + + {isRequired && ( + + )} + + ); +}; + +Label.displayName = "Label"; + +export { Label }; diff --git a/packages/react/src/components/label/index.ts b/packages/react/src/components/label/index.ts new file mode 100644 index 0000000..d87af99 --- /dev/null +++ b/packages/react/src/components/label/index.ts @@ -0,0 +1,3 @@ +export { Label } from "./Label"; +export type { LabelProps } from "./Label"; +export type { LabelVariantsProps } from "./label.variants"; diff --git a/packages/react/src/components/label/label.stories.tsx b/packages/react/src/components/label/label.stories.tsx new file mode 100644 index 0000000..eef052f --- /dev/null +++ b/packages/react/src/components/label/label.stories.tsx @@ -0,0 +1,266 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import { Label } from "./Label"; + +const meta = { + title: "Components/Label", + component: Label, + tags: ["autodocs"], + parameters: { + layout: "centered", + }, + args: { + children: "Email", + size: "md", + weight: "medium", + isRequired: false, + isInvalid: false, + isDisabled: false, + }, + argTypes: { + children: { + control: "text", + description: "Label content", + table: { + type: { summary: "ReactNode" }, + category: "Children", + }, + }, + size: { + control: "select", + options: ["sm", "md", "lg"], + description: + "Text size of the label. **sm** (text-xs) — dense forms or secondary labels. **md** (text-sm, default) — standard form fields. **lg** (text-base) — emphasized or large form sections.", + table: { + type: { summary: '"sm" | "md" | "lg"' }, + defaultValue: { summary: "md" }, + category: "Style Variants", + }, + }, + weight: { + control: "select", + options: ["normal", "medium", "semibold"], + description: + "Font weight. **normal** (400) — subtle labels. **medium** (500, default) — standard emphasis. **semibold** (600) — strong emphasis for section headings.", + table: { + type: { summary: '"normal" | "medium" | "semibold"' }, + defaultValue: { summary: "medium" }, + category: "Style Variants", + }, + }, + isRequired: { + control: "boolean", + description: "Shows red asterisk to indicate required field", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "false" }, + category: "Style Variants", + }, + }, + isInvalid: { + control: "boolean", + description: "Error state — label text in danger color", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "false" }, + category: "Style Variants", + }, + }, + isDisabled: { + control: "boolean", + description: "Dims the label and disables pointer events", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "false" }, + category: "Style Variants", + }, + }, + htmlFor: { + control: "text", + description: "ID of the form field this label is associated with", + table: { + type: { summary: "string" }, + category: "Content", + }, + }, + optionalMessage: { + control: "text", + description: + "Optional text shown in muted gray when field is NOT required (e.g. '(Optional)'). Ignored when `isRequired` is true.", + table: { + type: { summary: "string" }, + category: "Style Variants", + }, + }, + className: { + control: "text", + description: "Additional CSS classes", + table: { + type: { summary: "string" }, + category: "Styling", + }, + }, + }, +} satisfies Meta; + +type Story = StoryObj; + +const Default: Story = { + parameters: { + docs: { + source: { + code: `import { Label } from "@fried-ui/react"; + +const Basic = () => { + return ; +};`, + }, + }, + }, +}; + +const Required: Story = { + parameters: { + docs: { + source: { + code: `import { Label } from "@fried-ui/react"; + +const Required = () => { + return ; +};`, + }, + }, + }, + args: { + children: "Password", + isRequired: true, + }, +}; + +const Optional: Story = { + parameters: { + docs: { + source: { + code: `import { Label } from "@fried-ui/react"; + +const Optional = () => { + return ; +};`, + }, + }, + }, + args: { + children: "Phone number", + optionalMessage: "(Optional)", + }, +}; + +const Sizes: Story = { + parameters: { + docs: { + source: { + code: `import { Label } from "@fried-ui/react"; + +const Sizes = () => { + return ( +
+ + + +
+ ); +};`, + }, + }, + }, + render: (args): React.JSX.Element => ( +
+ + + + + +
+ ), +}; + +const Weights: Story = { + parameters: { + docs: { + source: { + code: `import { Label } from "@fried-ui/react"; + +const Weights = () => { + return ( +
+ + + +
+ ); +};`, + }, + }, + }, + render: (args): React.JSX.Element => ( +
+ + + + + +
+ ), +}; + +const Invalid: Story = { + parameters: { + docs: { + source: { + code: `import { Label } from "@fried-ui/react"; + +const Invalid = () => { + return ; +};`, + }, + }, + }, + args: { + children: "Email (Invalid Format)", + isInvalid: true, + }, +}; + +const Disabled: Story = { + parameters: { + docs: { + source: { + code: `import { Label } from "@fried-ui/react"; + +const Disabled = () => { + return ; +};`, + }, + }, + }, + args: { + children: "Username", + isDisabled: true, + }, +}; + +export { Default, Required, Optional, Invalid, Disabled, Sizes, Weights }; + +export default meta; diff --git a/packages/react/src/components/label/label.test.tsx b/packages/react/src/components/label/label.test.tsx new file mode 100644 index 0000000..a5841ce --- /dev/null +++ b/packages/react/src/components/label/label.test.tsx @@ -0,0 +1,102 @@ +import { createRef } from "react"; + +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; + +import { Label } from "./Label"; + +describe("Label", () => { + it("renders with default props", () => { + render(); + expect(screen.getByText("Email")).toBeInTheDocument(); + }); + + it("renders base class without explicit modifiers", () => { + render(); + const el = screen.getByText("Default"); + expect(el.className).toBe("fri-label"); + }); + + it("applies all size classes", () => { + const sizes = ["sm", "md", "lg"] as const; + + sizes.forEach((size) => { + const { unmount } = render(); + expect(screen.getByText(size).className).toContain(`fri-label--size-${size}`); + unmount(); + }); + }); + + it("applies all weight classes", () => { + const weights = ["normal", "medium", "semibold"] as const; + + weights.forEach((weight) => { + const { unmount } = render(); + expect(screen.getByText(weight).className).toContain(`fri-label--weight-${weight}`); + unmount(); + }); + }); + + it("renders required asterisk when isRequired is true", () => { + render(); + const asterisk = screen.getByText("*"); + expect(asterisk).toBeInTheDocument(); + expect(asterisk.className).toContain("fri-label__required"); + }); + + it("does not render asterisk when isRequired is false", () => { + render(); + expect(screen.queryByText("*")).not.toBeInTheDocument(); + }); + + it("applies required modifier class", () => { + const { container } = render(); + const label = container.querySelector("[data-slot='label']"); + expect(label?.className).toContain("fri-label--required"); + }); + + it("renders optional text when provided", () => { + render(); + const optional = screen.getByText("(Optional)"); + expect(optional).toBeInTheDocument(); + expect(optional.className).toContain("fri-label__optional"); + }); + + it("applies invalid modifier class", () => { + const { container } = render(); + const label = container.querySelector("[data-slot='label']"); + expect(label?.className).toContain("fri-label--invalid"); + }); + + it("applies disabled modifier class", () => { + const { container } = render(); + const label = container.querySelector("[data-slot='label']"); + expect(label?.className).toContain("fri-label--disabled"); + }); + + it("passes htmlFor attribute", () => { + const { container } = render(); + const label = container.querySelector("[data-slot='label']"); + expect(label).toHaveAttribute("for", "email-input"); + }); + + it("merges custom className", () => { + render(); + expect(screen.getByText("Styled").className).toContain("mt-4"); + }); + + it("forwards ref", () => { + const ref = createRef(); + render(); + expect(ref.current).toBeInstanceOf(HTMLLabelElement); + }); + + it("sets data-slot attribute", () => { + const { container } = render(); + expect(container.querySelector("[data-slot='label']")).toBeInTheDocument(); + }); + + it("sets displayName", () => { + expect(Label.displayName).toBe("Label"); + }); +}); diff --git a/packages/react/src/components/label/label.variants.ts b/packages/react/src/components/label/label.variants.ts new file mode 100644 index 0000000..e5c043e --- /dev/null +++ b/packages/react/src/components/label/label.variants.ts @@ -0,0 +1,8 @@ +export interface LabelVariantsProps { + isDisabled?: boolean; + isInvalid?: boolean; + isRequired?: boolean; + optionalMessage?: string; + size?: "sm" | "md" | "lg"; + weight?: "normal" | "medium" | "semibold"; +} diff --git a/packages/react/tsup.config.ts b/packages/react/tsup.config.ts index ab91953..f127d3b 100644 --- a/packages/react/tsup.config.ts +++ b/packages/react/tsup.config.ts @@ -7,6 +7,7 @@ export default defineConfig({ "components/badge/index": "src/components/badge/index.ts", "components/button/index": "src/components/button/index.ts", "components/icons/index": "src/components/icons/index.ts", + "components/label/index": "src/components/label/index.ts", "components/surface/index": "src/components/surface/index.ts", }, format: ["esm"], diff --git a/packages/styles/src/components/index.css b/packages/styles/src/components/index.css index acfe99f..88ae96b 100644 --- a/packages/styles/src/components/index.css +++ b/packages/styles/src/components/index.css @@ -4,3 +4,5 @@ @import "./surface.css"; @import "./avatar.css"; + +@import "./label.css"; diff --git a/packages/styles/src/components/label.css b/packages/styles/src/components/label.css new file mode 100644 index 0000000..2aca843 --- /dev/null +++ b/packages/styles/src/components/label.css @@ -0,0 +1,48 @@ +@layer components { + .fri-label { + @apply inline-flex items-center gap-1 select-none; + @apply text-sm leading-none font-medium; + @apply text-(--color-foreground); + } + + .fri-label__required { + @apply text-(--color-danger); + } + + .fri-label__optional { + @apply font-normal text-(--color-muted-foreground); + } + + .fri-label--size-sm { + @apply text-xs; + } + + .fri-label--size-md { + @apply text-sm; + } + + .fri-label--size-lg { + @apply text-base; + } + + .fri-label--weight-normal { + @apply font-normal; + } + + .fri-label--weight-medium { + @apply font-medium; + } + + .fri-label--weight-semibold { + @apply font-semibold; + } + + .fri-label--invalid { + @apply text-(--color-danger); + } + + .fri-label--disabled, + .fri-label[data-disabled] { + @apply pointer-events-none opacity-50; + } +} From 3302c328ace1b9666b5c489831b88efb84939c0d Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Sun, 19 Apr 2026 09:38:11 +0700 Subject: [PATCH 073/199] feat(react): Export Label component from index --- packages/react/src/components/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/react/src/components/index.ts b/packages/react/src/components/index.ts index d1d0570..2b311aa 100644 --- a/packages/react/src/components/index.ts +++ b/packages/react/src/components/index.ts @@ -7,3 +7,5 @@ export * from "./icons"; export * from "./surface"; export * from "./avatar"; + +export * from "./label"; From 34358454897605ab615405a1235d325e5a4c78d4 Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Sun, 19 Apr 2026 09:38:52 +0700 Subject: [PATCH 074/199] feat(react): Remove CustomStyle stories --- .../src/components/avatar/avatar.stories.tsx | 82 +------------------ .../src/components/badge/badge.stories.tsx | 24 ------ .../src/components/button/button.stories.tsx | 24 ------ .../components/surface/surface.stories.tsx | 26 ------ 4 files changed, 1 insertion(+), 155 deletions(-) diff --git a/packages/react/src/components/avatar/avatar.stories.tsx b/packages/react/src/components/avatar/avatar.stories.tsx index 0eb1d15..db84b87 100644 --- a/packages/react/src/components/avatar/avatar.stories.tsx +++ b/packages/react/src/components/avatar/avatar.stories.tsx @@ -452,86 +452,6 @@ const BrokenImage = () => { ), }; -const CUSTOM_SRC = - "https://images.unsplash.com/photo-1559828801-04565cd31e27?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTF8fGN5YmVycHVuayUyMGdpcmx8ZW58MHx8MHx8fDA%3D"; - -const CustomStyle: Story = { - parameters: { - docs: { - source: { - code: `import { Avatar } from "@fried-ui/react"; - -const CustomStyle = () => { - return ( -
- - - 4XL - - -
- - - ON - - - -
- -
- - - ON - - - -
- - - - CG - - -
- ); -};`, - }, - }, - }, - render: (args): React.JSX.Element => ( -
- - - 4XL - - -
- - - ON - - - -
- -
- - - ON - - - -
- - - - CG - - -
- ), -}; - -export { Default, WithFallback, Sizes, Radius, Ring, Disabled, BrokenImage, CustomStyle }; +export { Default, WithFallback, Sizes, Radius, Ring, Disabled, BrokenImage }; export default meta; diff --git a/packages/react/src/components/badge/badge.stories.tsx b/packages/react/src/components/badge/badge.stories.tsx index 1915630..3ab382a 100644 --- a/packages/react/src/components/badge/badge.stories.tsx +++ b/packages/react/src/components/badge/badge.stories.tsx @@ -707,29 +707,6 @@ const IconOnly = () => { ), }; -const CustomStyle: Story = { - parameters: { - docs: { - source: { - code: `import { Badge } from "@fried-ui/react"; - -const CustomStyle = () => { - return ( - - Gradient - - ); -};`, - }, - }, - }, - render: (args): React.JSX.Element => ( - - Gradient - - ), -}; - export { Default, Variants, @@ -743,7 +720,6 @@ export { Radius, WithIcon, IconOnly, - CustomStyle, }; export default meta; diff --git a/packages/react/src/components/button/button.stories.tsx b/packages/react/src/components/button/button.stories.tsx index 4847d17..1996a6e 100644 --- a/packages/react/src/components/button/button.stories.tsx +++ b/packages/react/src/components/button/button.stories.tsx @@ -882,29 +882,6 @@ const OverlayVariants = () => { ), }; -const CustomStyle: Story = { - parameters: { - docs: { - source: { - code: `import { Button } from "@fried-ui/react"; - -const CustomStyle = () => { - return ( - - ); -};`, - }, - }, - }, - render: (args): React.JSX.Element => ( - - ), -}; - export { Default, Variants, @@ -923,7 +900,6 @@ export { Disabled, Pending, RenderProps, - CustomStyle, }; export default meta; diff --git a/packages/react/src/components/surface/surface.stories.tsx b/packages/react/src/components/surface/surface.stories.tsx index 57a8fd6..34c680f 100644 --- a/packages/react/src/components/surface/surface.stories.tsx +++ b/packages/react/src/components/surface/surface.stories.tsx @@ -485,31 +485,6 @@ const Shadow = () => { ), }; -const CustomStyle: Story = { - parameters: { - docs: { - source: { - code: `import { Surface } from "@fried-ui/react"; - -const CustomStyle = () => { - return ( - -

Gradient Card

-

Pastel gradient via className.

-
- ); -};`, - }, - }, - }, - render: (args): React.JSX.Element => ( - -

Gradient Card

-

Pastel gradient via className.

-
- ), -}; - export { Default, Variants, @@ -520,7 +495,6 @@ export { OverlayVariants, Radius, Shadow, - CustomStyle, }; export default meta; From 49d6a8d7110c0933fe035b4c0ac35cf6283f1d3e Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Sun, 19 Apr 2026 09:56:59 +0700 Subject: [PATCH 075/199] feat(react): Imprement Description component --- packages/react/package.json | 4 + .../components/description/Description.tsx | 41 +++++ .../description/description.stories.tsx | 156 ++++++++++++++++++ .../description/description.test.tsx | 69 ++++++++ .../description/description.variants.ts | 5 + .../react/src/components/description/index.ts | 3 + packages/react/tsup.config.ts | 1 + .../styles/src/components/description.css | 27 +++ packages/styles/src/components/index.css | 2 + packages/styles/src/components/label.css | 2 +- 10 files changed, 309 insertions(+), 1 deletion(-) create mode 100644 packages/react/src/components/description/Description.tsx create mode 100644 packages/react/src/components/description/description.stories.tsx create mode 100644 packages/react/src/components/description/description.test.tsx create mode 100644 packages/react/src/components/description/description.variants.ts create mode 100644 packages/react/src/components/description/index.ts create mode 100644 packages/styles/src/components/description.css diff --git a/packages/react/package.json b/packages/react/package.json index 7f14d4b..6232938 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -39,6 +39,10 @@ "types": "./dist/components/button/index.d.ts", "import": "./dist/components/button/index.mjs" }, + "./description": { + "types": "./dist/components/description/index.d.ts", + "import": "./dist/components/description/index.mjs" + }, "./icons": { "types": "./dist/components/icons/index.d.ts", "import": "./dist/components/icons/index.mjs" diff --git a/packages/react/src/components/description/Description.tsx b/packages/react/src/components/description/Description.tsx new file mode 100644 index 0000000..2d34128 --- /dev/null +++ b/packages/react/src/components/description/Description.tsx @@ -0,0 +1,41 @@ +"use client"; + +import type { ComponentPropsWithRef } from "react"; + +import { Text as AriaText } from "react-aria-components"; + +import { clsx } from "clsx"; + +import { bem } from "../../utils/bem"; + +import type { DescriptionVariantsProps } from "./description.variants"; + +export type DescriptionProps = DescriptionVariantsProps & { + className?: string; +} & Omit, "className" | "slot">; + +const Description = (props: Readonly) => { + const { children, className, isDisabled, isInvalid, ref, size, ...rest } = props; + + const descriptionClassName = clsx( + bem({ + block: "fri-description", + modifiers: { + size, + invalid: isInvalid, + disabled: isDisabled, + }, + }), + className, + ); + + return ( + + {children} + + ); +}; + +Description.displayName = "Description"; + +export { Description }; diff --git a/packages/react/src/components/description/description.stories.tsx b/packages/react/src/components/description/description.stories.tsx new file mode 100644 index 0000000..76efdbc --- /dev/null +++ b/packages/react/src/components/description/description.stories.tsx @@ -0,0 +1,156 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import { Description } from "./Description"; + +const meta = { + title: "Components/Description", + component: Description, + tags: ["autodocs"], + parameters: { + layout: "centered", + }, + args: { + children: "We'll never share your email with anyone else.", + size: "md", + isInvalid: false, + isDisabled: false, + }, + argTypes: { + children: { + control: "text", + description: "Description content", + table: { + type: { summary: "ReactNode" }, + category: "Children", + }, + }, + size: { + control: "select", + options: ["sm", "md", "lg"], + description: + "Text size of the description. **sm** (text-xs) — dense forms or secondary helpers. **md** (text-sm, default) — standard form fields. **lg** (text-base) — emphasized or large form sections.", + table: { + type: { summary: '"sm" | "md" | "lg"' }, + defaultValue: { summary: "md" }, + category: "Style Variants", + }, + }, + isInvalid: { + control: "boolean", + description: "Error state — description text in danger color (use for error messages)", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "false" }, + category: "Style Variants", + }, + }, + isDisabled: { + control: "boolean", + description: "Dims the description and disables pointer events", + table: { + type: { summary: "boolean" }, + defaultValue: { summary: "false" }, + category: "Style Variants", + }, + }, + className: { + control: "text", + description: "Additional CSS classes", + table: { + type: { summary: "string" }, + category: "Styling", + }, + }, + }, +} satisfies Meta; + +type Story = StoryObj; + +const Default: Story = { + parameters: { + docs: { + source: { + code: `import { Description } from "@fried-ui/react"; + +const Basic = () => { + return We'll never share your email with anyone else.; +};`, + }, + }, + }, +}; + +const Sizes: Story = { + parameters: { + docs: { + source: { + code: `import { Description } from "@fried-ui/react"; + +const Sizes = () => { + return ( +
+ Small helper text + Medium helper text + Large helper text +
+ ); +};`, + }, + }, + }, + render: (args): React.JSX.Element => ( +
+ + Small helper text + + + + Medium helper text + + + + Large helper text + +
+ ), +}; + +const Invalid: Story = { + parameters: { + docs: { + source: { + code: `import { Description } from "@fried-ui/react"; + +const Invalid = () => { + return Email format is invalid; +};`, + }, + }, + }, + args: { + children: "Email format is invalid", + isInvalid: true, + }, +}; + +const Disabled: Story = { + parameters: { + docs: { + source: { + code: `import { Description } from "@fried-ui/react"; + +const Disabled = () => { + return This field is currently unavailable; +};`, + }, + }, + }, + args: { + children: "This field is currently unavailable", + isDisabled: true, + }, +}; + +export { Default, Sizes, Invalid, Disabled }; + +export default meta; diff --git a/packages/react/src/components/description/description.test.tsx b/packages/react/src/components/description/description.test.tsx new file mode 100644 index 0000000..11ca9f2 --- /dev/null +++ b/packages/react/src/components/description/description.test.tsx @@ -0,0 +1,69 @@ +import { createRef } from "react"; + +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; + +import { Description } from "./Description"; + +describe("Description", () => { + it("renders with default props", () => { + render(Help text); + expect(screen.getByText("Help text")).toBeInTheDocument(); + }); + + it("renders base class without explicit modifiers", () => { + render(Default); + const el = screen.getByText("Default"); + expect(el.className).toBe("fri-description"); + }); + + it("applies all size classes", () => { + const sizes = ["sm", "md", "lg"] as const; + + sizes.forEach((size) => { + const { unmount } = render({size}); + expect(screen.getByText(size).className).toContain(`fri-description--size-${size}`); + unmount(); + }); + }); + + it("applies invalid modifier class", () => { + render(Error); + expect(screen.getByText("Error").className).toContain("fri-description--invalid"); + }); + + it("applies disabled modifier class", () => { + render(Disabled); + expect(screen.getByText("Disabled").className).toContain("fri-description--disabled"); + }); + + it("merges custom className", () => { + render(Styled); + expect(screen.getByText("Styled").className).toContain("mt-4"); + }); + + it("forwards ref", () => { + const ref = createRef(); + render(Ref); + expect(ref.current).toBeInstanceOf(HTMLElement); + }); + + it("sets data-slot attribute", () => { + render(Slot); + expect(screen.getByText("Slot")).toHaveAttribute("data-slot", "description"); + }); + + it("sets slot=description attribute", () => { + render(Slot); + expect(screen.getByText("Slot")).toHaveAttribute("slot", "description"); + }); + + it("passes through native HTML attributes", () => { + render(Props); + expect(screen.getByTestId("custom")).toBeInTheDocument(); + }); + + it("sets displayName", () => { + expect(Description.displayName).toBe("Description"); + }); +}); diff --git a/packages/react/src/components/description/description.variants.ts b/packages/react/src/components/description/description.variants.ts new file mode 100644 index 0000000..02f1b82 --- /dev/null +++ b/packages/react/src/components/description/description.variants.ts @@ -0,0 +1,5 @@ +export interface DescriptionVariantsProps { + isDisabled?: boolean; + isInvalid?: boolean; + size?: "sm" | "md" | "lg"; +} diff --git a/packages/react/src/components/description/index.ts b/packages/react/src/components/description/index.ts new file mode 100644 index 0000000..59da406 --- /dev/null +++ b/packages/react/src/components/description/index.ts @@ -0,0 +1,3 @@ +export { Description } from "./Description"; +export type { DescriptionProps } from "./Description"; +export type { DescriptionVariantsProps } from "./description.variants"; diff --git a/packages/react/tsup.config.ts b/packages/react/tsup.config.ts index f127d3b..ebfdb87 100644 --- a/packages/react/tsup.config.ts +++ b/packages/react/tsup.config.ts @@ -6,6 +6,7 @@ export default defineConfig({ "components/avatar/index": "src/components/avatar/index.ts", "components/badge/index": "src/components/badge/index.ts", "components/button/index": "src/components/button/index.ts", + "components/description/index": "src/components/description/index.ts", "components/icons/index": "src/components/icons/index.ts", "components/label/index": "src/components/label/index.ts", "components/surface/index": "src/components/surface/index.ts", diff --git a/packages/styles/src/components/description.css b/packages/styles/src/components/description.css new file mode 100644 index 0000000..c8e0df0 --- /dev/null +++ b/packages/styles/src/components/description.css @@ -0,0 +1,27 @@ +@layer components { + .fri-description { + @apply block; + @apply text-sm text-(--color-muted-foreground); + } + + .fri-description--size-sm { + @apply text-xs; + } + + .fri-description--size-md { + @apply text-sm; + } + + .fri-description--size-lg { + @apply text-base; + } + + .fri-description--invalid { + @apply text-(--color-danger); + } + + .fri-description--disabled, + .fri-description[data-disabled] { + @apply pointer-events-none opacity-50; + } +} diff --git a/packages/styles/src/components/index.css b/packages/styles/src/components/index.css index 88ae96b..377129c 100644 --- a/packages/styles/src/components/index.css +++ b/packages/styles/src/components/index.css @@ -6,3 +6,5 @@ @import "./avatar.css"; @import "./label.css"; + +@import "./description.css"; diff --git a/packages/styles/src/components/label.css b/packages/styles/src/components/label.css index 2aca843..912ef3b 100644 --- a/packages/styles/src/components/label.css +++ b/packages/styles/src/components/label.css @@ -1,7 +1,7 @@ @layer components { .fri-label { @apply inline-flex items-center gap-1 select-none; - @apply text-sm leading-none font-medium; + @apply text-sm font-medium; @apply text-(--color-foreground); } From 12a5d743ae4383e4eb59a74d7f465a0f33f5f330 Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Sun, 19 Apr 2026 09:57:27 +0700 Subject: [PATCH 076/199] feat(react): Export Description component from index --- packages/react/src/components/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/react/src/components/index.ts b/packages/react/src/components/index.ts index 2b311aa..14bf97e 100644 --- a/packages/react/src/components/index.ts +++ b/packages/react/src/components/index.ts @@ -9,3 +9,5 @@ export * from "./surface"; export * from "./avatar"; export * from "./label"; + +export * from "./description"; From f5e36b18315ed0878f740cfe54dfc33c74013f0b Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Sun, 19 Apr 2026 09:57:57 +0700 Subject: [PATCH 077/199] feat(react): Update Label component stories --- packages/react/src/components/label/label.stories.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/react/src/components/label/label.stories.tsx b/packages/react/src/components/label/label.stories.tsx index eef052f..b09fa80 100644 --- a/packages/react/src/components/label/label.stories.tsx +++ b/packages/react/src/components/label/label.stories.tsx @@ -163,7 +163,7 @@ const Sizes: Story = { const Sizes = () => { return ( -
+
@@ -174,7 +174,7 @@ const Sizes = () => { }, }, render: (args): React.JSX.Element => ( -
+
@@ -198,7 +198,7 @@ const Weights: Story = { const Weights = () => { return ( -
+
@@ -209,7 +209,7 @@ const Weights = () => { }, }, render: (args): React.JSX.Element => ( -
+
From bb105530009c55ef20c679184425f1b745b42354 Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Sun, 19 Apr 2026 10:11:03 +0700 Subject: [PATCH 078/199] feat(react): Update stories with consistent text --- .../src/components/button/button.stories.tsx | 32 +++---- .../description/description.stories.tsx | 24 ++--- .../src/components/label/label.stories.tsx | 32 +++---- .../components/surface/surface.stories.tsx | 92 +++++++++---------- 4 files changed, 90 insertions(+), 90 deletions(-) diff --git a/packages/react/src/components/button/button.stories.tsx b/packages/react/src/components/button/button.stories.tsx index 1996a6e..af2dd7e 100644 --- a/packages/react/src/components/button/button.stories.tsx +++ b/packages/react/src/components/button/button.stories.tsx @@ -733,13 +733,13 @@ const Pending: Story = { code: `import { Button } from "@fried-ui/react"; const Pending = () => { - return ; + return ; };`, }, }, }, args: { - children: "Saving...", + children: "Pending", isPending: true, }, }; @@ -753,7 +753,7 @@ const RenderProps: Story = { const RenderProps = () => { return ( ); };`, @@ -761,7 +761,7 @@ const RenderProps = () => { }, }, render: (args): React.JSX.Element => ( - + ), }; @@ -775,9 +775,9 @@ const GlassVariants = () => { return (
- - - + + +
); @@ -789,15 +789,15 @@ const GlassVariants = () => {
@@ -814,9 +814,9 @@ const FrostVariants = () => { return (
- - - + + +
); @@ -828,15 +828,15 @@ const FrostVariants = () => {
diff --git a/packages/react/src/components/description/description.stories.tsx b/packages/react/src/components/description/description.stories.tsx index 76efdbc..c90c5b1 100644 --- a/packages/react/src/components/description/description.stories.tsx +++ b/packages/react/src/components/description/description.stories.tsx @@ -10,7 +10,7 @@ const meta = { layout: "centered", }, args: { - children: "We'll never share your email with anyone else.", + children: "Description", size: "md", isInvalid: false, isDisabled: false, @@ -73,7 +73,7 @@ const Default: Story = { code: `import { Description } from "@fried-ui/react"; const Basic = () => { - return We'll never share your email with anyone else.; + return Description; };`, }, }, @@ -89,9 +89,9 @@ const Sizes: Story = { const Sizes = () => { return (
- Small helper text - Medium helper text - Large helper text + Small Description + Medium Description + Large Description
); };`, @@ -101,15 +101,15 @@ const Sizes = () => { render: (args): React.JSX.Element => (
- Small helper text + Small Description - Medium helper text + Medium Description - Large helper text + Large Description
), @@ -122,13 +122,13 @@ const Invalid: Story = { code: `import { Description } from "@fried-ui/react"; const Invalid = () => { - return Email format is invalid; + return Invalid Description; };`, }, }, }, args: { - children: "Email format is invalid", + children: "Invalid Description", isInvalid: true, }, }; @@ -140,13 +140,13 @@ const Disabled: Story = { code: `import { Description } from "@fried-ui/react"; const Disabled = () => { - return This field is currently unavailable; + return Disabled Description; };`, }, }, }, args: { - children: "This field is currently unavailable", + children: "Disabled Description", isDisabled: true, }, }; diff --git a/packages/react/src/components/label/label.stories.tsx b/packages/react/src/components/label/label.stories.tsx index b09fa80..3ca83a1 100644 --- a/packages/react/src/components/label/label.stories.tsx +++ b/packages/react/src/components/label/label.stories.tsx @@ -10,7 +10,7 @@ const meta = { layout: "centered", }, args: { - children: "Email", + children: "Label", size: "md", weight: "medium", isRequired: false, @@ -112,7 +112,7 @@ const Default: Story = { code: `import { Label } from "@fried-ui/react"; const Basic = () => { - return ; + return ; };`, }, }, @@ -126,13 +126,13 @@ const Required: Story = { code: `import { Label } from "@fried-ui/react"; const Required = () => { - return ; + return ; };`, }, }, }, args: { - children: "Password", + children: "Required Label", isRequired: true, }, }; @@ -144,13 +144,13 @@ const Optional: Story = { code: `import { Label } from "@fried-ui/react"; const Optional = () => { - return ; + return ; };`, }, }, }, args: { - children: "Phone number", + children: "Optional Label", optionalMessage: "(Optional)", }, }; @@ -164,9 +164,9 @@ const Sizes: Story = { const Sizes = () => { return (
- - - + + +
); };`, @@ -176,15 +176,15 @@ const Sizes = () => { render: (args): React.JSX.Element => (
), @@ -232,13 +232,13 @@ const Invalid: Story = { code: `import { Label } from "@fried-ui/react"; const Invalid = () => { - return ; + return ; };`, }, }, }, args: { - children: "Email (Invalid Format)", + children: "Invalid Label", isInvalid: true, }, }; @@ -250,13 +250,13 @@ const Disabled: Story = { code: `import { Label } from "@fried-ui/react"; const Disabled = () => { - return ; + return ; };`, }, }, }, args: { - children: "Username", + children: "Disabled Label", isDisabled: true, }, }; diff --git a/packages/react/src/components/surface/surface.stories.tsx b/packages/react/src/components/surface/surface.stories.tsx index 34c680f..ae713a8 100644 --- a/packages/react/src/components/surface/surface.stories.tsx +++ b/packages/react/src/components/surface/surface.stories.tsx @@ -111,7 +111,7 @@ const Card = () => { return (

Card Title

-

Content goes here.

+

Surface content

); };`, @@ -121,7 +121,7 @@ const Card = () => { render: (args): React.JSX.Element => (

Card Title

-

Content goes here.

+

Surface content

), }; @@ -137,12 +137,12 @@ const Variants = () => {

Default

-

Subtle gray card on white.

+

Surface content

Default Bordered

-

Gray card with emphasis border.

+

Surface content

); @@ -154,12 +154,12 @@ const Variants = () => {

Default

-

Subtle gray card on white.

+

Surface content

Default Bordered

-

Gray card with emphasis border.

+

Surface content

), @@ -176,19 +176,19 @@ const StatusVariants = () => {

Success

-

Your changes have been saved.

+

Surface content

Warning

-

Your session expires in 5 minutes.

+

Surface content

-

Error

-

Unable to connect to the server.

+

Danger

+

Surface content

Info

-

New feature available — check it out!

+

Surface content

); @@ -200,22 +200,22 @@ const StatusVariants = () => {

Success

-

Your changes have been saved.

+

Surface content

Warning

-

Your session expires in 5 minutes.

+

Surface content

-

Error

-

Unable to connect to the server.

+

Danger

+

Surface content

Info

-

New feature available — check it out!

+

Surface content

), @@ -232,12 +232,12 @@ const PlainVariants = () => {

Plain

-

Pure white card on gray page.

+

Surface content

Plain Bordered

-

White card with emphasis border.

+

Surface content

); @@ -249,12 +249,12 @@ const PlainVariants = () => {

Plain

-

Pure white card on gray page.

+

Surface content

Plain Bordered

-

White card with emphasis border.

+

Surface content

), @@ -271,7 +271,7 @@ const GlassVariants = () => {

Glass Surface

-

Content over a gradient background.

+

Surface content

); @@ -283,7 +283,7 @@ const GlassVariants = () => {

Glass Surface

-

Content over a gradient background.

+

Surface content

), @@ -300,7 +300,7 @@ const FrostVariants = () => {

Frost Surface

-

Frosted content without a border.

+

Surface content

); @@ -312,7 +312,7 @@ const FrostVariants = () => {

Frost Surface

-

Frosted content without a border.

+

Surface content

), @@ -329,7 +329,7 @@ const OverlayVariants = () => {

Overlay Surface

-

Dark scrim over media content.

+

Surface content

); @@ -341,7 +341,7 @@ const OverlayVariants = () => {

Overlay Surface

-

Dark scrim over media content.

+

Surface content

), @@ -358,27 +358,27 @@ const Radius = () => {

None

-

No rounded corners.

+

Surface content

Small

-

Subtle rounding.

+

Surface content

Medium

-

Balanced rounding.

+

Surface content

Large

-

Soft rounding.

+

Surface content

Extra Large

-

Very rounded corners.

+

Surface content

); @@ -390,27 +390,27 @@ const Radius = () => {

None

-

No rounded corners.

+

Surface content

Small

-

Subtle rounding.

+

Surface content

Medium

-

Balanced rounding.

+

Surface content

Large

-

Soft rounding.

+

Surface content

Extra Large

-

Very rounded corners.

+

Surface content

), @@ -427,27 +427,27 @@ const Shadow = () => {

None

-

No shadow.

+

Surface content

Small

-

Subtle lift.

+

Surface content

Medium

-

Noticeable elevation.

+

Surface content

Large

-

Floating card.

+

Surface content

Extra Large

-

Strong elevation.

+

Surface content

); @@ -459,27 +459,27 @@ const Shadow = () => {

None

-

No shadow.

+

Surface content

Small

-

Subtle lift.

+

Surface content

Medium

-

Noticeable elevation.

+

Surface content

Large

-

Floating card.

+

Surface content

Extra Large

-

Strong elevation.

+

Surface content

), From b3964e91ba11b95cbbb34795f66eba818a03987e Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Sun, 19 Apr 2026 10:20:09 +0700 Subject: [PATCH 079/199] fix(react): Correct attribute order in Description component --- packages/react/src/components/description/Description.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react/src/components/description/Description.tsx b/packages/react/src/components/description/Description.tsx index 2d34128..53d98a4 100644 --- a/packages/react/src/components/description/Description.tsx +++ b/packages/react/src/components/description/Description.tsx @@ -30,7 +30,7 @@ const Description = (props: Readonly) => { ); return ( - + {children} ); From 837c45335803946cf0e21b1638ef6a2bab94fd60 Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Sun, 19 Apr 2026 10:20:50 +0700 Subject: [PATCH 080/199] feat(husky): Add lint command to pre-commit hook --- .husky/pre-commit | 1 + 1 file changed, 1 insertion(+) diff --git a/.husky/pre-commit b/.husky/pre-commit index cb2c84d..2dea76f 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1 +1,2 @@ pnpm lint-staged +pnpm lint From e4264181a3fbdb10aa784d80a5b2601e7dd4157a Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Tue, 21 Apr 2026 06:16:54 +0700 Subject: [PATCH 081/199] refactor(react): Surface API and color token contrast MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace 6 -bordered variant suffixes with isBordered boolean (15 variants → 9 variants + boolean) for consistency across glass/frost/overlay variants. Token fixes: - background-secondary: bump to neutral-50 for plain card contrast - scrim-border: 10% → 20% opacity for visible overlay borders --- .../react/src/components/surface/Surface.tsx | 3 +- .../components/surface/surface.stories.tsx | 144 +++++++++++++----- .../src/components/surface/surface.test.tsx | 30 ++-- .../components/surface/surface.variants.ts | 18 +-- packages/styles/src/components/surface.css | 72 +++++---- packages/styles/src/tokens/colors.css | 6 +- 6 files changed, 161 insertions(+), 112 deletions(-) diff --git a/packages/react/src/components/surface/Surface.tsx b/packages/react/src/components/surface/Surface.tsx index 20739ce..4fe7558 100644 --- a/packages/react/src/components/surface/Surface.tsx +++ b/packages/react/src/components/surface/Surface.tsx @@ -14,7 +14,7 @@ export type SurfaceProps = Omit, "className"> & }; const Surface = (props: Readonly) => { - const { children, className, radius, ref, shadow, variant, ...rest } = props; + const { children, className, isBordered, radius, ref, shadow, variant, ...rest } = props; const surfaceClassName = clsx( bem({ @@ -23,6 +23,7 @@ const Surface = (props: Readonly) => { variant, radius, shadow, + bordered: isBordered, }, }), className, diff --git a/packages/react/src/components/surface/surface.stories.tsx b/packages/react/src/components/surface/surface.stories.tsx index ae713a8..741c191 100644 --- a/packages/react/src/components/surface/surface.stories.tsx +++ b/packages/react/src/components/surface/surface.stories.tsx @@ -14,6 +14,7 @@ const meta = { variant: "default", radius: "md", shadow: "none", + isBordered: false, }, argTypes: { children: { @@ -28,29 +29,12 @@ const meta = { }, variant: { control: "select", - options: [ - "default", - "default-bordered", - "plain", - "plain-bordered", - "success", - "success-bordered", - "warning", - "warning-bordered", - "danger", - "danger-bordered", - "info", - "info-bordered", - "glass", - "frost", - "overlay", - ], + options: ["default", "plain", "success", "warning", "danger", "info", "glass", "frost", "overlay"], description: - "Visual style. **Base:** default/plain (neutral cards). **Status:** success/warning/danger/info (pale bg — alert/callout boxes). **Special:** glass/frost/overlay (media overlays). Add `-bordered` suffix for emphasis border. Use default on white pages, plain on gray pages, status for alerts.", + "Visual style. **Base:** default/plain (neutral cards). **Status:** success/warning/danger/info (pale bg — alert/callout boxes). **Special:** glass/frost/overlay (media overlays). Pair with `isBordered` for emphasis border. Use default on white pages, plain on gray pages, status for alerts.", table: { type: { - summary: - '"default" | "default-bordered" | "plain" | "plain-bordered" | "success" | "success-bordered" | "warning" | "warning-bordered" | "danger" | "danger-bordered" | "info" | "info-bordered" | "glass" | "frost" | "overlay"', + summary: '"default" | "plain" | "success" | "warning" | "danger" | "info" | "glass" | "frost" | "overlay"', }, defaultValue: { summary: "default", @@ -58,6 +42,19 @@ const meta = { category: "Style Variants", }, }, + isBordered: { + control: "boolean", + description: "Adds emphasis border using variant-specific border token", + table: { + type: { + summary: "boolean", + }, + defaultValue: { + summary: "false", + }, + category: "Style Variants", + }, + }, radius: { control: "select", options: ["none", "sm", "md", "lg", "xl"], @@ -140,7 +137,7 @@ const Variants = () => {

Surface content

- +

Default Bordered

Surface content

@@ -157,7 +154,7 @@ const Variants = () => {

Surface content

- +

Default Bordered

Surface content

@@ -178,7 +175,7 @@ const StatusVariants = () => {

Success

Surface content

- +

Warning

Surface content

@@ -186,7 +183,7 @@ const StatusVariants = () => {

Danger

Surface content

- +

Info

Surface content

@@ -203,7 +200,7 @@ const StatusVariants = () => {

Surface content

- +

Warning

Surface content

@@ -213,7 +210,7 @@ const StatusVariants = () => {

Surface content

- +

Info

Surface content

@@ -229,16 +226,11 @@ const PlainVariants: Story = { const PlainVariants = () => { return ( -
+

Plain

Surface content

- - -

Plain Bordered

-

Surface content

-
); };`, @@ -246,16 +238,11 @@ const PlainVariants = () => { }, }, render: (args): React.JSX.Element => ( -
+

Plain

Surface content

- - -

Plain Bordered

-

Surface content

-
), }; @@ -269,7 +256,7 @@ const GlassVariants: Story = { const GlassVariants = () => { return (
- +

Glass Surface

Surface content

@@ -281,7 +268,7 @@ const GlassVariants = () => { }, render: (args): React.JSX.Element => (
- +

Glass Surface

Surface content

@@ -327,7 +314,7 @@ const OverlayVariants: Story = { const OverlayVariants = () => { return (
- +

Overlay Surface

Surface content

@@ -339,7 +326,7 @@ const OverlayVariants = () => { }, render: (args): React.JSX.Element => (
- +

Overlay Surface

Surface content

@@ -485,6 +472,78 @@ const Shadow = () => { ), }; +const Bordered: Story = { + parameters: { + docs: { + source: { + code: `import { Surface } from "@fried-ui/react"; + +const Bordered = () => { + return ( +
+ +

Default

+

Surface content

+
+ + +

Success

+

Surface content

+
+ + +

Warning

+

Surface content

+
+ + +

Danger

+

Surface content

+
+ + +

Info

+

Surface content

+
+
+ ); +};`, + }, + }, + }, + args: { + isBordered: true, + }, + render: (args): React.JSX.Element => ( +
+ +

Default

+

Surface content

+
+ + +

Success

+

Surface content

+
+ + +

Warning

+

Surface content

+
+ + +

Danger

+

Surface content

+
+ + +

Info

+

Surface content

+
+
+ ), +}; + export { Default, Variants, @@ -493,6 +552,7 @@ export { GlassVariants, FrostVariants, OverlayVariants, + Bordered, Radius, Shadow, }; diff --git a/packages/react/src/components/surface/surface.test.tsx b/packages/react/src/components/surface/surface.test.tsx index 5c3e9f5..77d2f73 100644 --- a/packages/react/src/components/surface/surface.test.tsx +++ b/packages/react/src/components/surface/surface.test.tsx @@ -17,23 +17,7 @@ describe("Surface", () => { }); it("applies all variant classes", () => { - const variants = [ - "default", - "default-bordered", - "plain", - "plain-bordered", - "success", - "success-bordered", - "warning", - "warning-bordered", - "danger", - "danger-bordered", - "info", - "info-bordered", - "glass", - "frost", - "overlay", - ] as const; + const variants = ["default", "plain", "success", "warning", "danger", "info", "glass", "frost", "overlay"] as const; variants.forEach((variant) => { const { unmount } = render({variant}); @@ -42,6 +26,11 @@ describe("Surface", () => { }); }); + it("applies bordered modifier class", () => { + render(Bordered); + expect(screen.getByText("Bordered").className).toContain("fri-surface--bordered"); + }); + it("applies radius class with key-value format", () => { const radiusValues = ["none", "sm", "md", "lg", "xl"] as const; @@ -62,15 +51,16 @@ describe("Surface", () => { }); }); - it("combines variant + radius + shadow", () => { + it("combines variant + isBordered + radius + shadow", () => { render( - + Combined , ); const el = screen.getByText("Combined"); - expect(el.className).toContain("fri-surface--plain-bordered"); + expect(el.className).toContain("fri-surface--plain"); + expect(el.className).toContain("fri-surface--bordered"); expect(el.className).toContain("fri-surface--radius-lg"); expect(el.className).toContain("fri-surface--shadow-md"); }); diff --git a/packages/react/src/components/surface/surface.variants.ts b/packages/react/src/components/surface/surface.variants.ts index 727572d..9322622 100644 --- a/packages/react/src/components/surface/surface.variants.ts +++ b/packages/react/src/components/surface/surface.variants.ts @@ -1,20 +1,6 @@ export interface SurfaceVariantsProps { + isBordered?: boolean; radius?: "none" | "sm" | "md" | "lg" | "xl"; shadow?: "none" | "sm" | "md" | "lg" | "xl"; - variant?: - | "default" - | "default-bordered" - | "plain" - | "plain-bordered" - | "success" - | "success-bordered" - | "warning" - | "warning-bordered" - | "danger" - | "danger-bordered" - | "info" - | "info-bordered" - | "glass" - | "frost" - | "overlay"; + variant?: "default" | "plain" | "success" | "warning" | "danger" | "info" | "glass" | "frost" | "overlay"; } diff --git a/packages/styles/src/components/surface.css b/packages/styles/src/components/surface.css index 939b1a0..0d6e4d9 100644 --- a/packages/styles/src/components/surface.css +++ b/packages/styles/src/components/surface.css @@ -1,68 +1,80 @@ @layer components { .fri-surface { @apply border border-transparent; - @apply bg-(--color-surface) text-(--color-surface-foreground); + @apply bg-surface text-surface-foreground; @apply rounded-md; } .fri-surface--default { - @apply bg-(--color-surface) text-(--color-surface-foreground); + @apply bg-surface text-surface-foreground; } - .fri-surface--default-bordered { - @apply border-(--color-surface-border) bg-(--color-surface) text-(--color-surface-foreground); + .fri-surface--plain { + @apply bg-surface-plain text-surface-plain-foreground; } - .fri-surface--plain { - @apply bg-(--color-surface-plain) text-(--color-surface-plain-foreground); + .fri-surface--success { + @apply bg-success-soft text-success-soft-foreground; } - .fri-surface--plain-bordered { - @apply border-(--color-surface-plain-border) bg-(--color-surface-plain) text-(--color-surface-plain-foreground); + .fri-surface--warning { + @apply bg-warning-soft text-warning-soft-foreground; } - .fri-surface--success { - @apply bg-(--color-success-soft) text-(--color-success-soft-foreground); + .fri-surface--danger { + @apply bg-danger-soft text-danger-soft-foreground; } - .fri-surface--success-bordered { - @apply border-(--color-success-soft-border) bg-(--color-success-soft) text-(--color-success-soft-foreground); + .fri-surface--info { + @apply bg-info-soft text-info-soft-foreground; } - .fri-surface--warning { - @apply bg-(--color-warning-soft) text-(--color-warning-soft-foreground); + .fri-surface--glass { + @apply bg-glass text-glass-foreground backdrop-blur-xl backdrop-saturate-200; } - .fri-surface--warning-bordered { - @apply border-(--color-warning-soft-border) bg-(--color-warning-soft) text-(--color-warning-soft-foreground); + .fri-surface--frost { + @apply bg-glass text-glass-foreground backdrop-blur-2xl backdrop-saturate-200; } - .fri-surface--danger { - @apply bg-(--color-danger-soft) text-(--color-danger-soft-foreground); + .fri-surface--overlay { + @apply bg-scrim text-scrim-foreground backdrop-blur-xl backdrop-saturate-150; } - .fri-surface--danger-bordered { - @apply border-(--color-danger-soft-border) bg-(--color-danger-soft) text-(--color-danger-soft-foreground); + .fri-surface--bordered.fri-surface--default { + @apply border-surface-border; } - .fri-surface--info { - @apply bg-(--color-info-soft) text-(--color-info-soft-foreground); + .fri-surface--bordered.fri-surface--plain { + @apply border-surface-plain-border; } - .fri-surface--info-bordered { - @apply border-(--color-info-soft-border) bg-(--color-info-soft) text-(--color-info-soft-foreground); + .fri-surface--bordered.fri-surface--success { + @apply border-success-soft-border; } - .fri-surface--glass { - @apply border-glass-border bg-glass text-glass-foreground backdrop-blur-xl backdrop-saturate-200; + .fri-surface--bordered.fri-surface--warning { + @apply border-warning-soft-border; } - .fri-surface--frost { - @apply bg-glass text-glass-foreground backdrop-blur-2xl backdrop-saturate-200; + .fri-surface--bordered.fri-surface--danger { + @apply border-danger-soft-border; } - .fri-surface--overlay { - @apply border-scrim-border bg-scrim text-scrim-foreground backdrop-blur-xl backdrop-saturate-150; + .fri-surface--bordered.fri-surface--info { + @apply border-info-soft-border; + } + + .fri-surface--bordered.fri-surface--glass { + @apply border-glass-border; + } + + .fri-surface--bordered.fri-surface--frost { + @apply border-glass-border; + } + + .fri-surface--bordered.fri-surface--overlay { + @apply border-scrim-border; } .fri-surface--shadow-none { diff --git a/packages/styles/src/tokens/colors.css b/packages/styles/src/tokens/colors.css index c87837a..95501ac 100644 --- a/packages/styles/src/tokens/colors.css +++ b/packages/styles/src/tokens/colors.css @@ -184,9 +184,8 @@ --color-info-ghost-foreground: var(--color-fri-blue-500); --color-background: oklch(100% 0 0); - --color-background-secondary: oklch(98.875% 0 0); + --color-background-secondary: var(--color-fri-neutral-50); --color-foreground: oklch(0% 0 0); - --color-foreground-inverted: oklch(100% 0 0); --color-surface: oklch(97.75% 0 0); --color-surface-border: var(--color-fri-neutral-100); @@ -252,12 +251,13 @@ --color-scrim: oklch(0% 0 0 / 0.6); --color-scrim-hover: oklch(0% 0 0 / 0.75); --color-scrim-active: oklch(0% 0 0 / 0.85); - --color-scrim-border: oklch(100% 0 0 / 0.1); + --color-scrim-border: oklch(100% 0 0 / 0.2); --color-scrim-foreground: oklch(100% 0 0); } @layer base { .dark, [data-theme="dark"] { + --color-background-secondary: var(--color-fri-neutral-900); } } From 8427b3a3b8d0d935a9b2661598517dfe9ce87a70 Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Tue, 21 Apr 2026 06:17:32 +0700 Subject: [PATCH 082/199] feat(react): Media icons and consistent styling - Add PlayIcon, PauseIcon, SpeakerIcon for media overlay use cases - Standardize 4 component CSS to utility-style (bg-X) instead of bg-(--color-X) shorthand for consistency with shadcn pattern - Fix Button :hover/:active CSS cascade order so pressed state wins - Add icons to Glass/Frost/Overlay variant stories with start/end/start pattern to differentiate visually --- .../src/components/badge/badge.stories.tsx | 136 +++++++++------ .../src/components/button/button.stories.tsx | 165 ++++++++++++------ .../src/components/button/button.test.tsx | 8 +- .../react/src/components/icons/PauseIcon.tsx | 26 +++ .../react/src/components/icons/PlayIcon.tsx | 25 +++ .../src/components/icons/SpeakerIcon.tsx | 27 +++ packages/react/src/components/icons/index.ts | 3 + packages/styles/src/components/avatar.css | 16 +- packages/styles/src/components/button.css | 10 +- .../styles/src/components/description.css | 4 +- packages/styles/src/components/label.css | 8 +- .../component.test.tsx.hbs | 4 +- 12 files changed, 301 insertions(+), 131 deletions(-) create mode 100644 packages/react/src/components/icons/PauseIcon.tsx create mode 100644 packages/react/src/components/icons/PlayIcon.tsx create mode 100644 packages/react/src/components/icons/SpeakerIcon.tsx diff --git a/packages/react/src/components/badge/badge.stories.tsx b/packages/react/src/components/badge/badge.stories.tsx index 3ab382a..10c96bf 100644 --- a/packages/react/src/components/badge/badge.stories.tsx +++ b/packages/react/src/components/badge/badge.stories.tsx @@ -386,9 +386,20 @@ const GlassVariants = () => { return (
- Featured - New - Premium + + + Featured + + + + New + + + + + + Premium +
); @@ -400,14 +411,17 @@ const GlassVariants = () => {
+ Featured New + + Premium
@@ -425,9 +439,20 @@ const FrostVariants = () => { return (
- Featured - New - Premium + + + Verified + + + + Favorite + + + + + + Top Rated +
); @@ -439,15 +464,18 @@ const FrostVariants = () => {
- Featured + + Verified - New + Favorite + - Premium + + Top Rated
@@ -464,9 +492,20 @@ const OverlayVariants = () => { return (
- Live - HD - 4K + + + Live + + + + HD + + + + + + 4K +
); @@ -478,14 +517,17 @@ const OverlayVariants = () => {
+ Live HD + + 4K
@@ -577,19 +619,24 @@ const WithIcon: Story = { parameters: { docs: { source: { - code: `import { Badge, CheckCircleIcon, ExclamationTriangleIcon, InformationCircleIcon, XCircleIcon } from "@fried-ui/react"; + code: `import { Badge, CheckCircleIcon, ExclamationTriangleIcon, InformationCircleIcon, StarIcon, XCircleIcon } from "@fried-ui/react"; const WithIcon = () => { return (
+ + + Featured + + Info - Success + @@ -598,8 +645,8 @@ const WithIcon = () => { - - Error + Danger +
); @@ -609,14 +656,19 @@ const WithIcon = () => { }, render: (args): React.JSX.Element => (
+ + + Featured + + Info - Success + @@ -625,8 +677,8 @@ const WithIcon = () => { - - Error + Danger +
), @@ -636,36 +688,24 @@ const IconOnly: Story = { parameters: { docs: { source: { - code: `import { Badge, BellIcon, CheckCircleIcon, HeartIcon, InformationCircleIcon, LockIcon, StarIcon, XCircleIcon } from "@fried-ui/react"; + code: `import { Badge, BellIcon, HeartIcon, LockIcon, StarIcon } from "@fried-ui/react"; const IconOnly = () => { return ( -
- - - - - - - - - +
+ - - - - - + - + - +
@@ -675,32 +715,20 @@ const IconOnly = () => { }, }, render: (args): React.JSX.Element => ( -
- - - - - - - - - +
+ - - - - - + - + - +
diff --git a/packages/react/src/components/button/button.stories.tsx b/packages/react/src/components/button/button.stories.tsx index af2dd7e..df03313 100644 --- a/packages/react/src/components/button/button.stories.tsx +++ b/packages/react/src/components/button/button.stories.tsx @@ -1,6 +1,21 @@ import type { Meta, StoryObj } from "@storybook/react"; -import { ArrowRightIcon, MoreIcon, PlusIcon, SettingsIcon, ShareIcon } from "../icons"; +import { + ArrowRightIcon, + BellIcon, + CheckCircleIcon, + ExclamationTriangleIcon, + HeartIcon, + InformationCircleIcon, + MoreIcon, + PauseIcon, + PlayIcon, + PlusIcon, + SettingsIcon, + ShareIcon, + SpeakerIcon, + XCircleIcon, +} from "../icons"; import { Button } from "./Button"; const meta = { @@ -576,19 +591,29 @@ const WithIcon: Story = { const WithIcon = () => { return (
- - + + + + -
); @@ -598,19 +623,29 @@ const WithIcon = () => { }, render: (args): React.JSX.Element => (
- - - + + + +
), @@ -677,14 +712,6 @@ const FullWidth = () => { return (
- - - -
); };`, @@ -696,14 +723,6 @@ const FullWidth = () => { - - - -
), }; @@ -753,7 +772,7 @@ const RenderProps: Story = { const RenderProps = () => { return ( ); };`, @@ -761,7 +780,7 @@ const RenderProps = () => { }, }, render: (args): React.JSX.Element => ( - + ), }; @@ -775,9 +794,20 @@ const GlassVariants = () => { return (
- - - + + + + +
); @@ -789,15 +819,18 @@ const GlassVariants = () => {
@@ -814,9 +847,20 @@ const FrostVariants = () => { return (
- - - + + + + +
); @@ -828,15 +872,18 @@ const FrostVariants = () => {
@@ -853,9 +900,20 @@ const OverlayVariants = () => { return (
- - - + + + + +
); @@ -867,14 +925,17 @@ const OverlayVariants = () => {
diff --git a/packages/react/src/components/button/button.test.tsx b/packages/react/src/components/button/button.test.tsx index 8d872f8..3ff433a 100644 --- a/packages/react/src/components/button/button.test.tsx +++ b/packages/react/src/components/button/button.test.tsx @@ -180,16 +180,16 @@ describe("Button", () => { it("supports className as render prop function", () => { render( - , + , ); const el = screen.getByRole("button"); expect(el.className).toContain("fri-button"); - expect(el.className).toContain("idle-class"); + expect(el.className).toContain("Hover-class"); }); it("supports render props children", () => { - render(); - expect(screen.getByRole("button", { name: "Idle" })).toBeInTheDocument(); + render(); + expect(screen.getByRole("button", { name: "Hover" })).toBeInTheDocument(); }); }); diff --git a/packages/react/src/components/icons/PauseIcon.tsx b/packages/react/src/components/icons/PauseIcon.tsx new file mode 100644 index 0000000..18dbd44 --- /dev/null +++ b/packages/react/src/components/icons/PauseIcon.tsx @@ -0,0 +1,26 @@ +import type { ComponentPropsWithRef } from "react"; + +const PauseIcon = (props: Readonly & { slot?: string }>) => { + const { className, ref, ...rest } = props; + + return ( + + + + + ); +}; + +PauseIcon.displayName = "PauseIcon"; + +export { PauseIcon }; diff --git a/packages/react/src/components/icons/PlayIcon.tsx b/packages/react/src/components/icons/PlayIcon.tsx new file mode 100644 index 0000000..8d37468 --- /dev/null +++ b/packages/react/src/components/icons/PlayIcon.tsx @@ -0,0 +1,25 @@ +import type { ComponentPropsWithRef } from "react"; + +const PlayIcon = (props: Readonly & { slot?: string }>) => { + const { className, ref, ...rest } = props; + + return ( + + + + ); +}; + +PlayIcon.displayName = "PlayIcon"; + +export { PlayIcon }; diff --git a/packages/react/src/components/icons/SpeakerIcon.tsx b/packages/react/src/components/icons/SpeakerIcon.tsx new file mode 100644 index 0000000..5bec38a --- /dev/null +++ b/packages/react/src/components/icons/SpeakerIcon.tsx @@ -0,0 +1,27 @@ +import type { ComponentPropsWithRef } from "react"; + +const SpeakerIcon = (props: Readonly & { slot?: string }>) => { + const { className, ref, ...rest } = props; + + return ( + + + + + + ); +}; + +SpeakerIcon.displayName = "SpeakerIcon"; + +export { SpeakerIcon }; diff --git a/packages/react/src/components/icons/index.ts b/packages/react/src/components/icons/index.ts index 7054551..7643d93 100644 --- a/packages/react/src/components/icons/index.ts +++ b/packages/react/src/components/icons/index.ts @@ -8,10 +8,13 @@ export { HeartIcon } from "./HeartIcon"; export { InformationCircleIcon } from "./InformationCircleIcon"; export { LockIcon } from "./LockIcon"; export { MoreIcon } from "./MoreIcon"; +export { PauseIcon } from "./PauseIcon"; +export { PlayIcon } from "./PlayIcon"; export { PlusIcon } from "./PlusIcon"; export { ReactAriaIcon } from "./ReactAriaIcon"; export { SettingsIcon } from "./SettingsIcon"; export { ShareIcon } from "./ShareIcon"; +export { SpeakerIcon } from "./SpeakerIcon"; export { Spinner } from "./Spinner"; export { StarIcon } from "./StarIcon"; export { StorybookIcon } from "./StorybookIcon"; diff --git a/packages/styles/src/components/avatar.css b/packages/styles/src/components/avatar.css index 4918fea..38c605d 100644 --- a/packages/styles/src/components/avatar.css +++ b/packages/styles/src/components/avatar.css @@ -3,7 +3,7 @@ @apply relative flex shrink-0 items-center justify-center overflow-hidden select-none; @apply leading-none font-medium; @apply size-10 rounded-full text-sm; - @apply bg-(--color-avatar) text-(--color-avatar-foreground); + @apply bg-avatar text-avatar-foreground; } .fri-avatar__image { @@ -59,30 +59,30 @@ } .fri-avatar--ring-primary { - @apply ring-2 ring-(--color-primary-outline-border) ring-offset-2 ring-offset-(--color-background); + @apply ring-2 ring-primary-outline-border ring-offset-2 ring-offset-background; } .fri-avatar--ring-secondary { - @apply ring-2 ring-(--color-secondary-outline-border) ring-offset-2 ring-offset-(--color-background); + @apply ring-2 ring-secondary-outline-border ring-offset-2 ring-offset-background; } .fri-avatar--ring-accent { - @apply ring-2 ring-(--color-accent-outline-border) ring-offset-2 ring-offset-(--color-background); + @apply ring-2 ring-accent-outline-border ring-offset-2 ring-offset-background; } .fri-avatar--ring-success { - @apply ring-2 ring-(--color-success-outline-border) ring-offset-2 ring-offset-(--color-background); + @apply ring-2 ring-success-outline-border ring-offset-2 ring-offset-background; } .fri-avatar--ring-warning { - @apply ring-2 ring-(--color-warning-outline-border) ring-offset-2 ring-offset-(--color-background); + @apply ring-2 ring-warning-outline-border ring-offset-2 ring-offset-background; } .fri-avatar--ring-danger { - @apply ring-2 ring-(--color-danger-outline-border) ring-offset-2 ring-offset-(--color-background); + @apply ring-2 ring-danger-outline-border ring-offset-2 ring-offset-background; } .fri-avatar--ring-info { - @apply ring-2 ring-(--color-info-outline-border) ring-offset-2 ring-offset-(--color-background); + @apply ring-2 ring-info-outline-border ring-offset-2 ring-offset-background; } } diff --git a/packages/styles/src/components/button.css b/packages/styles/src/components/button.css index 2c992a2..ae24104 100644 --- a/packages/styles/src/components/button.css +++ b/packages/styles/src/components/button.css @@ -27,17 +27,17 @@ @apply status-pending; } - &:active, - &[data-pressed] { - @apply bg-(--fri-button-bg-pressed) duration-(--duration-press); - } - @media (hover: hover) { &:hover, &[data-hovered] { @apply bg-(--fri-button-bg-hover); } } + + &:active, + &[data-pressed] { + @apply bg-(--fri-button-bg-pressed) duration-(--duration-press); + } } .fri-button--primary { diff --git a/packages/styles/src/components/description.css b/packages/styles/src/components/description.css index c8e0df0..a1e2e48 100644 --- a/packages/styles/src/components/description.css +++ b/packages/styles/src/components/description.css @@ -1,7 +1,7 @@ @layer components { .fri-description { @apply block; - @apply text-sm text-(--color-muted-foreground); + @apply text-sm text-muted-foreground; } .fri-description--size-sm { @@ -17,7 +17,7 @@ } .fri-description--invalid { - @apply text-(--color-danger); + @apply text-danger; } .fri-description--disabled, diff --git a/packages/styles/src/components/label.css b/packages/styles/src/components/label.css index 912ef3b..ea0bb42 100644 --- a/packages/styles/src/components/label.css +++ b/packages/styles/src/components/label.css @@ -2,15 +2,15 @@ .fri-label { @apply inline-flex items-center gap-1 select-none; @apply text-sm font-medium; - @apply text-(--color-foreground); + @apply text-foreground; } .fri-label__required { - @apply text-(--color-danger); + @apply text-danger; } .fri-label__optional { - @apply font-normal text-(--color-muted-foreground); + @apply font-normal text-muted-foreground; } .fri-label--size-sm { @@ -38,7 +38,7 @@ } .fri-label--invalid { - @apply text-(--color-danger); + @apply text-danger; } .fri-label--disabled, diff --git a/turbo/generators/interactive-component/component.test.tsx.hbs b/turbo/generators/interactive-component/component.test.tsx.hbs index 7e00bd0..1919159 100644 --- a/turbo/generators/interactive-component/component.test.tsx.hbs +++ b/turbo/generators/interactive-component/component.test.tsx.hbs @@ -97,12 +97,12 @@ describe("{{pascalCase name}}", () => { it("supports className as render prop function", () => { render( - <{{pascalCase name}} className={({ isHovered }): string => (isHovered ? "hovered" : "idle")}>Dynamic, + <{{pascalCase name}} className={({ isHovered }): string => (isHovered ? "hovered" : "Hover")}>Dynamic, ); const el = screen.getByText("Dynamic"); expect(el.className).toContain("fri-{{dashCase name}}"); - expect(el.className).toContain("idle"); + expect(el.className).toContain("Hover"); }); it("passes through native HTML attributes", () => { From 7211a03ebe67dfc0aa5322030eac1f45ba365fc3 Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Tue, 21 Apr 2026 06:17:47 +0700 Subject: [PATCH 083/199] chore(docs): Strip component MDX, keep overview as template Pause docs work until all components are implemented. Keep only overview/ section (introduction + quick-start) as template structure. Remove MdxComponents and supporting files no longer referenced. --- apps/docs/src/app/docs/[[...slug]]/page.tsx | 52 --- apps/docs/src/components/BadgeLinks.tsx | 34 -- apps/docs/src/components/ColorSwatch.tsx | 43 -- apps/docs/src/components/MdxComponents.tsx | 377 ------------------ apps/docs/src/components/Preview.tsx | 17 - .../src/content/docs/components/badge.mdx | 153 ------- .../src/content/docs/components/button.mdx | 311 --------------- .../src/content/docs/components/surface.mdx | 92 ----- apps/docs/src/content/docs/meta.json | 14 +- apps/docs/src/content/docs/styles/colors.mdx | 219 ---------- .../src/content/docs/styles/design-tokens.mdx | 210 ---------- 11 files changed, 1 insertion(+), 1521 deletions(-) delete mode 100644 apps/docs/src/components/BadgeLinks.tsx delete mode 100644 apps/docs/src/components/ColorSwatch.tsx delete mode 100644 apps/docs/src/components/MdxComponents.tsx delete mode 100644 apps/docs/src/components/Preview.tsx delete mode 100644 apps/docs/src/content/docs/components/badge.mdx delete mode 100644 apps/docs/src/content/docs/components/button.mdx delete mode 100644 apps/docs/src/content/docs/components/surface.mdx delete mode 100644 apps/docs/src/content/docs/styles/colors.mdx delete mode 100644 apps/docs/src/content/docs/styles/design-tokens.mdx diff --git a/apps/docs/src/app/docs/[[...slug]]/page.tsx b/apps/docs/src/app/docs/[[...slug]]/page.tsx index 9eea264..e6b3210 100644 --- a/apps/docs/src/app/docs/[[...slug]]/page.tsx +++ b/apps/docs/src/app/docs/[[...slug]]/page.tsx @@ -2,34 +2,7 @@ import defaultMdxComponents from "fumadocs-ui/mdx"; import { DocsBody, DocsDescription, DocsPage, DocsTitle } from "fumadocs-ui/page"; import { notFound } from "next/navigation"; -import { ColorSwatch } from "@/components/ColorSwatch"; import { CopyMarkdown } from "@/components/CopyMarkdown"; -import { - BadgeBadgeLinks, - BadgeDemo, - BadgeIconOnly, - BadgeOutlineVariants, - BadgeSizes, - BadgeSoftVariants, - BadgeVariants, - ButtonAsLink, - ButtonBadgeLinks, - ButtonCustomClass, - ButtonDemo, - ButtonDisabled, - ButtonFullWidth, - ButtonIconOnly, - ButtonPending, - ButtonRadius, - ButtonSizes, - ButtonVariants, - ButtonWithIcons, - SurfaceBadgeLinks, - SurfaceDemo, - SurfaceRadius, - SurfaceVariants, -} from "@/components/MdxComponents"; -import { Preview } from "@/components/Preview"; import { source } from "@/lib/source"; interface PageProps { @@ -42,31 +15,6 @@ interface SlugParamsProps { const mdxComponents = { ...defaultMdxComponents, - BadgeBadgeLinks, - BadgeDemo, - BadgeIconOnly, - BadgeOutlineVariants, - BadgeSizes, - BadgeSoftVariants, - BadgeVariants, - ButtonAsLink, - ButtonBadgeLinks, - ButtonCustomClass, - ButtonDemo, - ButtonDisabled, - ButtonFullWidth, - ButtonIconOnly, - ButtonPending, - ButtonRadius, - ButtonSizes, - ButtonVariants, - ButtonWithIcons, - ColorSwatch, - Preview, - SurfaceBadgeLinks, - SurfaceDemo, - SurfaceRadius, - SurfaceVariants, }; const Page = async (props: Readonly): Promise => { diff --git a/apps/docs/src/components/BadgeLinks.tsx b/apps/docs/src/components/BadgeLinks.tsx deleted file mode 100644 index 4f079c6..0000000 --- a/apps/docs/src/components/BadgeLinks.tsx +++ /dev/null @@ -1,34 +0,0 @@ -"use client"; - -import type { ReactNode } from "react"; - -import { Button } from "@fried-ui/react"; - -interface BadgeLinksProps { - links: BadgeLinkProps[]; -} - -export interface BadgeLinkProps { - href: string; - icon: ReactNode; - label: string; -} - -const BadgeLinks = (props: Readonly): React.JSX.Element => { - const { links } = props; - - return ( -
- {links.map((link) => ( - - - - ))} -
- ); -}; - -export { BadgeLinks }; diff --git a/apps/docs/src/components/ColorSwatch.tsx b/apps/docs/src/components/ColorSwatch.tsx deleted file mode 100644 index cd7628e..0000000 --- a/apps/docs/src/components/ColorSwatch.tsx +++ /dev/null @@ -1,43 +0,0 @@ -"use client"; - -interface ColorSwatchProps { - colors: ColorItemProps[]; -} - -export interface ColorItemProps { - name: string; - token: string; -} - -const SwatchItem = (props: Readonly): React.JSX.Element => { - const { name, token } = props; - const label = `${name} color swatch`; - const bgStyle = { backgroundColor: `var(--color-${token})` }; - - return ( -
-
- - {name} -
- ); -}; - -const ColorSwatch = (props: Readonly): React.JSX.Element => { - const { colors } = props; - - return ( -
- {colors.map((color) => ( - - ))} -
- ); -}; - -export { ColorSwatch }; diff --git a/apps/docs/src/components/MdxComponents.tsx b/apps/docs/src/components/MdxComponents.tsx deleted file mode 100644 index 86e1773..0000000 --- a/apps/docs/src/components/MdxComponents.tsx +++ /dev/null @@ -1,377 +0,0 @@ -"use client"; - -import { - ArrowRightIcon, - Badge, - Button, - GitHubIcon, - InformationCircleIcon, - MoreIcon, - PlusIcon, - ReactAriaIcon, - SettingsIcon, - ShareIcon, - StorybookIcon, - Surface, -} from "@fried-ui/react"; - -import { BadgeLinks } from "./BadgeLinks"; - -const badgeBadgeLinksData = [ - { - href: "https://fried-ui-storybook.vercel.app/?path=/story/components-badge--default", - icon: , - label: "Storybook", - }, - { - href: "https://github.com/fried-day/fried-ui/blob/main/packages/react/src/components/badge/Badge.tsx", - icon: , - label: "Source", - }, - { - href: "https://github.com/fried-day/fried-ui/blob/main/packages/styles/src/components/badge.css", - icon: , - label: "Styles source", - }, -]; - -const BadgeBadgeLinks = (): React.JSX.Element => { - return ; -}; - -const BadgeDemo = (): React.JSX.Element => { - return New; -}; - -const BadgeVariants = (): React.JSX.Element => { - return ( -
- Primary - Secondary - Outline - Success - Warning - Danger - Info -
- ); -}; - -const BadgeSoftVariants = (): React.JSX.Element => { - return ( -
- Primary - Secondary - Success - Warning - Danger - Info -
- ); -}; - -const BadgeOutlineVariants = (): React.JSX.Element => { - return ( -
- Primary - Secondary - Success - Warning - Danger - Info -
- ); -}; - -const BadgeIconOnly = (): React.JSX.Element => { - return ( -
- - - - - - - - - - - -
- ); -}; - -const BadgeSizes = (): React.JSX.Element => { - return ( -
- Small - Medium - Large -
- ); -}; - -const buttonBadgeLinksData = [ - { - href: "https://fried-ui-storybook.vercel.app/?path=/story/components-button--default", - icon: , - label: "Storybook", - }, - { - href: "https://react-spectrum.adobe.com/react-aria/Button.html", - icon: , - label: "React Aria", - }, - { - href: "https://github.com/fried-day/fried-ui/blob/main/packages/react/src/components/button/Button.tsx", - icon: , - label: "Source", - }, - { - href: "https://github.com/fried-day/fried-ui/blob/main/packages/styles/src/components/button.css", - icon: , - label: "Styles source", - }, -]; - -const ButtonBadgeLinks = (): React.JSX.Element => { - return ; -}; - -const ButtonDemo = (): React.JSX.Element => { - return ; -}; - -const ButtonVariants = (): React.JSX.Element => { - return ( -
- - - - - - - - -
- ); -}; - -const ButtonSizes = (): React.JSX.Element => { - return ( -
- - - - -
- ); -}; - -const ButtonRadius = (): React.JSX.Element => { - return ( -
- - - - - -
- ); -}; - -const ButtonIconOnly = (): React.JSX.Element => { - return ( -
- - - - - - - -
- ); -}; - -const ButtonFullWidth = (): React.JSX.Element => { - return ( -
- - - - - -
- ); -}; - -const ButtonDisabled = (): React.JSX.Element => { - return ; -}; - -const ButtonPending = (): React.JSX.Element => { - return ; -}; - -const ButtonCustomClass = (): React.JSX.Element => { - return ; -}; - -const ButtonWithIcons = (): React.JSX.Element => { - return ( -
- - - - - -
- ); -}; - -const ButtonAsLink = (): React.JSX.Element => { - return ( - - - - ); -}; - -const surfaceBadgeLinksData = [ - { - href: "https://fried-ui-storybook.vercel.app/?path=/story/components-surface--default", - icon: , - label: "Storybook", - }, - { - href: "https://github.com/fried-day/fried-ui/blob/main/packages/react/src/components/surface/Surface.tsx", - icon: , - label: "Source", - }, - { - href: "https://github.com/fried-day/fried-ui/blob/main/packages/styles/src/components/surface.css", - icon: , - label: "Styles source", - }, -]; - -const SurfaceBadgeLinks = (): React.JSX.Element => { - return ; -}; - -const SurfaceDemo = (): React.JSX.Element => { - return ( - -

Card Title

-

Content goes here.

-
- ); -}; - -const SurfaceVariants = (): React.JSX.Element => { - return ( -
- - Default - - - - Default Bordered - - - - Plain - - - - Plain Bordered - - - - Glass - - - - Frost - - - - Overlay - -
- ); -}; - -const SurfaceRadius = (): React.JSX.Element => { - return ( -
- - none - - - - sm - - - - md - - - - lg - - - - xl - -
- ); -}; - -export { - BadgeBadgeLinks, - BadgeDemo, - BadgeIconOnly, - BadgeOutlineVariants, - BadgeSizes, - BadgeSoftVariants, - BadgeVariants, - ButtonAsLink, - ButtonBadgeLinks, - ButtonCustomClass, - ButtonDemo, - ButtonDisabled, - ButtonFullWidth, - ButtonIconOnly, - ButtonPending, - ButtonRadius, - ButtonSizes, - ButtonVariants, - ButtonWithIcons, - SurfaceBadgeLinks, - SurfaceDemo, - SurfaceRadius, - SurfaceVariants, -}; diff --git a/apps/docs/src/components/Preview.tsx b/apps/docs/src/components/Preview.tsx deleted file mode 100644 index 5bd3e91..0000000 --- a/apps/docs/src/components/Preview.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import type { ReactNode } from "react"; - -interface PreviewProps { - children: ReactNode; -} - -const Preview = (props: Readonly): React.JSX.Element => { - const { children } = props; - - return ( -
- {children} -
- ); -}; - -export { Preview }; diff --git a/apps/docs/src/content/docs/components/badge.mdx b/apps/docs/src/content/docs/components/badge.mdx deleted file mode 100644 index cc2dc28..0000000 --- a/apps/docs/src/content/docs/components/badge.mdx +++ /dev/null @@ -1,153 +0,0 @@ ---- -title: Badge -description: Display-only label for status indicators, counts, and categories. ---- - - - -## Import - -```tsx -import { Badge } from "@fried-ui/react"; -``` - -## Usage - - - - - -```tsx -New -``` - -## Variants - -The `variant` prop controls the visual style using semantic color tokens. - -### Solid - - - - - -```tsx -Primary -Secondary -Outline -Success -Warning -Danger -Info -``` - -### Soft - -Pale background with colored text and border. Uses the `{color}-soft` naming. - - - - - -```tsx -Primary -Secondary -Success -Warning -Danger -Info -``` - -### Colored Outline - -Transparent background with colored border and text. Uses the `{color}-outline` naming. - - - - - -```tsx -Primary -Secondary -Success -Warning -Danger -Info -``` - -## Icon Only - -Square badge for icon-only indicators. Always provide `aria-label`. - - - - - -```tsx - - - -``` - -## Sizes - -The `size` prop controls padding, gap, and font-size. - - - - - -```tsx -Small -Medium -Large -``` - -## API Reference - -### Badge Props - -| Prop | Type | Default | Description | -| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | ------------------------------------- | -| `variant` | `"primary"` \| `"secondary"` \| `"outline"` \| `"success"` \| `"warning"` \| `"danger"` \| `"info"` \| `"primary-soft"` \| `"secondary-soft"` \| `"success-soft"` \| `"warning-soft"` \| `"danger-soft"` \| `"info-soft"` \| `"primary-outline"` \| `"secondary-outline"` \| `"success-outline"` \| `"warning-outline"` \| `"danger-outline"` \| `"info-outline"` | `"primary"` | The visual variant of the badge | -| `size` | `"sm"` \| `"md"` \| `"lg"` | `"md"` | The size of the badge | -| `isIconOnly` | `boolean` | `false` | Square badge for icon-only indicators | -| `className` | `string` | - | Additional CSS classes | -| `ref` | `Ref` | - | Forwarded ref to the span | -| `children` | `ReactNode` | - | Badge content | - -### CSS Classes - -| Class | Description | -| ------------------------------ | ------------------------- | -| `fri-badge` | Base styles | -| `fri-badge--primary` | Primary variant | -| `fri-badge--secondary` | Secondary variant | -| `fri-badge--outline` | Outline variant | -| `fri-badge--success` | Success variant | -| `fri-badge--warning` | Warning variant | -| `fri-badge--danger` | Danger variant | -| `fri-badge--info` | Info variant | -| `fri-badge--primary-soft` | Primary soft variant | -| `fri-badge--secondary-soft` | Secondary soft variant | -| `fri-badge--success-soft` | Success soft variant | -| `fri-badge--warning-soft` | Warning soft variant | -| `fri-badge--danger-soft` | Danger soft variant | -| `fri-badge--info-soft` | Info soft variant | -| `fri-badge--primary-outline` | Primary outline variant | -| `fri-badge--secondary-outline` | Secondary outline variant | -| `fri-badge--success-outline` | Success outline variant | -| `fri-badge--warning-outline` | Warning outline variant | -| `fri-badge--danger-outline` | Danger outline variant | -| `fri-badge--info-outline` | Info outline variant | -| `fri-badge--size-sm` | Small size | -| `fri-badge--size-md` | Medium size | -| `fri-badge--size-lg` | Large size | -| `fri-badge--icon-only` | Icon-only square | - -### CSS Variables - -| CSS Variable | Description | Default | -| ---------------- | ---------------- | -------------- | -| `--fri-badge-bg` | Background color | `transparent` | -| `--fri-badge-fg` | Text color | `currentColor` | diff --git a/apps/docs/src/content/docs/components/button.mdx b/apps/docs/src/content/docs/components/button.mdx deleted file mode 100644 index 5019383..0000000 --- a/apps/docs/src/content/docs/components/button.mdx +++ /dev/null @@ -1,311 +0,0 @@ ---- -title: Button -description: An accessible button component built on React Aria with focus management, keyboard navigation, and screen reader support. ---- - - - -## Import - -```tsx -import { Button } from "@fried-ui/react"; -``` - -## Usage - - - - - -```tsx - -``` - -## Features - -- Golden ratio (φ) proportional spacing across all sizes -- Accessible via React Aria — keyboard, focus, screen reader built-in -- Supports loading (pending) and disabled states -- Zero JS runtime styling — pure CSS with BEM -- Independent `radius` prop — style separated from structure -- `prefers-reduced-motion` respected automatically - -## Anatomy - -Button accepts `children` directly. Icons use `size-match-font` to lock size to the font. Gap between icon and label is controlled by golden ratio. - -```tsx - -``` - -## Variants - -The `variant` prop controls the visual style using semantic color tokens. - - - - - -```tsx - - - - - - - - -``` - -## Sizes - -The `size` prop controls padding, gap, and font-size using golden ratio proportional scaling. - - - - - -```tsx - - - - -``` - -## Radius - -The `radius` prop controls border-radius independently from size. - - - - - -```tsx - - - - - -``` - -## With Icons - -Icons inside buttons use the `size-match-font` utility to lock their size to the font. Gap between icon and text is calculated via φ — so icons scale proportionally across all sizes. - - - - - -```tsx - -``` - -## Icon Only - -Square button for icon-only actions. Always provide `aria-label` for accessibility. - - - - - -```tsx - -``` - -## As a Link - -Wrap a `Button` with an anchor or Next.js `Link` to render a button that navigates. - - - - - -```tsx -import Link from "next/link"; - - - -; -``` - -## Full Width - -Stretches the button to fill its container width. - - - - - -```tsx - -``` - -## Disabled - -Prevents interaction and applies disabled styling. - - - - - -```tsx - -``` - -## Pending - -Shows a loading spinner and disables press/hover events. - - - - - -```tsx - -``` - -## Custom Class - -Use `className` to add layout or typography utilities. Component visual styles (background, color) are controlled by props. - - - - - -```tsx - -``` - -## Render Props - -Access internal state to dynamically change content using the children function. - -```tsx - -``` - -Available render props: `isHovered`, `isPressed`, `isFocused`, `isFocusVisible`, `isDisabled`, `isPending`. - -## Design Anatomy - -Every spacing value is derived from the **golden ratio (φ = 1.618)** using font-size as the base unit `x`: - -| Property | Formula | sm | md | lg | xl | -| -------------- | --------- | -------- | -------- | -------- | -------- | -| Font | x | 0.875rem | 1rem | 1.25rem | 1.5rem | -| Padding Inline | x | 0.875rem | 1rem | 1.25rem | 1.5rem | -| Padding Block | x × 0.485 | 0.375rem | 0.5rem | 0.625rem | 0.75rem | -| Gap | x / φ | 0.541rem | 0.618rem | 0.773rem | 0.927rem | - -Radius and font-size are **independent** — size controls structure (spacing via φ), radius controls style (branding). - -## Accessibility - -Built on [React Aria Button](https://react-spectrum.adobe.com/react-aria/Button.html) — accessible out of the box: - -| Feature | Description | -| ---------------- | ---------------------------------------------------------- | -| Keyboard | `Enter` and `Space` trigger press events | -| Focus management | Visible focus ring on keyboard navigation | -| Screen reader | Announces button role, label, disabled, and pending states | -| Touch | Optimized press handling with cancel on drag | -| Reduced motion | Respects `prefers-reduced-motion` automatically | - -## API Reference - -### Button Props - -| Prop | Type | Default | Description | -| ------------------ | -------------------------------------------------------------------------------------------------- | ----------- | ------------------------------------------ | -| `variant` | `"primary" \| "secondary" \| "ghost" \| "outline" \| "success" \| "warning" \| "danger" \| "info"` | `"primary"` | The visual variant of the button | -| `size` | `"sm" \| "md" \| "lg" \| "xl"` | `"md"` | The size of the button | -| `radius` | `"none" \| "sm" \| "md" \| "lg" \| "full"` | `"md"` | The border radius of the button | -| `isIconOnly` | `boolean` | `false` | Whether the button is icon-only (square) | -| `isFullWidth` | `boolean` | `false` | Whether the button takes full width | -| `isDisabled` | `boolean` | `false` | Whether the button is disabled | -| `isPending` | `boolean` | `false` | Whether the button shows a loading spinner | -| `onPress` | `(e: PressEvent) => void` | - | Handler called when the button is pressed | -| `onPressStart` | `(e: PressEvent) => void` | - | Handler called when press starts | -| `onPressEnd` | `(e: PressEvent) => void` | - | Handler called when press ends | -| `onPressChange` | `(isPressed: boolean) => void` | - | Handler called when press state changes | -| `onHoverStart` | `(e: HoverEvent) => void` | - | Handler called when hover starts | -| `onHoverEnd` | `(e: HoverEvent) => void` | - | Handler called when hover ends | -| `onHoverChange` | `(isHovering: boolean) => void` | - | Handler called when hover state changes | -| `onFocus` | `(e: FocusEvent) => void` | - | Handler called when focused | -| `onBlur` | `(e: FocusEvent) => void` | - | Handler called when blurred | -| `onFocusChange` | `(isFocused: boolean) => void` | - | Handler called when focus state changes | -| `onKeyDown` | `(e: KeyboardEvent) => void` | - | Handler called on key down | -| `onKeyUp` | `(e: KeyboardEvent) => void` | - | Handler called on key up | -| `type` | `"button" \| "submit" \| "reset"` | `"button"` | The button type attribute | -| `form` | `string` | - | The form element to associate with | -| `formAction` | `string` | - | The URL for form submission | -| `autoFocus` | `boolean` | `false` | Whether to auto focus on mount | -| `aria-label` | `string` | - | Accessible label | -| `aria-labelledby` | `string` | - | ID of the labelling element | -| `aria-describedby` | `string` | - | ID of the describing element | -| `className` | `string` | - | Additional CSS classes | -| `ref` | `Ref` | - | Forwarded ref to the button element | -| `children` | `ReactNode \| (renderProps: ButtonRenderProps) => ReactNode` | - | Button content | - -### Render Props - -| Render Prop | CSS Selector | Description | -| ---------------- | ---------------------- | --------------------------- | -| `isHovered` | `[data-hovered]` | Mouse hover state | -| `isPressed` | `[data-pressed]` | Pressed/active state | -| `isFocused` | `[data-focused]` | Focused (mouse or keyboard) | -| `isFocusVisible` | `[data-focus-visible]` | Keyboard focused only | -| `isDisabled` | `[data-disabled]` | Disabled state | -| `isPending` | `[data-pending]` | Pending/loading state | - -### CSS Classes - -| Class | Description | -| ------------------------- | ------------------------- | -| `fri-button` | Base styles | -| `fri-button--primary` | Primary variant | -| `fri-button--secondary` | Secondary variant | -| `fri-button--ghost` | Ghost variant | -| `fri-button--outline` | Outline variant | -| `fri-button--success` | Success variant | -| `fri-button--warning` | Warning variant | -| `fri-button--danger` | Danger variant | -| `fri-button--info` | Info variant | -| `fri-button--sm` | Small size | -| `fri-button--md` | Medium size | -| `fri-button--lg` | Large size | -| `fri-button--xl` | Extra large size | -| `fri-button--radius-none` | No border radius | -| `fri-button--radius-sm` | Small border radius | -| `fri-button--radius-md` | Medium border radius | -| `fri-button--radius-lg` | Large border radius | -| `fri-button--radius-full` | Full (pill) border radius | -| `fri-button--disabled` | Disabled state | -| `fri-button--full-width` | Full width button | -| `fri-button--icon-only` | Icon-only square button | -| `fri-button--pending` | Pending/loading state | -| `fri-button__spinner` | Internal spinner element | - -### CSS Variables - -| CSS Variable | Description | Default | -| ------------------------- | ------------------------- | ---------------------------- | -| `--fri-button-bg` | Background color | `transparent` | -| `--fri-button-bg-hover` | Hover background | `var(--fri-button-bg)` | -| `--fri-button-bg-pressed` | Active/pressed background | `var(--fri-button-bg-hover)` | -| `--fri-button-fg` | Text and icon color | `currentColor` | diff --git a/apps/docs/src/content/docs/components/surface.mdx b/apps/docs/src/content/docs/components/surface.mdx deleted file mode 100644 index 0ca0aa4..0000000 --- a/apps/docs/src/content/docs/components/surface.mdx +++ /dev/null @@ -1,92 +0,0 @@ ---- -title: Surface -description: Visual layer component for cards, panels, and elevated content areas. ---- - - - -## Import - -```tsx -import { Surface } from "@fried-ui/react"; -``` - -## Usage - - - - - -```tsx - -

Card Title

-

Content goes here.

-
-``` - -## Variants - -The `variant` prop controls the visual style of the surface. - - - - - -```tsx -Default -Bordered -Glass -Primary -``` - -## Radius - -The `radius` prop controls border-radius. - - - - - -```tsx -none -sm -md -lg -xl -``` - -## API Reference - -### Surface Props - -| Prop | Type | Default | Description | -| ----------- | --------------------------------------------------------------------------------------------------------------- | ----------- | ------------------------ | -| `variant` | `"default"` \| `"default-bordered"` \| `"plain"` \| `"plain-bordered"` \| `"glass"` \| `"frost"` \| `"overlay"` | `"default"` | The visual variant | -| `radius` | `"none"` \| `"sm"` \| `"md"` \| `"lg"` \| `"xl"` | `"md"` | Border radius | -| `shadow` | `"none"` \| `"sm"` \| `"md"` \| `"lg"` \| `"xl"` | `"none"` | Elevation shadow | -| `className` | `string` | - | Additional CSS classes | -| `ref` | `Ref` | - | Forwarded ref to the div | -| `children` | `ReactNode` | - | Surface content | - -### CSS Classes - -| Class | Description | -| ------------------------------- | -------------------- | -| `fri-surface` | Base styles | -| `fri-surface--default` | Default variant | -| `fri-surface--default-bordered` | Default + border | -| `fri-surface--plain` | Plain variant | -| `fri-surface--plain-bordered` | Plain + border | -| `fri-surface--glass` | Glass variant | -| `fri-surface--frost` | Frost variant | -| `fri-surface--overlay` | Overlay variant | -| `fri-surface--radius-none` | No border radius | -| `fri-surface--radius-sm` | Small border radius | -| `fri-surface--radius-md` | Medium border radius | -| `fri-surface--radius-lg` | Large border radius | -| `fri-surface--radius-xl` | Extra large radius | -| `fri-surface--shadow-none` | No shadow | -| `fri-surface--shadow-sm` | Small shadow | -| `fri-surface--shadow-md` | Medium shadow | -| `fri-surface--shadow-lg` | Large shadow | -| `fri-surface--shadow-xl` | Extra large shadow | diff --git a/apps/docs/src/content/docs/meta.json b/apps/docs/src/content/docs/meta.json index 8eb814d..ae8575d 100644 --- a/apps/docs/src/content/docs/meta.json +++ b/apps/docs/src/content/docs/meta.json @@ -1,16 +1,4 @@ { "title": "Documentation", - "pages": [ - "---Overview---", - "index", - "overview/introduction", - "overview/quick-start", - "---Styles---", - "styles/design-tokens", - "styles/colors", - "---Components---", - "components/badge", - "components/button", - "components/surface" - ] + "pages": ["---Overview---", "index", "overview/introduction", "overview/quick-start"] } diff --git a/apps/docs/src/content/docs/styles/colors.mdx b/apps/docs/src/content/docs/styles/colors.mdx deleted file mode 100644 index eb88b77..0000000 --- a/apps/docs/src/content/docs/styles/colors.mdx +++ /dev/null @@ -1,219 +0,0 @@ ---- -title: Colors -description: Semantic color tokens for light and dark mode — 7 tokens per group, 4-layer elevation, typography hierarchy. ---- - -## Primary Colors - -Your main brand color used for primary actions and CTAs. - - - -## Secondary Colors - -For secondary actions and less emphasis. - - - -## Status Colors - -For alerts, validation, and status messages. - -### Success - - - -### Warning - - - -### Danger - - - -### Info - - - -## Background & Surface - -4 elevation layers from lowest to highest. - - - -## Muted Colors - -For ghost buttons, tags, chips, and subtle backgrounds. - - - -## Typography - -Text color hierarchy from strongest to weakest. - - - -## Border & Separator - - - -## Form Field Colors - -For consistent form field styling across input components. - - - -## Other Colors - - - -## Chart Colors - -5 colors for data visualization. - - - -## Focus Ring - -Two-color focus indicator for keyboard navigation (WCAG AAA). - - - -## How to Use - -```tsx - -
- Alert -
-``` - -## Dark Mode - -All colors switch automatically via `.dark` class or `data-theme="dark"`. Toggle the theme on this page to see the difference. diff --git a/apps/docs/src/content/docs/styles/design-tokens.mdx b/apps/docs/src/content/docs/styles/design-tokens.mdx deleted file mode 100644 index b4482b1..0000000 --- a/apps/docs/src/content/docs/styles/design-tokens.mdx +++ /dev/null @@ -1,210 +0,0 @@ ---- -title: Design Tokens -description: Color, spacing, elevation, motion, and layout tokens that power every Fried UI component. ---- - -## Overview - -Fried UI uses CSS custom properties as design tokens. All tokens are defined in `@fried-ui/styles` and automatically available as Tailwind CSS v4 utilities. - -```css -@import "tailwindcss"; -@import "@fried-ui/styles"; -``` - -## Semantic Colors - -Every semantic color has 7 tokens: - -| Token | Purpose | -| ------------------------ | ------------------------------------ | -| `{name}` | Solid background (button, badge) | -| `{name}-hover` | Background on hover | -| `{name}-active` | Background on pressed | -| `{name}-foreground` | Text on solid background | -| `{name}-soft` | Soft background (alert, soft button) | -| `{name}-soft-foreground` | Text on pale background | -| `{name}-soft-border` | Border on pale background | - -Available groups: `primary`, `secondary`, `success`, `warning`, `danger`, `info` - -```tsx -
Solid
-
Soft
-``` - -## Shadow - -Each color group has a shadow token using `em` units — shadows scale proportionally with the component's font-size. - -| Token | Usage | -| ----- | ----- | - -## Layout Elevation - -4 layers from lowest to highest: - -| Layer | Token | Light | Dark | Usage | -| ----- | ---------------------- | ----------- | ----------- | ----------------------- | -| 1 | `background` | white | black | Page background | -| 2 | `background-secondary` | neutral-50 | neutral-950 | Sidebar, header, footer | -| 3 | `surface` | neutral-100 | neutral-900 | Card, modal | -| 4 | `popover` | white | neutral-800 | Tooltip, dropdown | - -```tsx -
Page
- -
Card
-
Dropdown
-``` - -## Typography - -### Hierarchy - -| Token | Purpose | -| ---------------------- | ------------------------ | -| `heading` | h1-h6 headings | -| `foreground` | Body text | -| `foreground-secondary` | Subtitle, description | -| `foreground-muted` | Caption, hint, timestamp | - -```tsx -

Title

-

Body text

-

Description

-Updated 2 hours ago -``` - -### Heading Sizes - -Use Tailwind text size utilities directly for headings (`text-4xl`, `text-3xl`, etc.). - -## Spacing (Golden Ratio) - -All spacing derives from font-size × φ (1.618), rounded to nearest Tailwind step: - -| Property | Formula | sm | md | lg | xl | -| -------------- | --------- | ------- | --------- | ------- | -------- | -| Font | x | text-sm | text-base | text-xl | text-2xl | -| Padding Inline | x | px-3.5 | px-4 | px-5 | px-6 | -| Padding Block | x × 0.485 | py-1.5 | py-2 | py-2.5 | py-3 | -| Gap | x / φ | gap-2 | gap-2.5 | gap-3 | gap-4 | - -## Layout - -### Radius - -| Token | Value | Usage | -| ---------------- | ------------------------- | -------------------- | -| `--radius` | 0.5rem | Base border-radius | -| `--field-radius` | calc(var(--radius) × 1.5) | Input, select radius | - -### Z-Index (Semantic Layers) - -| Token | Value | Usage | -| ---------------- | ----- | ---------------------- | -| `--z-popover` | 10 | Dropdown, tooltip | -| `--z-navigation` | 20 | Sticky header, sidebar | -| `--z-overlay` | 30 | Backdrop | -| `--z-dialog` | 40 | Modal, drawer | -| `--z-alert` | 50 | Toast, notification | - -```tsx -
Dropdown
-
Modal
-``` - -### Disabled & Focus - -| Token | Value | Usage | -| ------------------------ | ----------- | ---------------------------------- | -| `--disabled-opacity` | 0.5 | Opacity for disabled elements | -| `--outline-offset-focus` | 2px | Gap between element and focus ring | -| `--cursor-interactive` | pointer | Clickable elements | -| `--cursor-disabled` | not-allowed | Disabled elements | - -## Motion - -All durations stay under 400ms (Doherty Threshold). Automatically disabled when `prefers-reduced-motion: reduce`. - -### Duration - -| Token | Value | Usage | -| ------------------- | ----- | ------------------------------ | -| `--duration-press` | 80ms | Button press, tactile feedback | -| `--duration-toggle` | 100ms | Switch, checkbox flip | -| `--duration-hover` | 150ms | Button hover, color change | -| `--duration-reveal` | 200ms | Dropdown, tooltip, fade | -| `--duration-enter` | 300ms | Modal, drawer, slide | - -### Easing - -| Token | Value | Usage | -| --------------- | ---------------------------- | ---------------- | -| `--ease-smooth` | cubic-bezier(0.4, 0, 0.2, 1) | Default for most | -| `--ease-in` | cubic-bezier(0.4, 0, 1, 1) | Element exiting | -| `--ease-out` | cubic-bezier(0, 0, 0.2, 1) | Element entering | - -```tsx -
Hover me
-``` - -### Animations - -| Token | Keyframes | Usage | -| ------------------------- | ------------------- | ------------- | -| `--animate-fade-in` | fade-in 200ms | Enter | -| `--animate-fade-out` | fade-out 200ms | Exit | -| `--animate-slide-in-up` | slide-in-up 200ms | Dropdown open | -| `--animate-slide-in-down` | slide-in-down 200ms | Tooltip below | -| `--animate-scale-in` | scale-in 200ms | Modal enter | - -```tsx -
Entering
-
Dropdown
-``` - -## Utilities - -Custom `@utility` directives available as Tailwind classes: - -| Utility | Purpose | -| ----------------- | ------------------------------------------------ | -| `focus-ring` | Two-color focus indicator (WCAG AAA) | -| `status-disabled` | opacity + not-allowed cursor + no pointer events | -| `status-pending` | No pointer events (component shows spinner) | -| `no-highlight` | Remove mobile tap highlight | -| `size-match-font` | 1em × 1em (icon scales with font-size) | - -## Border & Field - -| Token | Purpose | -| ------------------- | -------------------------------------------- | -| `border` | Default border (card, input, outline button) | -| `border-hover` | Border on hover | -| `field` | Input background | -| `field-border` | Input border | -| `field-placeholder` | Placeholder text | - -## Dark Mode - -Dark mode activates via `.dark` class or `data-theme="dark"`: - -```html - -``` - -All tokens automatically switch. Light values are defined in `@theme`, dark overrides in `@layer base`. - -## Chart Colors - -5 colors for data visualization: - -| Token | Source | -| --------- | ------------------ | -| `chart-1` | fried-lime (brand) | -| `chart-2` | blue | -| `chart-3` | rose | -| `chart-4` | amber | -| `chart-5` | green | From 2de55b6b92984ee9cfd201cdaae0057134cc68f1 Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Tue, 21 Apr 2026 06:35:41 +0700 Subject: [PATCH 084/199] feat(components): Enhance spacing documentation --- .claude/rules/spacing.md | 22 ++ .../components/surface/surface.stories.tsx | 8 +- .../display-component/component.css.hbs | 162 ++++++++------- .../component.stories.tsx.hbs | 12 +- .../interactive-component/component.css.hbs | 194 ++++++++++-------- .../component.stories.tsx.hbs | 12 +- 6 files changed, 241 insertions(+), 169 deletions(-) diff --git a/.claude/rules/spacing.md b/.claude/rules/spacing.md index ff01055..e0e7aa6 100644 --- a/.claude/rules/spacing.md +++ b/.claude/rules/spacing.md @@ -33,6 +33,28 @@ Font scale ใช้ √φ (1.272): 14 → 16 → 20 → 24 — ข้าม 18p | lg | 20px | `text-xl` | `px-5` | `py-2.5` | `gap-3` | `rounded-xl` | | xl | 24px | `text-2xl` | `px-6` | `py-3` | `gap-4` | `rounded-2xl` | +## Size Philosophy: Relative Scale (per Component) + +`size="md"` ของ component ต่างกัน **ไม่จำเป็นต้องมีขนาดเท่ากัน** — sm/md/lg เป็น **relative scale ภายใน range ของ component นั้นๆ** (ไม่ใช่หน่วยกลาง) + +ตัวอย่างที่ถูกต้อง: + +| Component | sm | md | lg | xl | +| --------- | -- | -- | -- | -- | +| Badge | 12px | 14px | 16px | — | +| Button | 14px | 16px | 20px | 24px | + +**เหตุผล:** Badge = label/tag (display, not pressed) → naturally smaller กว่า Button = interactive CTA → visual hierarchy ถูกต้อง + +**Industry standard:** MUI / shadcn / HeroUI / Mantine ทุก lib ใช้ pattern เดียวกัน — `Chip md` ≠ `Button md` + +**ห้าม unify** — ถ้า unify แล้ว Badge `lg` จะเท่า Button `md` → tag ใหญ่เกินสัดส่วน → break visual hierarchy + +**Component start size:** +- Display (Badge, Tag, Chip) → start `sm = 12px (text-xs)` +- Interactive (Button, Input) → start `sm = 14px (text-sm)` +- Container (Card, Modal) → ใช้ body text base = 16px + ## Container Anchor Base Container components (Card, Modal, Alert) ใช้ body text เป็น x: diff --git a/packages/react/src/components/surface/surface.stories.tsx b/packages/react/src/components/surface/surface.stories.tsx index 741c191..e2adb3f 100644 --- a/packages/react/src/components/surface/surface.stories.tsx +++ b/packages/react/src/components/surface/surface.stories.tsx @@ -31,7 +31,7 @@ const meta = { control: "select", options: ["default", "plain", "success", "warning", "danger", "info", "glass", "frost", "overlay"], description: - "Visual style. **Base:** default/plain (neutral cards). **Status:** success/warning/danger/info (pale bg — alert/callout boxes). **Special:** glass/frost/overlay (media overlays). Pair with `isBordered` for emphasis border. Use default on white pages, plain on gray pages, status for alerts.", + "Visual style. **Base:** default (neutral gray on white pages), plain (pure white on gray pages). **Status:** success (positive — completed actions), warning (caution — needs attention), danger (negative — destructive/error), info (neutral notice). **Special:** glass (frosted blur with border — premium overlay on media), frost (heavier blur no border — text-heavy overlay), overlay (dark scrim — subtitle/caption over images). Pair with `isBordered` for emphasis border on any variant. Use default for cards on white background, plain for cards on gray background, status variants for alert/callout boxes, glass/frost/overlay for media-heavy designs.", table: { type: { summary: '"default" | "plain" | "success" | "warning" | "danger" | "info" | "glass" | "frost" | "overlay"', @@ -58,7 +58,8 @@ const meta = { radius: { control: "select", options: ["none", "sm", "md", "lg", "xl"], - description: "The border radius of the surface", + description: + "Border radius scale. **none** — sharp corners (data tables, embedded). **sm** — subtle rounding. **md** (default) — standard cards. **lg** — emphasized cards/dialogs. **xl** — hero cards/banners.", table: { type: { summary: '"none" | "sm" | "md" | "lg" | "xl"', @@ -72,7 +73,8 @@ const meta = { shadow: { control: "select", options: ["none", "sm", "md", "lg", "xl"], - description: "Elevation shadow — orthogonal to variant", + description: + "Elevation depth (orthogonal to variant — combine freely). **none** (default) — flat. **sm** — subtle lift (hover hint). **md** — noticeable elevation (cards). **lg** — floating (popovers). **xl** — strong elevation (modals/dialogs).", table: { type: { summary: '"none" | "sm" | "md" | "lg" | "xl"', diff --git a/turbo/generators/display-component/component.css.hbs b/turbo/generators/display-component/component.css.hbs index ac6cf95..3662989 100644 --- a/turbo/generators/display-component/component.css.hbs +++ b/turbo/generators/display-component/component.css.hbs @@ -1,70 +1,94 @@ -.fri-{{dashCase name}} { - @apply inline-flex items-center justify-center; - @apply leading-none font-medium whitespace-nowrap; - @apply border border-transparent select-none; - - --fri-{{dashCase name}}-bg: var(--color-primary); - --fri-{{dashCase name}}-fg: var(--color-primary-foreground); - - @apply bg-(--fri-{{dashCase name}}-bg) text-(--fri-{{dashCase name}}-fg); - - @apply gap-2.5 px-4 py-2 text-base; -} - -.fri-{{dashCase name}}--primary { - --fri-{{dashCase name}}-bg: var(--color-primary); - --fri-{{dashCase name}}-fg: var(--color-primary-foreground); -} - -.fri-{{dashCase name}}--secondary { - --fri-{{dashCase name}}-bg: var(--color-secondary); - --fri-{{dashCase name}}-fg: var(--color-secondary-foreground); -} - -.fri-{{dashCase name}}--ghost { - --fri-{{dashCase name}}-bg: transparent; - --fri-{{dashCase name}}-fg: var(--color-foreground); -} - -.fri-{{dashCase name}}--outline { - --fri-{{dashCase name}}-bg: transparent; - --fri-{{dashCase name}}-fg: var(--color-foreground); - - @apply border-border; -} - -.fri-{{dashCase name}}--success { - --fri-{{dashCase name}}-bg: var(--color-success); - --fri-{{dashCase name}}-fg: var(--color-success-foreground); -} - -.fri-{{dashCase name}}--warning { - --fri-{{dashCase name}}-bg: var(--color-warning); - --fri-{{dashCase name}}-fg: var(--color-warning-foreground); -} - -.fri-{{dashCase name}}--danger { - --fri-{{dashCase name}}-bg: var(--color-danger); - --fri-{{dashCase name}}-fg: var(--color-danger-foreground); -} - -.fri-{{dashCase name}}--info { - --fri-{{dashCase name}}-bg: var(--color-info); - --fri-{{dashCase name}}-fg: var(--color-info-foreground); -} - -.fri-{{dashCase name}} [slot="icon"] { - @apply size-match-font; -} - -.fri-{{dashCase name}}--size-sm { - @apply gap-2 px-3.5 py-1.5 text-sm; -} - -.fri-{{dashCase name}}--size-md { - @apply gap-2.5 px-4 py-2 text-base; -} - -.fri-{{dashCase name}}--size-lg { - @apply gap-3 px-5 py-2.5 text-xl; +@layer components { + .fri-{{dashCase name}} { + @apply inline-flex items-center justify-center; + @apply leading-none font-medium whitespace-nowrap; + @apply border border-transparent select-none; + + --fri-{{dashCase name}}-bg: var(--color-primary); + --fri-{{dashCase name}}-fg: var(--color-primary-foreground); + + @apply bg-(--fri-{{dashCase name}}-bg) text-(--fri-{{dashCase name}}-fg); + @apply gap-[calc(1rem/2.058)] px-[calc(1rem)] py-[calc(1rem/2.058)] text-base; + @apply rounded-[calc(1em*1.272*2/4)]; + } + + .fri-{{dashCase name}}--primary { + --fri-{{dashCase name}}-bg: var(--color-primary); + --fri-{{dashCase name}}-fg: var(--color-primary-foreground); + } + + .fri-{{dashCase name}}--secondary { + --fri-{{dashCase name}}-bg: var(--color-secondary); + --fri-{{dashCase name}}-fg: var(--color-secondary-foreground); + } + + .fri-{{dashCase name}}--ghost { + --fri-{{dashCase name}}-bg: transparent; + --fri-{{dashCase name}}-fg: var(--color-foreground); + } + + .fri-{{dashCase name}}--outline { + --fri-{{dashCase name}}-bg: transparent; + --fri-{{dashCase name}}-fg: var(--color-foreground); + + @apply border-border; + } + + .fri-{{dashCase name}}--success { + --fri-{{dashCase name}}-bg: var(--color-success); + --fri-{{dashCase name}}-fg: var(--color-success-foreground); + } + + .fri-{{dashCase name}}--warning { + --fri-{{dashCase name}}-bg: var(--color-warning); + --fri-{{dashCase name}}-fg: var(--color-warning-foreground); + } + + .fri-{{dashCase name}}--danger { + --fri-{{dashCase name}}-bg: var(--color-danger); + --fri-{{dashCase name}}-fg: var(--color-danger-foreground); + } + + .fri-{{dashCase name}}--info { + --fri-{{dashCase name}}-bg: var(--color-info); + --fri-{{dashCase name}}-fg: var(--color-info-foreground); + } + + .fri-{{dashCase name}} [slot="icon"], + .fri-{{dashCase name}} [slot="icon-start"], + .fri-{{dashCase name}} [slot="icon-end"] { + @apply size-match-font; + } + + .fri-{{dashCase name}}--size-sm { + @apply gap-[calc(0.875rem/2.058)] px-[calc(0.875rem)] py-[calc(0.875rem/2.058)] text-sm; + } + + .fri-{{dashCase name}}--size-md { + @apply gap-[calc(1rem/2.058)] px-[calc(1rem)] py-[calc(1rem/2.058)] text-base; + } + + .fri-{{dashCase name}}--size-lg { + @apply gap-[calc(1.25rem/2.058)] px-[calc(1.25rem)] py-[calc(1.25rem/2.058)] text-xl; + } + + .fri-{{dashCase name}}--radius-none { + @apply rounded-none; + } + + .fri-{{dashCase name}}--radius-sm { + @apply rounded-[calc(1em*1.272/4)]; + } + + .fri-{{dashCase name}}--radius-md { + @apply rounded-[calc(1em*1.272*2/4)]; + } + + .fri-{{dashCase name}}--radius-lg { + @apply rounded-[calc(1em*1.272*3/4)]; + } + + .fri-{{dashCase name}}--radius-full { + @apply rounded-full; + } } diff --git a/turbo/generators/display-component/component.stories.tsx.hbs b/turbo/generators/display-component/component.stories.tsx.hbs index 9180ca0..44eb173 100644 --- a/turbo/generators/display-component/component.stories.tsx.hbs +++ b/turbo/generators/display-component/component.stories.tsx.hbs @@ -118,9 +118,9 @@ const Sizes: Story = { const Sizes = () => { return (
- <{{pascalCase name}} size="sm">Small - <{{pascalCase name}} size="md">Medium - <{{pascalCase name}} size="lg">Large + <{{pascalCase name}} size="sm">Small {{pascalCase name}} + <{{pascalCase name}} size="md">Medium {{pascalCase name}} + <{{pascalCase name}} size="lg">Large {{pascalCase name}}
); };`, @@ -129,9 +129,9 @@ const Sizes = () => { }, render: (args): React.JSX.Element => (
- <{{pascalCase name}} {...args} size="sm">Small - <{{pascalCase name}} {...args} size="md">Medium - <{{pascalCase name}} {...args} size="lg">Large + <{{pascalCase name}} {...args} size="sm">Small {{pascalCase name}} + <{{pascalCase name}} {...args} size="md">Medium {{pascalCase name}} + <{{pascalCase name}} {...args} size="lg">Large {{pascalCase name}}
), }; diff --git a/turbo/generators/interactive-component/component.css.hbs b/turbo/generators/interactive-component/component.css.hbs index abc0731..71432ee 100644 --- a/turbo/generators/interactive-component/component.css.hbs +++ b/turbo/generators/interactive-component/component.css.hbs @@ -1,110 +1,134 @@ -.fri-{{dashCase name}} { - @apply inline-flex items-center justify-center; - @apply leading-none font-medium whitespace-nowrap; - @apply cursor-pointer border border-transparent select-none no-highlight; - @apply transition-colors duration-(--duration-hover) ease-out motion-reduce:transition-none; +@layer components { + .fri-{{dashCase name}} { + @apply inline-flex items-center justify-center; + @apply leading-none font-medium whitespace-nowrap; + @apply cursor-pointer border border-transparent select-none no-highlight; + @apply transition-colors duration-(--duration-hover) ease-out motion-reduce:transition-none; + + --fri-{{dashCase name}}-bg: var(--color-primary); + --fri-{{dashCase name}}-bg-hover: var(--color-primary-hover); + --fri-{{dashCase name}}-bg-pressed: var(--color-primary-active); + --fri-{{dashCase name}}-fg: var(--color-primary-foreground); + + @apply bg-(--fri-{{dashCase name}}-bg) text-(--fri-{{dashCase name}}-fg); + @apply gap-[calc(1rem/2.058)] px-[calc(1rem)] py-[calc(1rem/2.058)] text-base; + @apply rounded-[calc(1em*1.272*2/4)]; + + &:focus-visible, + &[data-focus-visible] { + @apply focus-ring; + } - --fri-{{dashCase name}}-bg: var(--color-primary); - --fri-{{dashCase name}}-bg-hover: var(--color-primary-hover); - --fri-{{dashCase name}}-bg-pressed: var(--color-primary-active); - --fri-{{dashCase name}}-fg: var(--color-primary-foreground); + &.fri-{{dashCase name}}--disabled { + @apply status-disabled; + } - @apply bg-(--fri-{{dashCase name}}-bg) text-(--fri-{{dashCase name}}-fg); + @media (hover: hover) { + &:hover, + &[data-hovered] { + @apply bg-(--fri-{{dashCase name}}-bg-hover); + } + } - @apply gap-2.5 px-4 py-2 text-base; + &:active, + &[data-pressed] { + @apply bg-(--fri-{{dashCase name}}-bg-pressed) duration-(--duration-press); + } + } - &:focus-visible, - &[data-focus-visible] { - @apply focus-ring; + .fri-{{dashCase name}}--primary { + --fri-{{dashCase name}}-bg: var(--color-primary); + --fri-{{dashCase name}}-bg-hover: var(--color-primary-hover); + --fri-{{dashCase name}}-bg-pressed: var(--color-primary-active); + --fri-{{dashCase name}}-fg: var(--color-primary-foreground); } - &.fri-{{dashCase name}}--disabled { - @apply status-disabled; + .fri-{{dashCase name}}--secondary { + --fri-{{dashCase name}}-bg: var(--color-secondary); + --fri-{{dashCase name}}-bg-hover: var(--color-secondary-hover); + --fri-{{dashCase name}}-bg-pressed: var(--color-secondary-active); + --fri-{{dashCase name}}-fg: var(--color-secondary-foreground); } - &:active, - &[data-pressed] { - @apply bg-(--fri-{{dashCase name}}-bg-pressed); + .fri-{{dashCase name}}--ghost { + --fri-{{dashCase name}}-bg: transparent; + --fri-{{dashCase name}}-bg-hover: var(--color-muted); + --fri-{{dashCase name}}-bg-pressed: var(--color-muted-hover); + --fri-{{dashCase name}}-fg: var(--color-foreground); } - @media (hover: hover) { - &:hover, - &[data-hovered] { - @apply bg-(--fri-{{dashCase name}}-bg-hover); - } + .fri-{{dashCase name}}--outline { + --fri-{{dashCase name}}-bg: transparent; + --fri-{{dashCase name}}-bg-hover: var(--color-muted); + --fri-{{dashCase name}}-bg-pressed: var(--color-muted-hover); + --fri-{{dashCase name}}-fg: var(--color-foreground); + + @apply border-border; } -} -.fri-{{dashCase name}}--primary { - --fri-{{dashCase name}}-bg: var(--color-primary); - --fri-{{dashCase name}}-bg-hover: var(--color-primary-hover); - --fri-{{dashCase name}}-bg-pressed: var(--color-primary-active); - --fri-{{dashCase name}}-fg: var(--color-primary-foreground); -} + .fri-{{dashCase name}}--success { + --fri-{{dashCase name}}-bg: var(--color-success); + --fri-{{dashCase name}}-bg-hover: var(--color-success-hover); + --fri-{{dashCase name}}-bg-pressed: var(--color-success-active); + --fri-{{dashCase name}}-fg: var(--color-success-foreground); + } -.fri-{{dashCase name}}--secondary { - --fri-{{dashCase name}}-bg: var(--color-secondary); - --fri-{{dashCase name}}-bg-hover: var(--color-secondary-hover); - --fri-{{dashCase name}}-bg-pressed: var(--color-secondary-active); - --fri-{{dashCase name}}-fg: var(--color-secondary-foreground); -} + .fri-{{dashCase name}}--warning { + --fri-{{dashCase name}}-bg: var(--color-warning); + --fri-{{dashCase name}}-bg-hover: var(--color-warning-hover); + --fri-{{dashCase name}}-bg-pressed: var(--color-warning-active); + --fri-{{dashCase name}}-fg: var(--color-warning-foreground); + } -.fri-{{dashCase name}}--ghost { - --fri-{{dashCase name}}-bg: transparent; - --fri-{{dashCase name}}-bg-hover: var(--color-muted); - --fri-{{dashCase name}}-bg-pressed: var(--color-muted-hover); - --fri-{{dashCase name}}-fg: var(--color-foreground); -} + .fri-{{dashCase name}}--danger { + --fri-{{dashCase name}}-bg: var(--color-danger); + --fri-{{dashCase name}}-bg-hover: var(--color-danger-hover); + --fri-{{dashCase name}}-bg-pressed: var(--color-danger-active); + --fri-{{dashCase name}}-fg: var(--color-danger-foreground); + } -.fri-{{dashCase name}}--outline { - --fri-{{dashCase name}}-bg: transparent; - --fri-{{dashCase name}}-bg-hover: var(--color-surface-hover); - --fri-{{dashCase name}}-bg-pressed: var(--color-surface-active); - --fri-{{dashCase name}}-fg: var(--color-foreground); + .fri-{{dashCase name}}--info { + --fri-{{dashCase name}}-bg: var(--color-info); + --fri-{{dashCase name}}-bg-hover: var(--color-info-hover); + --fri-{{dashCase name}}-bg-pressed: var(--color-info-active); + --fri-{{dashCase name}}-fg: var(--color-info-foreground); + } - @apply border-border; -} + .fri-{{dashCase name}} [slot="icon"], + .fri-{{dashCase name}} [slot="icon-start"], + .fri-{{dashCase name}} [slot="icon-end"] { + @apply size-match-font; + } -.fri-{{dashCase name}}--success { - --fri-{{dashCase name}}-bg: var(--color-success); - --fri-{{dashCase name}}-bg-hover: var(--color-success-hover); - --fri-{{dashCase name}}-bg-pressed: var(--color-success-active); - --fri-{{dashCase name}}-fg: var(--color-success-foreground); -} + .fri-{{dashCase name}}--size-sm { + @apply gap-[calc(0.875rem/2.058)] px-[calc(0.875rem)] py-[calc(0.875rem/2.058)] text-sm; + } -.fri-{{dashCase name}}--warning { - --fri-{{dashCase name}}-bg: var(--color-warning); - --fri-{{dashCase name}}-bg-hover: var(--color-warning-hover); - --fri-{{dashCase name}}-bg-pressed: var(--color-warning-active); - --fri-{{dashCase name}}-fg: var(--color-warning-foreground); -} + .fri-{{dashCase name}}--size-md { + @apply gap-[calc(1rem/2.058)] px-[calc(1rem)] py-[calc(1rem/2.058)] text-base; + } -.fri-{{dashCase name}}--danger { - --fri-{{dashCase name}}-bg: var(--color-danger); - --fri-{{dashCase name}}-bg-hover: var(--color-danger-hover); - --fri-{{dashCase name}}-bg-pressed: var(--color-danger-active); - --fri-{{dashCase name}}-fg: var(--color-danger-foreground); -} + .fri-{{dashCase name}}--size-lg { + @apply gap-[calc(1.25rem/2.058)] px-[calc(1.25rem)] py-[calc(1.25rem/2.058)] text-xl; + } -.fri-{{dashCase name}}--info { - --fri-{{dashCase name}}-bg: var(--color-info); - --fri-{{dashCase name}}-bg-hover: var(--color-info-hover); - --fri-{{dashCase name}}-bg-pressed: var(--color-info-active); - --fri-{{dashCase name}}-fg: var(--color-info-foreground); -} + .fri-{{dashCase name}}--radius-none { + @apply rounded-none; + } -.fri-{{dashCase name}} [slot="icon"] { - @apply size-match-font; -} + .fri-{{dashCase name}}--radius-sm { + @apply rounded-[calc(1em*1.272/4)]; + } -.fri-{{dashCase name}}--size-sm { - @apply gap-2 px-3.5 py-1.5 text-sm; -} + .fri-{{dashCase name}}--radius-md { + @apply rounded-[calc(1em*1.272*2/4)]; + } -.fri-{{dashCase name}}--size-md { - @apply gap-2.5 px-4 py-2 text-base; -} + .fri-{{dashCase name}}--radius-lg { + @apply rounded-[calc(1em*1.272*3/4)]; + } -.fri-{{dashCase name}}--size-lg { - @apply gap-3 px-5 py-2.5 text-xl; + .fri-{{dashCase name}}--radius-full { + @apply rounded-full; + } } diff --git a/turbo/generators/interactive-component/component.stories.tsx.hbs b/turbo/generators/interactive-component/component.stories.tsx.hbs index 13f11d5..e0e351f 100644 --- a/turbo/generators/interactive-component/component.stories.tsx.hbs +++ b/turbo/generators/interactive-component/component.stories.tsx.hbs @@ -135,9 +135,9 @@ const Sizes: Story = { const Sizes = () => { return (
- <{{pascalCase name}} size="sm">Small - <{{pascalCase name}} size="md">Medium - <{{pascalCase name}} size="lg">Large + <{{pascalCase name}} size="sm">Small {{pascalCase name}} + <{{pascalCase name}} size="md">Medium {{pascalCase name}} + <{{pascalCase name}} size="lg">Large {{pascalCase name}}
); };`, @@ -146,9 +146,9 @@ const Sizes = () => { }, render: (args): React.JSX.Element => (
- <{{pascalCase name}} {...args} size="sm">Small - <{{pascalCase name}} {...args} size="md">Medium - <{{pascalCase name}} {...args} size="lg">Large + <{{pascalCase name}} {...args} size="sm">Small {{pascalCase name}} + <{{pascalCase name}} {...args} size="md">Medium {{pascalCase name}} + <{{pascalCase name}} {...args} size="lg">Large {{pascalCase name}}
), }; From 8f9dedd61afbf36161242d503f927dd4df5b02d6 Mon Sep 17 00:00:00 2001 From: Joe Takara Date: Tue, 21 Apr 2026 07:25:36 +0700 Subject: [PATCH 085/199] docs(react): Unify JSDoc pattern across all components Add component-level JSDoc (1-2 line description + slot contract for Button/Badge) and prop-level JSDoc (one-liner + @default) following React Aria + Mantine hybrid pattern. Standardize boolean props to "Whether the..." opening, drop implementation details from public API descriptions, and unify variant description perspective across Surface and Badge/Button. --- packages/react/src/components/avatar/Avatar.tsx | 12 ++++++++++++ .../react/src/components/avatar/avatar.variants.ts | 4 ++++ packages/react/src/components/badge/Badge.tsx | 4 ++++ .../react/src/components/badge/badge.variants.ts | 7 +++++++ packages/react/src/components/button/Button.tsx | 4 ++++ .../react/src/components/button/button.variants.ts | 8 ++++++++ .../react/src/components/description/Description.tsx | 4 ++++ .../components/description/description.variants.ts | 3 +++ packages/react/src/components/label/Label.tsx | 4 ++++ .../react/src/components/label/label.variants.ts | 6 ++++++ packages/react/src/components/surface/Surface.tsx | 4 ++++ .../react/src/components/surface/surface.variants.ts | 7 +++++++ 12 files changed, 67 insertions(+) diff --git a/packages/react/src/components/avatar/Avatar.tsx b/packages/react/src/components/avatar/Avatar.tsx index d952eb7..0495b7f 100644 --- a/packages/react/src/components/avatar/Avatar.tsx +++ b/packages/react/src/components/avatar/Avatar.tsx @@ -23,6 +23,10 @@ export type AvatarFallbackProps = Omit