diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 71f8a264..8e95a515 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -4,6 +4,7 @@ on: push: branches: - main + - alpha permissions: contents: write # to be able to publish a GitHub release diff --git a/.releaserc.json b/.releaserc.json index 0acfe82b..fcd57804 100644 --- a/.releaserc.json +++ b/.releaserc.json @@ -1,5 +1,5 @@ { - "branches": ["main"], + "branches": ["main", { "name": "alpha", "prerelease": true }], "plugins": [ "@semantic-release/commit-analyzer", "@semantic-release/release-notes-generator", diff --git a/.storybook/fonts.css b/.storybook/fonts.css new file mode 100644 index 00000000..8cd6127a --- /dev/null +++ b/.storybook/fonts.css @@ -0,0 +1,47 @@ +/* + * Custom Fonts for Storybook + * Fonts are loaded from .storybook/public/fonts/ + */ + +/* Diatype Font Family */ +@font-face { + font-family: "Diatype"; + src: url("/fonts/diatype/ABCDiatype-Regular.woff2") format("woff2"); + font-weight: 400; + font-style: normal; + font-display: block; +} + +@font-face { + font-family: "Diatype"; + src: url("/fonts/diatype/ABCDiatype-Light.woff2") format("woff2"); + font-weight: 300; + font-style: normal; + font-display: block; +} + +/* Diatype Mono Font Family */ +@font-face { + font-family: "Diatype Mono"; + src: url("/fonts/diatype-mono/ABCDiatypeMono-Regular.woff2") format("woff2"); + font-weight: 400; + font-style: normal; + font-display: block; +} + +@font-face { + font-family: "Diatype Mono"; + src: url("/fonts/diatype-mono/ABCDiatypeMono-Light.woff2") format("woff2"); + font-weight: 300; + font-style: normal; + font-display: block; +} + +/* Tobias Font Family */ +@font-face { + font-family: "Tobias"; + src: url("/fonts/tobias/Tobias-Thin.woff2") format("woff2"); + font-weight: 100; + font-style: normal; + font-display: block; +} \ No newline at end of file diff --git a/.storybook/main.ts b/.storybook/main.ts index ce35df8d..755380f4 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -13,6 +13,7 @@ const config: StorybookConfig = { name: '@storybook/react-vite', options: {}, }, + staticDirs: ['./public'], core: { builder: '@storybook/builder-vite', }, diff --git a/.storybook/manager.ts b/.storybook/manager.ts index eba56228..f9e96291 100644 --- a/.storybook/manager.ts +++ b/.storybook/manager.ts @@ -1,4 +1,3 @@ -import { addons } from '@storybook/manager-api' import { create } from '@storybook/theming' // Define the colors and styles for the theme @@ -36,6 +35,6 @@ export const speakeasyTheme = create({ brandUrl: 'https://speakeasy.com', }) -addons.setConfig({ - theme: speakeasyTheme, -}) +// addons.setConfig({ +// theme: speakeasyTheme, +// }) diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 6fe202ca..5df5983e 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -1,5 +1,6 @@ import type { Preview } from '@storybook/react' import '../src/global.css' +import './fonts.css' import React from 'react' import { allModes } from './modes' import { withThemeByClassName } from '@storybook/addon-themes' diff --git a/.storybook/public/fonts/diatype-mono/ABCDiatypeMono-Light.woff2 b/.storybook/public/fonts/diatype-mono/ABCDiatypeMono-Light.woff2 new file mode 100644 index 00000000..7dc0e8f7 Binary files /dev/null and b/.storybook/public/fonts/diatype-mono/ABCDiatypeMono-Light.woff2 differ diff --git a/.storybook/public/fonts/diatype-mono/ABCDiatypeMono-Regular.woff2 b/.storybook/public/fonts/diatype-mono/ABCDiatypeMono-Regular.woff2 new file mode 100644 index 00000000..2c9c5e8e Binary files /dev/null and b/.storybook/public/fonts/diatype-mono/ABCDiatypeMono-Regular.woff2 differ diff --git a/.storybook/public/fonts/diatype/ABCDiatype-Light.woff2 b/.storybook/public/fonts/diatype/ABCDiatype-Light.woff2 new file mode 100644 index 00000000..aa6bc4ad Binary files /dev/null and b/.storybook/public/fonts/diatype/ABCDiatype-Light.woff2 differ diff --git a/.storybook/public/fonts/diatype/ABCDiatype-Regular.woff2 b/.storybook/public/fonts/diatype/ABCDiatype-Regular.woff2 new file mode 100644 index 00000000..891068ff Binary files /dev/null and b/.storybook/public/fonts/diatype/ABCDiatype-Regular.woff2 differ diff --git a/.storybook/public/fonts/tobias/Tobias-Thin.woff2 b/.storybook/public/fonts/tobias/Tobias-Thin.woff2 new file mode 100644 index 00000000..ac6e7650 Binary files /dev/null and b/.storybook/public/fonts/tobias/Tobias-Thin.woff2 differ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..ad375e7f --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,1375 @@ +# Moonshine CSS Architecture - Technical Guide + +This document provides technical details about Moonshine's CSS architecture for AI assistants and developers working on the design system. + +## Overview + +Moonshine is a utility-first design system built on Tailwind CSS v4 that enforces design consistency through constrained, semantic utilities rather than arbitrary values. + +## File Structure + +``` +src/ +├── global.css # Entry point and orchestration +├── base.css # Design tokens and primitives +└── utilities.css # Public utility classes +``` + +## Architecture Details + +### base.css - Design Tokens + +This file contains four main sections: + +1. **Primitive Tokens** - Raw design values (should not be used directly) + ```css + --color-neutral-200: hsl(0, 0%, 92%); + --font-diatype: 'Diatype', -apple-system, ...; + ``` + +2. **Semantic/Utility Tokens** - Theme-aware variables that map to utilities + ```css + /* Light mode */ + --text-warning: var(--color-feedback-orange-700); + --bg-warning: var(--color-feedback-orange-100); + + /* Automatically switches in dark mode */ + ``` + +3. **Component Tokens** - Higher-level semantic tokens + ```css + --radius: 0.625rem; + --shadow: hsl(0 0% 50%); + ``` + +4. **Deprecated Tokens** - Shadcn compatibility (to be removed) + +### utilities.css - Public API + +Contains the utility classes that developers should use: + +- **Typography utilities**: `text-heading-xl`, `text-body-sm` (enforced combinations) +- **Semantic colors**: `bg-warning`, `text-error`, `border-success` +- **Surface utilities**: `bg-surface-primary`, `bg-surface-secondary` +- **Component utilities**: `container`, `bg-mask` + +### global.css - Configuration + +Handles Tailwind configuration and imports: + +- Imports core dependencies in correct order +- Defines custom variants (`dark`, `interact`) +- Configures responsive utility generation via `@source` +- Maps fonts to Tailwind's theme + +## Key Design Decisions + +### Why Custom Typography Utilities? + +Instead of allowing arbitrary combinations like: +```css +/* ❌ Bad - leads to inconsistency */ +.heading { + @apply text-[1.813rem] leading-[1.5] tracking-[0.0015em] font-light; +} +``` + +We provide semantic utilities: +```css +/* ✅ Good - enforces design system */ +.heading { + @apply text-heading-lg; +} +``` + +### Theme-Aware Semantic Tokens + +The "utility tokens" pattern in base.css enables automatic theme switching: + +```css +/* Developers use this */ +.warning-banner { + @apply bg-warning text-warning border-warning; +} + +/* It automatically adapts to light/dark mode */ +``` + +### Preventing Design System Escape Hatches + +We intentionally: +- Don't expose raw color values as utilities +- Provide complete typography utilities (not individual properties) +- Use semantic naming to guide correct usage + +## Common Tasks + +### Adding a New Color Utility + +1. Add primitive colors to base.css if needed +2. Create semantic tokens in base.css (both light and dark variants) +3. Create utility classes in utilities.css that reference the tokens + +### Adding a New Typography Scale + +1. Define the complete set of properties in a single `@utility` in utilities.css +2. Use semantic naming (e.g., `text-label-sm` not `text-13`) +3. Ensure it works with all font families + +### Extending Responsive Utilities + +Add new `@source` declarations in global.css: +```css +@source inline("{,sm:,md:,lg:,xl:,2xl:}your-utility-{value1,value2}"); +``` + +## Available Utility Classes + +This section is auto-generated from the CSS files. Last updated: 2025-07-16T17:53:27.862Z + +### Typography Utilities + +#### `typography-heading-xl` + +```css +.typography-heading-xl { + font-size: 2.063rem; + font-weight: 300; + line-height: 1.375; + letter-spacing: 0.0015em; + font-family: var(--font-diatype); +} +``` + +#### `typography-heading-lg` + +```css +.typography-heading-lg { + font-size: 1.813rem; + font-weight: 300; + line-height: 1.5; + letter-spacing: 0.0015em; + font-family: var(--font-diatype); +} +``` + +#### `typography-heading-md` + +```css +.typography-heading-md { + font-size: 1.625rem; + font-weight: 300; + line-height: 1.6; + letter-spacing: 0.0015em; + font-family: var(--font-diatype); +} +``` + +#### `typography-heading-sm` + +```css +.typography-heading-sm { + font-size: 1.438rem; + font-weight: 300; + line-height: 1.6; + letter-spacing: 0.0015em; + font-family: var(--font-diatype); +} +``` + +#### `typography-heading-xs` + +```css +.typography-heading-xs { + font-size: 1.25rem; + font-weight: 300; + line-height: 1.6; + letter-spacing: 0.0015em; + font-family: var(--font-diatype); +} +``` + +#### `typography-heading-xxs` + +```css +.typography-heading-xxs { + font-size: 1.125rem; + font-weight: 300; + line-height: 1.778; + letter-spacing: 0.0015em; + font-family: var(--font-diatype); +} +``` + +#### `typography-body-lg` + +```css +.typography-body-lg { + font-size: 1.125rem; + font-weight: 400; + line-height: 1.75; + letter-spacing: 0.0025em; + font-family: var(--font-diatype); +} +``` + +#### `typography-body-md` + +```css +.typography-body-md { + font-size: 1rem; + font-weight: 400; + line-height: 1.75; + letter-spacing: 0.0025em; + font-family: var(--font-diatype); +} +``` + +#### `typography-body-sm` + +```css +.typography-body-sm { + font-size: 0.875rem; + font-weight: 400; + line-height: 1.75; + letter-spacing: 0.0025em; + font-family: var(--font-diatype); +} +``` + +#### `typography-body-xs` + +```css +.typography-body-xs { + font-size: 0.75rem; + font-weight: 400; + line-height: 1.75; + letter-spacing: 0.0025em; + font-family: var(--font-diatype); +} +``` + +#### `text-display-2xl` + +```css +.text-display-2xl { + font-size: 11.375rem; + font-weight: 100; + line-height: 1; + letter-spacing: -0.04em; + font-family: var(--font-tobias); + color: var(--text-display); +} +``` + +#### `text-display-xl` + +```css +.text-display-xl { + font-size: 5.625rem; + font-weight: 100; + line-height: 1.1; + letter-spacing: -0.04em; + font-family: var(--font-tobias); + color: var(--text-display); +} +``` + +#### `text-display-lg` + +```css +.text-display-lg { + font-size: 4.188rem; + font-weight: 100; + line-height: 1.2; + letter-spacing: -0.04em; + font-family: var(--font-tobias); + color: var(--text-display); +} +``` + +#### `text-display-md` + +```css +.text-display-md { + font-size: 3.188rem; + font-weight: 100; + line-height: 1.3; + letter-spacing: -0.04em; + font-family: var(--font-tobias); + color: var(--text-display); +} +``` + +#### `text-display-sm` + +```css +.text-display-sm { + font-size: 2.375rem; + font-weight: 100; + line-height: 1.375; + letter-spacing: -0.04em; + font-family: var(--font-tobias); + color: var(--text-display); +} +``` + +#### `text-display-xs` + +```css +.text-display-xs { + font-size: 1.75rem; + font-weight: 100; + line-height: 1.4; + letter-spacing: -0.04em; + font-family: var(--font-tobias); + color: var(--text-display); +} +``` + +#### `text-heading-xl` + +```css +.text-heading-xl { + font-size: 2.063rem; + font-weight: 300; + line-height: 1.375; + letter-spacing: 0.0015em; + font-family: var(--font-diatype); + color: var(--text-heading-xl); +} +``` + +#### `text-heading-lg` + +```css +.text-heading-lg { + font-size: 1.813rem; + font-weight: 300; + line-height: 1.5; + letter-spacing: 0.0015em; + font-family: var(--font-diatype); + color: var(--text-heading-lg); +} +``` + +#### `text-heading-md` + +```css +.text-heading-md { + font-size: 1.625rem; + font-weight: 300; + line-height: 1.6; + letter-spacing: 0.0015em; + font-family: var(--font-diatype); + color: var(--text-heading-md); +} +``` + +#### `text-heading-sm` + +```css +.text-heading-sm { + font-size: 1.438rem; + font-weight: 300; + line-height: 1.6; + letter-spacing: 0.0015em; + font-family: var(--font-diatype); + color: var(--text-heading-sm); +} +``` + +#### `text-heading-xs` + +```css +.text-heading-xs { + font-size: 1.25rem; + font-weight: 300; + line-height: 1.6; + letter-spacing: 0.0015em; + font-family: var(--font-diatype); + color: var(--text-heading-xs); +} +``` + +#### `text-body-lg` + +```css +.text-body-lg { + font-size: 1.125rem; + font-weight: 400; + line-height: 1.75; + letter-spacing: 0.0025em; + font-family: var(--font-diatype); + color: var(--text-default); +} +``` + +#### `text-body-md` + +```css +.text-body-md { + font-size: 1rem; + font-weight: 400; + line-height: 1.75; + letter-spacing: 0.0025em; + font-family: var(--font-diatype); + color: var(--text-default); +} +``` + +#### `text-body-sm` + +```css +.text-body-sm { + font-size: 0.875rem; + font-weight: 400; + line-height: 1.75; + letter-spacing: 0.0025em; + font-family: var(--font-diatype); +} +``` + +#### `text-body-xs` + +```css +.text-body-xs { + font-size: 0.75rem; + font-weight: 400; + line-height: 1.75; + letter-spacing: 0.0025em; + font-family: var(--font-diatype); + color: var(--text-default); +} +``` + +#### `text-codeline-md` + +```css +.text-codeline-md { + font-size: 1rem; + font-weight: 300; + line-height: 1.75; + letter-spacing: 0.0025em; + font-family: var(--font-diatype-mono); + color: var(--text-default); +} +``` + +#### `text-codeline-sm` + +```css +.text-codeline-sm { + font-size: 0.875rem; + font-weight: 300; + line-height: 1.75; + letter-spacing: 0.0025em; + font-family: var(--font-diatype-mono); + color: var(--text-default); +} +``` + +#### `text-codeline-xs` + +```css +.text-codeline-xs { + font-size: 0.75rem; + font-weight: 300; + line-height: 1.7; + letter-spacing: 0.0025em; + font-family: var(--font-diatype-mono); + color: var(--text-default); +} +``` + +#### `text-hightlight` + +```css +.text-hightlight { + color: var(--text-highlight); +} +``` + +#### `text-text-default` + +```css +.text-text-default { + color: var(--text-default); +} +``` + +#### `text-muted` + +```css +.text-muted { + color: var(--text-muted); +} +``` + +#### `text-placeholder` + +```css +.text-placeholder { + color: var(--text-placeholder); +} +``` + +#### `text-disabled` + +```css +.text-disabled { + color: var(--text-disabled); +} +``` + +#### `text-dark-light-highlight` + +```css +.text-dark-light-highlight { + color: var(--text-dark-light-highlight); +} +``` + +#### `text-dark-light-default` + +```css +.text-dark-light-default { + color: var(--text-dark-light-default); +} +``` + +#### `text-dark-light-muted` + +```css +.text-dark-light-muted { + color: var(--text-dark-light-muted); +} +``` + +#### `text-light-dark-highlight` + +```css +.text-light-dark-highlight { + color: var(--text-light-dark-highlight); +} +``` + +#### `text-light-dark-default` + +```css +.text-light-dark-default { + color: var(--text-light-dark-default); +} +``` + +#### `text-light-dark-muted` + +```css +.text-light-dark-muted { + color: var(--text-light-dark-muted); +} +``` + +#### `text-link-primary` + +```css +.text-link-primary { + color: var(--text-link-primary); +} +``` + +#### `text-link-secondary` + +```css +.text-link-secondary { + color: var(--text-link-secondary); +} +``` + +#### `text-link-visited` + +```css +.text-link-visited { + color: var(--text-link-visited); +} +``` + +#### `text-default-destructive` + +```css +.text-default-destructive { + color: var(--text-default-destructive); +} +``` + +#### `text-link-destructive` + +```css +.text-link-destructive { + color: var(--text-link-destructive); +} +``` + +#### `text-default-information` + +```css +.text-default-information { + color: var(--text-default-information); +} +``` + +#### `text-link-information` + +```css +.text-link-information { + color: var(--text-link-information); +} +``` + +#### `text-default-success` + +```css +.text-default-success { + color: var(--text-default-success); +} +``` + +#### `text-link-success` + +```css +.text-link-success { + color: var(--text-link-success); +} +``` + +#### `text-default-warning` + +```css +.text-default-warning { + color: var(--text-default-warning); +} +``` + +#### `text-link-warning` + +```css +.text-link-warning { + color: var(--text-link-warning); +} +``` + +#### `text-warning` + +```css +.text-warning { + color: var(--text-warning); +} +``` + +### Background Utilities + +#### `bg-mask` + +```css +.bg-mask { + background-image: linear-gradient( + to bottom, + var(--background) 0%, + var(--color-transparent) 5%, + var(--color-transparent) 95%, + var(--background) 100% + ); +} +``` + +#### `bg-gradient-primary` + +```css +.bg-gradient-primary { + background: var(--gradient-brand-primary); +} +``` + +#### `bg-surface-primary` + +```css +.bg-surface-primary { + background-color: var(--color-base-white); + + /* dark variant */ + background-color: var(--color-base-black); +} +``` + +#### `bg-surface-secondary` + +```css +.bg-surface-secondary { + background-color: var(--color-neutral-100); +} +``` + +#### `bg-warning` + +```css +.bg-warning { + background-color: var(--bg-warning); +} +``` + +#### `bg-highlight` + +```css +.bg-highlight { + background-color: var(--bg-highlight); +} +``` + +#### `bg-active` + +```css +.bg-active { + background-color: var(--bg-active); +} +``` + +#### `bg-default` + +```css +.bg-default { + background-color: var(--bg-default); +} +``` + +#### `bg-muted` + +```css +.bg-muted { + background-color: var(--bg-muted); +} +``` + +#### `bg-inset` + +```css +.bg-inset { + background-color: var(--bg-inset); +} +``` + +#### `bg-light-dark-primary` + +```css +.bg-light-dark-primary { + background-color: var(--bg-light-dark-primary); +} +``` + +#### `bg-light-dark-secondary` + +```css +.bg-light-dark-secondary { + background-color: var(--bg-light-dark-secondary); +} +``` + +#### `bg-light-dark-tertiary` + +```css +.bg-light-dark-tertiary { + background-color: var(--bg-light-dark-tertiary); +} +``` + +#### `bg-dark-light-primary` + +```css +.bg-dark-light-primary { + background-color: var(--bg-dark-light-primary); +} +``` + +#### `bg-dark-light-secondary` + +```css +.bg-dark-light-secondary { + background-color: var(--bg-dark-light-secondary); +} +``` + +#### `bg-dark-light-tertiary` + +```css +.bg-dark-light-tertiary { + background-color: var(--bg-dark-light-tertiary); +} +``` + +#### `bg-destructive-highlight` + +```css +.bg-destructive-highlight { + background-color: var(--bg-destructive-highlight); +} +``` + +#### `bg-destructive-default` + +```css +.bg-destructive-default { + background-color: var(--bg-destructive-default); +} +``` + +#### `bg-destructive-muted` + +```css +.bg-destructive-muted { + background-color: var(--bg-destructive-muted); +} +``` + +#### `bg-destructive-softest` + +```css +.bg-destructive-softest { + background-color: var(--bg-destructive-softest); +} +``` + +#### `bg-information-highlight` + +```css +.bg-information-highlight { + background-color: var(--bg-information-highlight); +} +``` + +#### `bg-information-default` + +```css +.bg-information-default { + background-color: var(--bg-information-default); +} +``` + +#### `bg-information-muted` + +```css +.bg-information-muted { + background-color: var(--bg-information-muted); +} +``` + +#### `bg-information-softest` + +```css +.bg-information-softest { + background-color: var(--bg-information-softest); +} +``` + +#### `bg-success-highlight` + +```css +.bg-success-highlight { + background-color: var(--bg-success-highlight); +} +``` + +#### `bg-success-default` + +```css +.bg-success-default { + background-color: var(--bg-success-default); +} +``` + +#### `bg-success-muted` + +```css +.bg-success-muted { + background-color: var(--bg-success-muted); +} +``` + +#### `bg-success-softest` + +```css +.bg-success-softest { + background-color: var(--bg-success-softest); +} +``` + +#### `bg-warning-highlight` + +```css +.bg-warning-highlight { + background-color: var(--bg-warning-highlight); +} +``` + +#### `bg-warning-default` + +```css +.bg-warning-default { + background-color: var(--bg-warning-default); +} +``` + +#### `bg-warning-muted` + +```css +.bg-warning-muted { + background-color: var(--bg-warning-muted); +} +``` + +#### `bg-warning-softest` + +```css +.bg-warning-softest { + background-color: var(--bg-warning-softest); +} +``` + +### Border Utilities + +#### `border-gradient-primary` + +```css +.border-gradient-primary { + border-image: var(--gradient-brand-primary) 1; +} +``` + +#### `border-warning` + +```css +.border-warning { + border-color: var(--border-warning); + border-width: 1px; + border-style: solid; +} +``` + +#### `border-neutral-active` + +```css +.border-neutral-active { + border-color: var(--border-neutral-active); +} +``` + +#### `border-neutral-hover` + +```css +.border-neutral-hover { + border-color: var(--border-neutral-hover); +} +``` + +#### `border-neutral-default` + +```css +.border-neutral-default { + border-color: var(--border-neutral-default); +} +``` + +#### `border-neutral-disabled` + +```css +.border-neutral-disabled { + border-color: var(--border-neutral-disabled); + opacity: 0.4; +} +``` + +#### `border-neutral-softest` + +```css +.border-neutral-softest { + border-color: var(--border-neutral-softest); +} +``` + +#### `border-neutral-inset` + +```css +.border-neutral-inset { + border-color: var(--border-neutral-inset); +} +``` + +#### `border-neutral-alpha` + +```css +.border-neutral-alpha { + border-color: var(--border-neutral-alpha); +} +``` + +#### `border-destructive-highlight` + +```css +.border-destructive-highlight { + border-color: var(--border-destructive-highlight); +} +``` + +#### `border-destructive-default` + +```css +.border-destructive-default { + border-color: var(--border-destructive-default); +} +``` + +#### `border-destructive-muted` + +```css +.border-destructive-muted { + border-color: var(--border-destructive-muted); +} +``` + +#### `border-destructive-softest` + +```css +.border-destructive-softest { + border-color: var(--border-destructive-softest); +} +``` + +#### `border-information-highlight` + +```css +.border-information-highlight { + border-color: var(--border-information-highlight); +} +``` + +#### `border-information-default` + +```css +.border-information-default { + border-color: var(--border-information-default); +} +``` + +#### `border-information-muted` + +```css +.border-information-muted { + border-color: var(--border-information-muted); +} +``` + +#### `border-information-softest` + +```css +.border-information-softest { + border-color: var(--border-information-softest); +} +``` + +#### `border-success-highlight` + +```css +.border-success-highlight { + border-color: var(--border-success-highlight); +} +``` + +#### `border-success-default` + +```css +.border-success-default { + border-color: var(--border-success-default); +} +``` + +#### `border-success-muted` + +```css +.border-success-muted { + border-color: var(--border-success-muted); +} +``` + +#### `border-success-softest` + +```css +.border-success-softest { + border-color: var(--border-success-softest); +} +``` + +#### `border-focus` + +```css +.border-focus { + border-color: var(--border-focus); +} +``` + +### Other Utilities + +#### `container` + +```css +.container { + padding-inline: 1rem; + margin-inline: auto; +} +``` + +#### `underline-link-primary` + +```css +.underline-link-primary { + color: var(--underline-link-primary); + text-decoration: underline; + text-underline-offset: 0.125rem; +} +``` + +#### `underline-link-secondary` + +```css +.underline-link-secondary { + color: var(--underline-link-secondary); + text-decoration: underline; + text-underline-offset: 0.125rem; +} +``` + +#### `underline-link-visited` + +```css +.underline-link-visited { + color: var(--underline-link-visited); + text-decoration: underline; + text-underline-offset: 0.125rem; +} +``` + +#### `fill-neutral-highlight` + +```css +.fill-neutral-highlight { + fill: var(--fill-neutral-highlight); +} +``` + +#### `fill-neutral-active` + +```css +.fill-neutral-active { + fill: var(--fill-neutral-active); +} +``` + +#### `fill-neutral-default` + +```css +.fill-neutral-default { + fill: var(--fill-neutral-default); +} +``` + +#### `fill-neutral-muted` + +```css +.fill-neutral-muted { + fill: var(--fill-neutral-muted); +} +``` + +#### `fill-onColor-dark-highlight` + +```css +.fill-onColor-dark-highlight { + fill: var(--fill-onColor-dark-highlight); +} +``` + +#### `fill-onColor-dark-default` + +```css +.fill-onColor-dark-default { + fill: var(--fill-onColor-dark-default); +} +``` + +#### `fill-onColor-dark-muted` + +```css +.fill-onColor-dark-muted { + fill: var(--fill-onColor-dark-muted); +} +``` + +#### `fill-onColor-light-highlight` + +```css +.fill-onColor-light-highlight { + fill: var(--fill-onColor-light-highlight); +} +``` + +#### `fill-onColor-light-default` + +```css +.fill-onColor-light-default { + fill: var(--fill-onColor-light-default); +} +``` + +#### `fill-onColor-light-muted` + +```css +.fill-onColor-light-muted { + fill: var(--fill-onColor-light-muted); +} +``` + +#### `fill-link-primary` + +```css +.fill-link-primary { + fill: var(--fill-link-primary); +} +``` + +#### `fill-link-secondary` + +```css +.fill-link-secondary { + fill: var(--fill-link-secondary); +} +``` + +#### `fill-link-visited` + +```css +.fill-link-visited { + fill: var(--fill-link-visited); +} +``` + +#### `fill-destructive-highlight` + +```css +.fill-destructive-highlight { + fill: var(--fill-destructive-highlight); +} +``` + +#### `fill-destructive-default` + +```css +.fill-destructive-default { + fill: var(--fill-destructive-default); +} +``` + +#### `fill-destructive-muted` + +```css +.fill-destructive-muted { + fill: var(--fill-destructive-muted); +} +``` + +#### `fill-information-highlight` + +```css +.fill-information-highlight { + fill: var(--fill-information-highlight); +} +``` + +#### `fill-information-default` + +```css +.fill-information-default { + fill: var(--fill-information-default); +} +``` + +#### `fill-information-muted` + +```css +.fill-information-muted { + fill: var(--fill-information-muted); +} +``` + +#### `fill-success-highlight` + +```css +.fill-success-highlight { + fill: var(--fill-success-highlight); +} +``` + +#### `fill-success-default` + +```css +.fill-success-default { + fill: var(--fill-success-default); +} +``` + +#### `fill-success-muted` + +```css +.fill-success-muted { + fill: var(--fill-success-muted); +} +``` + +#### `fill-warning-highlight` + +```css +.fill-warning-highlight { + fill: var(--fill-warning-highlight); +} +``` + +#### `fill-warning-default` + +```css +.fill-warning-default { + fill: var(--fill-warning-default); +} +``` + +#### `fill-warning-muted` + +```css +.fill-warning-muted { + fill: var(--fill-warning-muted); +} +``` + +## Migration Notes + +- **Shadcn components**: Currently using deprecated tokens, will need updates +- **Typography consolidation**: `typography-*` utilities should be removed in favor of `text-*` +- **Raw color usage**: Audit and replace any `var(--color-neutral-*)` usage with semantic utilities + +## Performance Considerations + +- Use `@source inline` for utilities that need responsive variants +- Avoid creating too many custom `@utility` declarations +- Leverage Tailwind's built-in utilities where they align with the design system + +## Future Improvements + +1. **Type generation**: Generate TypeScript types for all utilities +2. **Custom properties API**: Expose some CSS variables for component libraries +3. **Animation utilities**: Add semantic animation classes +4. **Layout utilities**: More sophisticated grid/flexbox patterns diff --git a/README.md b/README.md index c4b191fa..588a98f4 100644 --- a/README.md +++ b/README.md @@ -4,19 +4,40 @@ Speakeasy's design system. -## Installing the NPM package +## Installation & Setup + +### 1. Install the package and dependencies ```bash pnpm add @speakeasy-api/moonshine +pnpm add -D tw-animate-css @tailwindcss/typography ``` -Reference the CSS file in your project: +### 2. Configure Tailwind CSS + +Add this to the top of your project's CSS file where you configure Tailwind: + +```css +/* This must come BEFORE your Tailwind imports */ +@reference "../node_modules/@speakeasy-api/moonshine/src/global.css"; + +/* Your Tailwind setup */ +@import 'tailwindcss'; +``` + +**Note:** The `@reference` directive is required for Tailwind v4 to recognize Moonshine's custom utilities and make them available in your project. + +### 3. Import Moonshine's Compiled CSS + +In your main app file (or root layout): ```ts import '@speakeasy-api/moonshine/moonshine.css' ``` -Wrap your application in the `MoonshineConfigProvider` component, passing in the HTML element where the tailwind dark/light class is applied: +### 4. Set up the Provider + +Wrap your application in the `MoonshineConfigProvider` component: ```tsx import { MoonshineConfigProvider } from '@speakeasy-api/moonshine' @@ -26,14 +47,149 @@ import { MoonshineConfigProvider } from '@speakeasy-api/moonshine' ``` -Then you can import components from the package: +### 5. Configure Custom Fonts (Optional) + +Moonshine uses custom fonts (Diatype, Tobias). If you have licenses for these fonts, add them to your project: + +```css +/* In your global CSS */ +@font-face { + font-family: "Diatype"; + src: url("/fonts/diatype/ABCDiatype-Regular.woff2") format("woff2"); + font-weight: 400; + font-style: normal; + font-display: block; +} + +@font-face { + font-family: "Diatype"; + src: url("/fonts/diatype/ABCDiatype-Light.woff2") format("woff2"); + font-weight: 300; + font-style: normal; + font-display: block; +} + +/* Add other font weights and Tobias font-face declarations as needed */ +``` + +If you don't have these fonts, the design system will fall back to system fonts. + +### 6. Use Components and Utilities ```tsx import { Grid } from '@speakeasy-api/moonshine' + +// Use semantic utility classes +
+ Hello Moonshine! +
``` +### TypeScript Support for Utility Classes + +Moonshine provides TypeScript types for all available utility classes to improve your development experience: + +```tsx +import type { MoonshineClasses } from '@speakeasy-api/moonshine/types/utilities' + +// Use for type-safe className props +interface MyComponentProps { + className?: MoonshineClasses +} + +// Get autocomplete for all available utilities +const styles: MoonshineClasses = 'text-heading-lg' // ✅ Autocompletes! +``` + +The types are automatically generated during the build process and include: +- All custom utilities (`text-heading-xl`, `bg-surface-primary`, etc.) +- All semantic color utilities (`bg-warning`, `text-success`, etc.) +- Full IntelliSense support in your IDE + +💡 **Tip**: This prevents typos and helps you discover available utilities without leaving your editor! + The package is built with [vite](https://vitejs.dev/), and is distributed in both [ESM](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules) and [CommonJS](https://nodejs.org/api/modules.html#modules-commonjs) formats. +### Using Tailwind Merge + +Moonshine exports a custom wrapper for Tailwind Merge to avoid unexpected clashes between semantic class names. This version should be used in favour of using tailwind merge directly. + +```tsx +import { cn } from '@speakeasy-api/moonshine' + +return ( + + Lorem Ipsum + +) + +``` + + + +## Design System Architecture + +Moonshine is a utility-first design system built on top of [Tailwind CSS v4](https://tailwindcss.com/). It provides a curated set of design tokens and utilities that enforce consistency while preventing common pitfalls. + +### CSS Architecture + +Our CSS is organized into three main files: + +1. **`base.css`** - Primitive design tokens (colors, fonts, spacing scales) + - Contains raw values that should **not** be used directly in components + - Defines theme-aware semantic tokens that adapt to light/dark mode + - Houses base element styles and resets + +2. **`utilities.css`** - The public API of our design system + - Exposes carefully crafted utility classes like `text-heading-xl`, `bg-surface-primary` + - Enforces typography combinations to prevent arbitrary text styling + - Provides semantic color utilities that automatically handle theming + +3. **`global.css`** - Orchestration and configuration + - Imports Tailwind and configures plugins + - Defines custom variants (dark mode, interaction states) + - Sets up responsive utility generation + +### Design Principles + +- **Constrained, not restrictive**: We provide a curated set of utilities that make the right thing easy +- **Semantic, not arbitrary**: Use `text-heading-lg` not `text-[29px] leading-[1.5]` +- **Theme-aware by default**: Colors and styles automatically adapt to light/dark mode +- **Type-safe when possible**: Utilities are designed to work with TypeScript autocomplete + +### Usage Guidelines + +✅ **Do:** +- Use semantic utilities: `bg-warning`, `text-body`, `border-error` +- Leverage pre-defined typography scales: `text-heading-xl`, `text-body-sm` +- Stick to the exposed utility classes in `utilities.css` + +❌ **Don't:** +- Access raw color values: `bg-[var(--color-neutral-200)]` +- Create arbitrary combinations: `text-[1.813rem] leading-[1.5]` +- Override the design system without discussing with the team + +For more technical details about the CSS architecture, see [CLAUDE.md](./CLAUDE.md). + +## Troubleshooting + +### Utilities not working +Make sure you've added the `@reference` directive to your global CSS file. This is required for Tailwind v4 to pick up Moonshine's utility classes. + +### Fonts not loading +The custom fonts (Diatype, Tobias) require licenses. If you don't have them, the system will use fallback fonts. Ensure your font files are in the correct path if you do have licenses. + +### Dark mode not working +Ensure the `themeElement` prop in `MoonshineConfigProvider` points to the element where your `dark` class is applied (usually `document.documentElement`). + +### TypeScript types not found +Make sure you're importing from the correct path: +```tsx +import type { MoonshineClasses } from '@speakeasy-api/moonshine/types/utilities' +``` + ## Contributing ### Setup diff --git a/package.json b/package.json index 5b93c233..54729906 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,10 @@ "./moonshine.css": { "require": "./dist/style.css", "import": "./dist/style.css" + }, + "./src/global.css": { + "require": "./src/global.css", + "import": "./src/global.css" } }, "repository": { @@ -22,7 +26,11 @@ "url": "https://github.com/speakeasy-api/moonshine.git" }, "files": [ - "dist" + "dist", + "src/global.css", + "src/base.css", + "src/utilities.css", + "types" ], "scripts": { "clean": "rimraf dist", @@ -36,7 +44,9 @@ "lint": "eslint", "lint:fix": "eslint --fix", "test": "vitest", - "visualize": "npx vite-bundle-visualizer" + "visualize": "npx vite-bundle-visualizer", + "generate:docs": "node scripts/generate-utility-docs.js", + "postbuild": "pnpm generate:docs" }, "author": "Speakeasy", "license": "ISC", diff --git a/scripts/generate-utility-docs.js b/scripts/generate-utility-docs.js new file mode 100644 index 00000000..1d75be52 --- /dev/null +++ b/scripts/generate-utility-docs.js @@ -0,0 +1,324 @@ +#!/usr/bin/env node + +const fs = require('fs') +const path = require('path') +const postcss = require('postcss') + +// Paths +const UTILITIES_CSS = path.join(__dirname, '../src/utilities.css') +const BASE_CSS = path.join(__dirname, '../src/base.css') +const CLAUDE_MD = path.join(__dirname, '../CLAUDE.md') +const TYPES_DIR = path.join(__dirname, '../types') +const UTILITIES_TYPES = path.join(TYPES_DIR, 'utilities.d.ts') + +// Ensure types directory exists +if (!fs.existsSync(TYPES_DIR)) { + fs.mkdirSync(TYPES_DIR) +} + +// Parse CSS and extract utilities +async function extractUtilities(cssPath) { + if (!fs.existsSync(cssPath)) { + console.error(`CSS file not found: ${cssPath}`) + return [] + } + + const css = fs.readFileSync(cssPath, 'utf8') + + let ast + try { + ast = postcss.parse(css) + } catch (error) { + console.error(`Error parsing CSS file ${cssPath}:`, error.message) + return [] + } + + const utilities = [] + + ast.walkAtRules('utility', (rule) => { + const name = rule.params.trim() + + // Extract the CSS properties + const properties = [] + rule.walkDecls((decl) => { + // Skip nested rules (like @variant) + if (decl.parent === rule) { + properties.push(` ${decl.prop}: ${decl.value};`) + } + }) + + // Check for variant rules (like dark mode) + const variants = [] + rule.walkAtRules('variant', (variantRule) => { + const variantProps = [] + variantRule.walkDecls((decl) => { + variantProps.push(` ${decl.prop}: ${decl.value};`) + }) + variants.push({ + name: variantRule.params.trim(), + properties: variantProps, + }) + }) + + utilities.push({ + name, + properties, + variants, + raw: rule.toString(), + }) + }) + + return utilities +} + +// Extract theme colors that are exposed as utilities +async function extractThemeColors(cssPath) { + const css = fs.readFileSync(cssPath, 'utf8') + const ast = postcss.parse(css) + const colors = new Set() + + ast.walkAtRules('theme', (rule) => { + rule.walkDecls((decl) => { + if (decl.prop.startsWith('--color-') && !decl.prop.includes('*')) { + const colorName = decl.prop.replace('--color-', '') + // Only include semantic colors, not raw values + if ( + !colorName.match( + /^(brand|neutral|destructive|warning|success|info)-\d+$/ + ) + ) { + colors.add(colorName) + } + } + }) + }) + + return Array.from(colors) +} + +// Generate TypeScript definitions +function generateTypeDefinitions(utilities, themeColors) { + const utilityTypes = utilities.map((u) => ` | '${u.name}'`).join('\n') + + // Generate color utility types + const colorPrefixes = ['bg', 'text', 'border', 'ring', 'divide'] + const colorUtilities = themeColors + .flatMap((color) => + colorPrefixes.map((prefix) => ` | '${prefix}-${color}'`) + ) + .join('\n') + + return `/** + * Auto-generated TypeScript definitions for Moonshine utilities + * Generated on: ${new Date().toISOString()} + * + * DO NOT EDIT THIS FILE DIRECTLY + * Run 'pnpm generate:docs' to update + */ + +/** + * Custom utility classes defined in utilities.css + */ +export type MoonshineUtilities = +${utilityTypes}; + +/** + * Semantic color utilities available from theme + * These are auto-generated from theme colors with standard prefixes + */ +export type MoonshineColorUtilities = +${colorUtilities}; + +/** + * All available Moonshine utility classes + */ +export type MoonshineClasses = MoonshineUtilities | MoonshineColorUtilities; + +/** + * Helper type for className props + */ +export type MoonshineClassName = MoonshineClasses | MoonshineClasses[] | undefined | null | false; +` +} + +// Generate documentation section for CLAUDE.md +function generateDocSection(utilities) { + const sections = [] + + // Group utilities by type + const typography = utilities.filter( + (u) => u.name.startsWith('text-') || u.name.startsWith('typography-') + ) + const backgrounds = utilities.filter((u) => u.name.startsWith('bg-')) + const borders = utilities.filter((u) => u.name.startsWith('border-')) + const other = utilities.filter( + (u) => + !u.name.startsWith('text-') && + !u.name.startsWith('typography-') && + !u.name.startsWith('bg-') && + !u.name.startsWith('border-') + ) + + sections.push('## Available Utility Classes\n') + sections.push( + 'This section is auto-generated from the CSS files. Last updated: ' + + new Date().toISOString() + + '\n' + ) + + // Typography + if (typography.length > 0) { + sections.push('### Typography Utilities\n') + typography.forEach((u) => { + sections.push(`#### \`${u.name}\`\n`) + sections.push('```css') + sections.push(`.${u.name} {`) + sections.push(u.properties.join('\n')) + + if (u.variants.length > 0) { + u.variants.forEach((v) => { + sections.push(`\n /* ${v.name} variant */`) + sections.push(v.properties.join('\n')) + }) + } + + sections.push('}') + sections.push('```\n') + }) + } + + // Backgrounds + if (backgrounds.length > 0) { + sections.push('### Background Utilities\n') + backgrounds.forEach((u) => { + sections.push(`#### \`${u.name}\`\n`) + sections.push('```css') + sections.push(`.${u.name} {`) + sections.push(u.properties.join('\n')) + + if (u.variants.length > 0) { + u.variants.forEach((v) => { + sections.push(`\n /* ${v.name} variant */`) + sections.push(v.properties.join('\n')) + }) + } + + sections.push('}') + sections.push('```\n') + }) + } + + // Borders + if (borders.length > 0) { + sections.push('### Border Utilities\n') + borders.forEach((u) => { + sections.push(`#### \`${u.name}\`\n`) + sections.push('```css') + sections.push(`.${u.name} {`) + sections.push(u.properties.join('\n')) + sections.push('}') + sections.push('```\n') + }) + } + + // Other + if (other.length > 0) { + sections.push('### Other Utilities\n') + other.forEach((u) => { + sections.push(`#### \`${u.name}\`\n`) + sections.push('```css') + sections.push(`.${u.name} {`) + sections.push(u.properties.join('\n')) + sections.push('}') + sections.push('```\n') + }) + } + + return sections.join('\n') +} + +// Update CLAUDE.md with generated content +function updateClaudeMd(newContent) { + const claudeMd = fs.readFileSync(CLAUDE_MD, 'utf8') + + // Find the section to replace + const startMarker = '## Available Utility Classes' + const endMarker = '## Migration Notes' + + const startIndex = claudeMd.indexOf(startMarker) + let endIndex = claudeMd.indexOf(endMarker) + + // If end marker not found, look for the next ## heading + if (endIndex === -1) { + const afterStart = claudeMd.substring(startIndex + startMarker.length) + const nextHeadingMatch = afterStart.match(/\n## /) + if (nextHeadingMatch) { + endIndex = startIndex + startMarker.length + nextHeadingMatch.index + 1 + } + } + + if (startIndex === -1) { + console.error( + 'Could not find "## Available Utility Classes" section in CLAUDE.md' + ) + console.error( + 'Please ensure CLAUDE.md contains this section for auto-generation to work' + ) + return + } + + if (endIndex === -1 || endIndex <= startIndex) { + // No end marker found, append to end of file + const updated = claudeMd + '\n\n' + newContent + fs.writeFileSync(CLAUDE_MD, updated, 'utf8') + console.log(' Appended to end of CLAUDE.md (no end marker found)') + } else { + const before = claudeMd.substring(0, startIndex) + const after = claudeMd.substring(endIndex) + const updated = before + newContent + '\n' + after + fs.writeFileSync(CLAUDE_MD, updated, 'utf8') + } +} + +// Main function +async function main() { + console.log('🎨 Generating Moonshine utility documentation...\n') + + try { + // Extract utilities + console.log('📖 Reading utilities.css...') + const utilities = await extractUtilities(UTILITIES_CSS) + console.log(` Found ${utilities.length} utility classes`) + + // Extract theme colors + console.log('🎨 Reading theme colors from utilities.css...') + const themeColors = await extractThemeColors(UTILITIES_CSS) + console.log(` Found ${themeColors.length} theme colors`) + + // Generate TypeScript definitions + console.log('\n📝 Generating TypeScript definitions...') + const typeDefs = generateTypeDefinitions(utilities, themeColors) + fs.writeFileSync(UTILITIES_TYPES, typeDefs, 'utf8') + console.log( + ` Written to ${path.relative(process.cwd(), UTILITIES_TYPES)}` + ) + + // Generate documentation + console.log('\n📚 Generating documentation...') + const docSection = generateDocSection(utilities) + updateClaudeMd(docSection) + console.log(` Updated ${path.relative(process.cwd(), CLAUDE_MD)}`) + + console.log('\n✅ Documentation generation complete!') + console.log('\n💡 Tip: Import the types in your components:') + console.log( + ' import type { MoonshineClasses } from "@speakeasy-api/moonshine/types/utilities"' + ) + } catch (error) { + console.error('\n❌ Error generating documentation:', error) + process.exit(1) + } +} + +// Run the script +main() diff --git a/src/base.css b/src/base.css new file mode 100644 index 00000000..2f5b0e67 --- /dev/null +++ b/src/base.css @@ -0,0 +1,914 @@ +/* + * Base CSS - Design Tokens and CSS Variables + * + * ⚠️ WARNING: DO NOT IMPORT THIS FILE DIRECTLY IN YOUR PROJECT! + * This file is only exposed for Tailwind's @reference directive to work. + * Use the utility classes from utilities.css instead of these raw tokens. + * + * Structure: + * 1. Primitive Tokens - Raw values (colors, fonts, etc.) + * 2. Semantic/Utility Tokens - Theme-aware variables that map to utilities + * 3. Component Tokens - Higher-level semantic tokens + * 4. Deprecated Tokens - Shadcn compatibility (to be removed) + */ + +:root, +:root.light { + /* ============================================ + * 1. PRIMITIVE TOKENS + * Raw design values - do not use directly in components + * ============================================ */ + + /* Font Families */ + --font-diatype: 'Diatype', -apple-system, BlinkMacSystemFont, 'Segoe UI', + Roboto, Helvetica, Arial, sans-serif; + --font-diatype-mono: 'Diatype Mono', SFMono-Regular, Menlo, Monaco, Consolas, + 'Liberation Mono', 'Courier New', monospace; + --font-tobias: 'Tobias', 'Cormorant', Georgia, Cambria, 'Times New Roman', + Times, serif; + + --color-base-white: hsl(0 0% 100%); + --color-base-black: hsl(0 0% 0%); + + /* Neutrals */ + --color-neutral-100: hsl(0, 0%, 98%); + --color-neutral-200: hsl(0, 0%, 92%); + --color-neutral-300: hsl(0, 0%, 86%); + --color-neutral-400: hsl(0, 0%, 73%); + --color-neutral-500: hsl(0, 0%, 59%); + --color-neutral-600: hsl(0, 0%, 46%); + --color-neutral-700: hsl(0, 0%, 33%); + --color-neutral-800: hsl(0, 0%, 20%); + --color-neutral-900: hsl(0, 0%, 7%); + + --color-neutral-100-40: hsl(from var(--color-neutral-100) h s l / 40%); + --color-neutral-100-56: hsl(from var(--color-neutral-100) h s l / 56%); + --color-neutral-100-64: hsl(from var(--color-neutral-100) h s l / 64%); + + --color-neutral-200-56: hsl(from var(--color-neutral-200) h s l / 56%); + + --color-neutral-400-40: hsl(from var(--color-neutral-400) h s l / 40%); + + --color-neutral-600-40: hsl(from var(--color-neutral-600) h s l / 40%); + + --color-neutral-800-56: hsl(from var(--color-neutral-800) h s l / 56%); + + --color-neutral-900-40: hsl(from var(--color-neutral-900) h s l / 40%); + --color-neutral-900-56: hsl(from var(--color-neutral-900) h s l / 56%); + --color-neutral-900-64: hsl(from var(--color-neutral-900) h s l / 64%); + + /* Brand red */ + --color-brand-red-100: hsl(23, 96%, 62%); + --color-brand-red-200: hsl(19, 84%, 58%); + --color-brand-red-300: hsl(14, 74%, 54%); + --color-brand-red-400: hsl(9, 67%, 51%); + --color-brand-red-500: hsl(4, 67%, 47%); + --color-brand-red-600: hsl(1, 62%, 39%); + --color-brand-red-700: hsl(359, 58%, 31%); + --color-brand-red-800: hsl(352, 57%, 22%); + --color-brand-red-900: hsl(334, 54%, 13%); + + --gradient-brand-red: radial-gradient( + 138.34% 138.34% at 0% 4.4%, + var(--color-destructive-900) 0%, + var(--color-destructive-800) 12.5%, + var(--color-destructive-700) 25%, + var(--color-destructive-600) 37.5%, + var(--color-destructive-500) 50%, + var(--color-destructive-400) 62.5%, + var(--color-destructive-300) 75%, + var(--color-destructive-200) 87.5%, + var(--color-destructive-100) 100% + ); + + /* Brand green */ + --color-brand-green-100: hsl(68, 52%, 72%); + --color-brand-green-200: hsl(75, 37%, 64%); + --color-brand-green-300: hsl(84, 28%, 56%); + --color-brand-green-400: hsl(95, 23%, 49%); + --color-brand-green-500: hsl(108, 24%, 41%); + --color-brand-green-600: hsl(116, 24%, 34%); + --color-brand-green-700: hsl(128, 30%, 25%); + --color-brand-green-800: hsl(140, 44%, 16%); + --color-brand-green-900: hsl(154, 100%, 7%); + + --gradient-brand-green: radial-gradient( + 137.26% 137.26% at 0% 0%, + var(--color-brand-green-900) 0%, + var(--color-brand-green-800) 12.5%, + var(--color-brand-green-700) 25%, + var(--color-brand-green-600) 37.5%, + var(--color-brand-green-500) 50%, + var(--color-brand-green-400) 62.5%, + var(--color-brand-green-300) 75%, + var(--color-brand-green-200) 87.5%, + var(--color-brand-green-100) 100% + ); + + /* Brand blue */ + --color-brand-blue-100: hsl(216, 100%, 100%); + --color-brand-blue-200: hsl(215, 86%, 73%); + --color-brand-blue-300: hsl(215, 77%, 65%); + --color-brand-blue-400: hsl(215, 72%, 58%); + --color-brand-blue-500: hsl(214, 69%, 50%); + --color-brand-blue-600: hsl(215, 71%, 40%); + --color-brand-blue-700: hsl(215, 75%, 31%); + --color-brand-blue-800: hsl(217, 82%, 21%); + --color-brand-blue-900: hsl(220, 100%, 12%); + + --gradient-brand-blue: radial-gradient( + 141.42% 141.42% at 0% 0%, + var(--color-brand-blue-900) 0%, + var(--color-brand-blue-800) 12.5%, + var(--color-brand-blue-700) 25%, + var(--color-brand-blue-600) 37.5%, + var(--color-brand-blue-500) 50%, + var(--color-brand-blue-400) 62.5%, + var(--color-brand-blue-300) 75%, + var(--color-brand-blue-200) 87.5%, + var(--color-brand-blue-100) 100% + ); + + /* Tech colors */ + --color-brand-typescript: hsl(0, 0%, 0%); + --color-brand-go: hsl(220, 100%, 12%); + --color-brand-java: hsl(214, 69%, 50%); + --color-brand-python: hsl(216, 100%, 80%); + --color-brand-c: hsl(154, 100%, 7%); + --color-brand-terraform: hsl(108, 24%, 41%); + --color-brand-unity: hsl(68, 52%, 72%); + --color-brand-php: hsl(334, 54%, 13%); + --color-brand-swift: hsl(4, 67%, 47%); + --color-brand-ruby: hsl(23, 96%, 62%); + --color-brand-postman: hsl(0, 0%, 83%); + + --gradient-brand-primary-colors: var(--color-brand-php) 0%, + var(--color-brand-swift) 12.56%, var(--color-brand-ruby) 25.06%, + var(--color-brand-unity) 37.56%, var(--color-brand-terraform) 50.06%, + var(--color-brand-c) 62.06%, var(--color-brand-go) 74.06%, + var(--color-brand-java) 86.06%, var(--color-brand-python) 97.06%; + + --gradient-brand-primary: linear-gradient( + 90deg, + var(--gradient-brand-primary-colors) + ); + + --gradient-brand-primary-y: linear-gradient( + 180deg, + var(--gradient-brand-primary-colors) + ); + + /* Feedback colors */ + + /* Red */ + --color-feedback-red-100: hsl(0, 100%, 97%); + --color-feedback-red-200: hsl(3, 84%, 85%); + --color-feedback-red-300: hsl(3, 78%, 71%); + --color-feedback-red-400: hsl(2, 74%, 58%); + --color-feedback-red-500: hsl(4, 67%, 47%); + --color-feedback-red-600: hsl(2, 65%, 39%); + --color-feedback-red-700: hsl(1, 64%, 32%); + --color-feedback-red-800: hsl(1, 63%, 24%); + --color-feedback-red-900: hsl(0, 62%, 17%); + + --color-feedback-red-400-56: hsl(from var(--color-feedback-red-400) h s l / 56%); + --color-feedback-red-500-56: hsl(from var(--color-feedback-red-500) h s l / 56%); + --color-feedback-red-600-56: hsl(from var(--color-feedback-red-600) h s l / 56%); + + + /* Orange */ + --color-feedback-orange-100: hsl(29, 100%, 95%); + --color-feedback-orange-200: hsl(30, 100%, 85%); + --color-feedback-orange-300: hsl(28, 100%, 74%); + --color-feedback-orange-400: hsl(27, 100%, 66%); + --color-feedback-orange-500: hsl(23, 96%, 62%); + --color-feedback-orange-600: hsl(22, 70%, 53%); + --color-feedback-orange-700: hsl(21, 64%, 43%); + --color-feedback-orange-800: hsl(19, 66%, 33%); + --color-feedback-orange-900: hsl(18, 69%, 24%); + + --color-feedback-orange-400-56: hsl(from var(--color-feedback-orange-400) h s l / 56%); + --color-feedback-orange-500-56: hsl(from var(--color-feedback-orange-500) h s l / 56%); + + /* Green */ + --color-feedback-green-100: hsl(102, 35%, 93%); + --color-feedback-green-200: hsl(99, 33%, 83%); + --color-feedback-green-300: hsl(99, 30%, 73%); + --color-feedback-green-400: hsl(101, 28%, 62%); + --color-feedback-green-500: hsl(108, 24%, 41%); + --color-feedback-green-600: hsl(107, 24%, 34%); + --color-feedback-green-700: hsl(105, 24%, 27%); + --color-feedback-green-800: hsl(104, 24%, 20%); + --color-feedback-green-900: hsl(105, 24%, 13%); + + --color-feedback-green-400-56: hsl(from var(--color-feedback-green-400) h s l / 56%); + --color-feedback-green-500-56: hsl(from var(--color-feedback-green-500) h s l / 56%); + + /* Blue */ + --color-feedback-blue-100: hsl(212, 100%, 95%); + --color-feedback-blue-200: hsl(214, 100%, 86%); + --color-feedback-blue-300: hsl(214, 100%, 77%); + --color-feedback-blue-400: hsl(214, 89%, 67%); + --color-feedback-blue-500: hsl(214, 69%, 50%); + --color-feedback-blue-600: hsl(215, 70%, 41%); + --color-feedback-blue-700: hsl(215, 71%, 32%); + --color-feedback-blue-800: hsl(217, 73%, 23%); + --color-feedback-blue-900: hsl(218, 73%, 15%); + + --color-feedback-blue-400-56: hsl(from var(--color-feedback-blue-400) h s l / 56%); + --color-feedback-blue-500-56: hsl(from var(--color-feedback-blue-500) h s l / 56%); + + /* Violet */ + --color-feedback-violet-300: hsl(265, 77%, 65%); + --color-feedback-violet-600: hsl(268, 75%, 31%); + + --color-link-default: var(--color-brand-blue-600); + --color-link-visited-primary: var(--color-feedback-violet-600); + --color-link-secondary: var(--color-base-black); + --color-link-visited-secondary: var(--color-feedback-violet-600); + + /* ============================================ + * 2. SEMANTIC/UTILITY TOKENS + * Theme-aware variables that map directly to utility classes + * These change based on light/dark mode + * ============================================ */ + + /* Text Colors */ + /* TODO: These should use our skimmable colors for headings */ + --text-heading-xl: var(--color-neutral-900); + --text-heading-lg: var(--color-neutral-900); + --text-heading-md: var(--color-neutral-900); + --text-heading-sm: var(--color-neutral-800); + --text-heading-xs: var(--color-neutral-800); + + --text-display: var(--color-base-black); + + --text-highlight: var(--color-neutral-900); + --text-default: var(--color-neutral-700); + --text-muted: var(--color-neutral-900-64); + --text-placeholder: var(--color-neutral-900-56); + --text-disabled: var(--color-neutral-900-40); + + --text-dark-light-highlight: var(--color-base-black); + --text-dark-light-default: var(--color-neutral-900); + --text-dark-light-muted: var(--color-neutral-900-64); + + --text-light-dark-highlight: var(--color-base-white); + --text-light-dark-default: var(--color-neutral-100); + --text-light-dark-muted: var(--color-neutral-100-64); + + --text-link-primary: var(--color-brand-blue-600); + --text-link-secondary: var(--color-base-black); + --text-link-visited: var(--color-feedback-violet-600); + + --text-default-destructive: var(--color-feedback-red-700); + --text-link-destructive: var(--color-feedback-red-900); + + --text-default-information: var(--color-feedback-blue-700); + --text-link-information: var(--color-feedback-blue-900); + + --text-default-success: var(--color-feedback-green-700); + --text-link-success: var(--color-feedback-green-900); + + --text-default-warning: var(--color-feedback-orange-700); + --text-link-warning: var(--color-feedback-orange-900); + + + --text-body: var(--color-neutral-900); + --text-body-muted: var(--color-neutral-400); + + /* Warning State */ + --bg-warning: var(--color-feedback-orange-100); + --text-warning: var(--color-feedback-orange-700); + --border-warning: var(--color-feedback-orange-300); + + + /* Link utilities */ + --underline-link-primary: var(--color-brand-blue-600); + --underline-link-secondary: var(--color-base-black); + --underline-link-visited: var(--color-feedback-violet-600); + + + /* ============================================ + * BORDER TOKENS + * ============================================ */ + + /* Border utilities - neutral states */ + --border-neutral-active: var(--color-neutral-600); + --border-neutral-hover: var(--color-neutral-500); + --border-neutral-default: var(--color-neutral-400); + --border-neutral-disabled: var(--color-neutral-400-40); + --border-neutral-softest: var(--color-neutral-300); + --border-neutral-inset: var(--color-base-black); + --border-neutral-alpha: hsla( + 0, + 0%, + 0%, + 0.2 + ); /* Black with 20% opacity for light mode */ + + + /* Border Destructive utilities */ + --border-destructive-highlight: var(--color-feedback-red-600); + --border-destructive-default: var(--color-feedback-red-500); + --border-destructive-muted: var(--color-feedback-red-500-56); + --border-destructive-softest: var(--color-feedback-red-300); + + /* Border Information utilities */ + --border-information-highlight: var(--color-feedback-blue-600); + --border-information-default: var(--color-feedback-blue-500); + --border-information-muted: var(--color-feedback-blue-500-56); + --border-information-softest: var(--color-feedback-blue-300); + + /* Border Success utilities */ + --border-success-highlight: var(--color-feedback-green-600); + --border-success-default: var(--color-feedback-green-500); + --border-success-muted: var(--color-feedback-green-500-56); + --border-success-softest: var(--color-feedback-green-300); + + /* Border Warning utilities */ + --border-warning-highlight: var(--color-feedback-orange-600); + --border-warning-default: var(--color-feedback-orange-500); + --border-warning-muted: var(--color-feedback-orange-500-56); + --border-warning-softest: var(--color-feedback-orange-300); + + /* Border Focus utilities */ + --border-focus: var(--color-brand-blue-600); + + + /* ============================================ + * FILLS + * ============================================ */ + + /* Neutrals */ + --fill-neutral-highlight: var(--color-base-black); + --fill-neutral-active: var(--color-neutral-900); + --fill-neutral-default: var(--color-neutral-800); + --fill-neutral-muted: var(--color-neutral-800-56); + + /* onColor / Dark */ + --fill-onColor-dark-highlight: var(--color-base-black); + --fill-onColor-dark-default: var(--color-neutral-800); + --fill-onColor-dark-muted: var(--color-neutral-900-56); + + /* onColor / Light */ + --fill-onColor-light-highlight: var(--color-base-white); + --fill-onColor-light-default: var(--color-neutral-100); + --fill-onColor-light-muted: var(--color-neutral-100-56); + + /* Links */ + --fill-link-primary: var(--color-brand-blue-600); + --fill-link-secondary: var(--color-base-black); + --fill-link-visited: var(--color-feedback-violet-600); + + /* Destructive */ + --fill-destructive-highlight: var(--color-feedback-red-600); + --fill-destructive-default: var(--color-feedback-red-500); + --fill-destructive-muted: var(--color-feedback-red-600-56); + + /* Information */ + --fill-information-highlight: var(--color-feedback-blue-600); + --fill-information-default: var(--color-feedback-blue-500); + --fill-information-muted: var(--color-feedback-blue-500-56); + + /* Success */ + --fill-success-highlight: var(--color-feedback-green-600); + --fill-success-default: var(--color-feedback-green-500); + --fill-success-muted: var(--color-feedback-green-500-56); + + /* Warning */ + --fill-warning-highlight: var(--color-feedback-orange-600); + --fill-warning-default: var(--color-feedback-orange-500); + --fill-warning-muted: var(--color-feedback-orange-500-56); + + /* ============================================ + * Strokes + * ============================================ */ + + /* Neutrals */ + --stroke-neutral-highlight: var(--color-base-black); + --stroke-neutral-active: var(--color-neutral-900); + --stroke-neutral-default: var(--color-neutral-800); + --stroke-neutral-muted: var(--color-neutral-800-56); + + /* onColor / Dark */ + --stroke-onColor-dark-highlight: var(--color-base-black); + --stroke-onColor-dark-default: var(--color-neutral-800); + --stroke-onColor-dark-muted: var(--color-neutral-900-56); + + /* onColor / Light */ + --stroke-onColor-light-highlight: var(--color-base-white); + --stroke-onColor-light-default: var(--color-neutral-100); + --stroke-onColor-light-muted: var(--color-neutral-100-56); + + /* Links */ + --stroke-link-primary: var(--color-brand-blue-600); + --stroke-link-secondary: var(--color-base-black); + --stroke-link-visited: var(--color-feedback-violet-600); + + /* Destructive */ + --stroke-destructive-highlight: var(--color-feedback-red-600); + --stroke-destructive-default: var(--color-feedback-red-500); + --stroke-destructive-muted: var(--color-feedback-red-600-56); + + /* Information */ + --stroke-information-highlight: var(--color-feedback-blue-600); + --stroke-information-default: var(--color-feedback-blue-500); + --stroke-information-muted: var(--color-feedback-blue-500-56); + + /* Success */ + --stroke-success-highlight: var(--color-feedback-green-600); + --stroke-success-default: var(--color-feedback-green-500); + --stroke-success-muted: var(--color-feedback-green-500-56); + + /* Warning */ + --stroke-warning-highlight: var(--color-feedback-orange-600); + --stroke-warning-default: var(--color-feedback-orange-500); + --stroke-warning-muted: var(--color-feedback-orange-500-56); + + /* ============================================ + * BACKGROUNDS + * ============================================ */ + --bg-primary: var(--color-base-white); + --bg-secondary: var(--color-neutral-100); + --bg-tertiary: var(--color-neutral-200); + + /* State */ + --bg-highlight: var(--color-neutral-300); + --bg-active: var(--color-neutral-200); + --bg-default: var(--color-neutral-100); + --bg-muted: var(--color-neutral-100-56); + --bg-inset: var(--color-base-white); + + /* Light background in dark mode */ + --bg-light-dark-primary: var(--color-base-white); + --bg-light-dark-secondary: var(--color-neutral-100); + --bg-light-dark-tertiary: var(--color-neutral-200); + + /* Dark background in light mode */ + --bg-dark-light-primary: var(--color-base-black); + --bg-dark-light-secondary: var(--color-neutral-900); + --bg-dark-light-tertiary: var(--color-neutral-800); + + /* Destructive */ + --bg-destructive-highlight: var(--color-feedback-red-600); + --bg-destructive-default: var(--color-feedback-red-500); + --bg-destructive-muted: var(--color-feedback-red-500-56); + --bg-destructive-softest: var(--color-feedback-red-100); + + /* Information */ + --bg-information-highlight: var(--color-feedback-blue-600); + --bg-information-default: var(--color-feedback-blue-500); + --bg-information-muted: var(--color-feedback-blue-500-56); + --bg-information-softest: var(--color-feedback-blue-100); + + /* Success */ + --bg-success-highlight: var(--color-feedback-green-600); + --bg-success-default: var(--color-feedback-green-500); + --bg-success-muted: var(--color-feedback-green-500-56); + --bg-success-softest: var(--color-feedback-green-100); + + /* Warning */ + --bg-warning-highlight: var(--color-feedback-orange-600); + --bg-warning-default: var(--color-feedback-orange-500); + --bg-warning-muted: var(--color-feedback-orange-500-56); + --bg-warning-softest: var(--color-feedback-orange-100); + + + + + /* ============================================ + * 3. COMPONENT TOKENS + * Higher-level semantic tokens for specific use cases + * ============================================ */ + + /* Border Radius */ + --radius: 0.625rem; + /* ============================================ + * 4. DEPRECATED TOKENS - SHADCN COMPATIBILITY + * These will be removed in future versions + * DO NOT use in new code + * ============================================ */ + + --background-pure: var(--color-base-white); + --background: var(--color-neutral-100); + --foreground: var(--color-neutral-900); + --card: var(--color-base-white); + --card-foreground: var(--color-neutral-900); + --popover: var(--color-base-white); + --popover-foreground: var(--color-neutral-900); + --primary: var(--color-base-black); + --primary-foreground: var(--color-base-white); + --secondary: var(--color-neutral-100); + --secondary-foreground: var(--color-neutral-900); + --muted: var(--color-neutral-200); + --muted-foreground: var(--color-neutral-900-64); + --accent: var(--color-neutral-200); + --accent-foreground: var(--color-neutral-900); + --destructive: var( + --color-feedback-red-700 + ); /* Note: conflicts with semantic token above */ + --border: var(--color-neutral-300); + --input: var(--color-neutral-400); + --ring: var(--color-brand-blue-600); + --chart-1: var(--color-brand-red-600); + --chart-2: var(--color-brand-green-600); + --chart-3: var(--color-brand-red-600); + --chart-4: var(--color-brand-blue-600); + --chart-5: var(--color-brand-green-600); + --sidebar: var(--color-neutral-100); + --sidebar-foreground: var(--color-neutral-900); + --sidebar-primary: var(--color-base-black); + --sidebar-primary-foreground: var(--color-neutral-300); + --sidebar-accent: var(--color-neutral-300); + --sidebar-accent-foreground: var(--color-neutral-900); + --sidebar-border: var(--color-neutral-300); + --sidebar-ring: var(--color-brand-blue-600); + + /* Feedback States */ + --success: var(--color-feedback-green-100); + --success-foreground: var(--color-feedback-green-700); + --warning: var(--color-feedback-orange-100); + --warning-foreground: var(--color-feedback-orange-700); + --info: var(--color-info-100); + --info-foreground: var(--color-info-700); + /* This is a dupe of a shadcn token, may need to rename*/ + --destructive: var(--color-destructive-100); + --destructive-foreground: var(--color-destructive-700); + --feature: var(--color-info-100); + --feature-foreground: var(--color-info-700); + + /* UI Elements */ + --shadow: hsl(0 0% 50%); + + /* Custom Scrollbar */ + --sb-size: 0.5rem; + --sb-track-color: var(--color-background); + --sb-thumb-color: var(--color-background); + --sb-track-border: var(--color-neutral-300); + + /* Score Colors */ + --score-low: var(--color-destructive-500); + --score-mid: var(--color-feedback-orange-500); + --score-high: var(--color-feedback-green-500); + --score-track: var(--color-neutral-700); + + /* Animation Easing Functions */ + --ease-in-quad: cubic-bezier(0.55, 0.085, 0.68, 0.53); + --ease-in-cubic: cubic-bezier(0.55, 0.055, 0.675, 0.19); + --ease-in-quart: cubic-bezier(0.895, 0.03, 0.685, 0.22); + --ease-in-quint: cubic-bezier(0.755, 0.05, 0.855, 0.06); + --ease-in-expo: cubic-bezier(0.95, 0.05, 0.795, 0.035); + --ease-in-circ: cubic-bezier(0.6, 0.04, 0.98, 0.335); + + --ease-out-quad: cubic-bezier(0.25, 0.46, 0.45, 0.94); + --ease-out-cubic: cubic-bezier(0.215, 0.61, 0.355, 1); + --ease-out-quart: cubic-bezier(0.165, 0.84, 0.44, 1); + --ease-out-quint: cubic-bezier(0.23, 1, 0.32, 1); + --ease-out-expo: cubic-bezier(0.19, 1, 0.22, 1); + --ease-out-circ: cubic-bezier(0.075, 0.82, 0.165, 1); + + --ease-in-out-quad: cubic-bezier(0.455, 0.03, 0.515, 0.955); + --ease-in-out-cubic: cubic-bezier(0.645, 0.045, 0.355, 1); + --ease-in-out-quart: cubic-bezier(0.77, 0, 0.175, 1); + --ease-in-out-quint: cubic-bezier(0.86, 0, 0.07, 1); + --ease-in-out-expo: cubic-bezier(1, 0, 0, 1); + --ease-in-out-circ: cubic-bezier(0.785, 0.135, 0.15, 0.86); +} + +/* ============================================ + * BASE ELEMENT STYLES + * Default styles for HTML elements + * ============================================ */ + +/* + * Tailwind v4 Compatibility Layer + * The default border color has changed to `currentColor` in Tailwind CSS v4. + * These styles maintain v3 behavior until we can audit all border usage. + */ +@layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--border-neutral-default, currentColor); + } + + button:not(:disabled), + [role='button']:not(:disabled) { + cursor: pointer; + } + + body { + @apply bg-background text-foreground; + } +} + +:root.dark { + /* ============================================ + * DARK MODE OVERRIDES + * Only define values that change in dark mode + * ============================================ */ + + --color-link-default: var(--color-brand-blue-300); + --color-link-visited-primary: var(--color-feedback-violet-300); + --color-link-secondary: var(--color-base-white); + --color-link-visited-secondary: var(--color-feedback-violet-300); + + /* ============================================ + * 2. SEMANTIC/UTILITY TOKENS (Dark Mode) + * ============================================ */ + + /* Text Colors */ + --text-heading-xl: var(--color-neutral-100); + --text-heading-lg: var(--color-neutral-100); + --text-heading-md: var(--color-neutral-100); + --text-heading-sm: var(--color-neutral-200); + --text-heading-xs: var(--color-neutral-200); + + --text-display: var(--color-base-white); + + --text-highlight: var(--color-neutral-100); + --text-default: var(--color-neutral-300); + --text-muted: var(--color-neutral-100-64); + --text-placeholder: var(--color-neutral-100-56); + --text-disabled: var(--color-neutral-100-40); + + --text-link-primary: var(--color-brand-blue-300); + --text-link-secondary: var(--color-base-white); + --text-link-visited: var(--color-feedback-violet-300); + + --text-default-destructive: var(--color-feedback-red-300); + --text-link-destructive: var(--color-feedback-red-100); + + --text-default-information: var(--color-feedback-blue-300); + --text-link-information: var(--color-feedback-blue-100); + + --text-default-success: var(--color-feedback-green-300); + --text-link-success: var(--color-feedback-green-100); + + --text-default-warning: var(--color-feedback-orange-300); + --text-link-warning: var(--color-feedback-orange-100); + + --text-body: var(--color-neutral-200); + --text-body-muted: var(--color-neutral-600); + + /* Warning State */ + --bg-warning: var(--color-feedback-orange-900); + --text-warning: var(--color-feedback-orange-300); + --border-warning: var(--color-feedback-orange-700); + + /* Link utilities (dark mode) */ + --underline-link-primary: var(--color-brand-blue-300); + --underline-link-secondary: var(--color-base-white); + --underline-link-visited: var(--color-feedback-violet-300); + + /* ============================================ + * BORDER TOKENS + * ============================================ */ + + /* Border utilities - neutral states (dark mode) */ + --border-neutral-active: var(--color-neutral-400); + --border-neutral-hover: var(--color-neutral-500); + --border-neutral-default: var(--color-neutral-600); + --border-neutral-disabled: var(--color-neutral-600-40); + --border-neutral-softest: var(--color-neutral-700); + --border-neutral-inset: var(--color-base-white); + --border-neutral-alpha: hsla( + 255, + 255%, + 255%, + 0.2 + ); /* White with 20% opacity */ + + /* Border destructive utilities */ + --border-destructive-highlight: var(--color-feedback-red-300); + --border-destructive-default: var(--color-feedback-red-500); + --border-destructive-muted: var(--color-feedback-red-500-56); + --border-destructive-softest: var(--color-feedback-red-700); + + /* Border Information utilities */ + --border-information-highlight: var(--color-feedback-blue-300); + --border-information-default: var(--color-feedback-blue-500); + --border-information-muted: var(--color-feedback-blue-500-56); + --border-information-softest: var(--color-feedback-blue-700); + + /* Border Success utilities */ + --border-success-highlight: var(--color-feedback-green-300); + --border-success-default: var(--color-feedback-green-500); + --border-success-muted: var(--color-feedback-green-500-56); + --border-success-softest: var(--color-feedback-green-700); + + /* Border Warning utilities */ + --border-warning-highlight: var(--color-feedback-orange-300); + --border-warning-default: var(--color-feedback-orange-500); + --border-warning-muted: var(--color-feedback-orange-500-56); + --border-warning-softest: var(--color-feedback-orange-700); + + /* Border Focus utilities */ + --border-focus: var(--color-brand-blue-300); + + + /* ============================================ + * FILLS + * ============================================ */ + + /* Neutrals */ + --fill-neutral-highlight: var(--color-base-white); + --fill-neutral-active: var(--color-neutral-100); + --fill-neutral-default: var(--color-neutral-200); + --fill-neutral-muted: var(--color-neutral-200-56); + + /* onColor / Dark */ + --fill-onColor-dark-highlight: var(--color-base-black); + --fill-onColor-dark-default: var(--color-neutral-800); + --fill-onColor-dark-muted: var(--color-neutral-900-56); + + /* onColor / Light */ + --fill-onColor-light-highlight: var(--color-base-white); + --fill-onColor-light-default: var(--color-neutral-100); + --fill-onColor-light-muted: var(--color-neutral-100-56); + + /* Links */ + --fill-link-primary: var(--color-brand-blue-300); + --fill-link-secondary: var(--color-base-white); + --fill-link-visited: var(--color-feedback-violet-300); + + /* Destructive */ + --fill-destructive-highlight: var(--color-feedback-red-300); + --fill-destructive-default: var(--color-feedback-red-400); + --fill-destructive-muted: var(--color-feedback-red-400-56); + + /* Information */ + --fill-information-highlight: var(--color-feedback-blue-300); + --fill-information-default: var(--color-feedback-blue-400); + --fill-information-muted: var(--color-feedback-blue-400-56); + + /* Success */ + --fill-success-highlight: var(--color-feedback-green-300); + --fill-success-default: var(--color-feedback-green-400); + --fill-success-muted: var(--color-feedback-green-400-56); + + /* Warning */ + --fill-warning-highlight: var(--color-feedback-orange-300); + --fill-warning-default: var(--color-feedback-orange-400); + --fill-warning-muted: var(--color-feedback-orange-400-56); + + /* ============================================ + * Strokes + * ============================================ */ + + /* Neutrals */ + --stroke-neutral-highlight: var(--color-base-white); + --stroke-neutral-active: var(--color-neutral-100); + --stroke-neutral-default: var(--color-neutral-200); + --stroke-neutral-muted: var(--color-neutral-200-56); + + /* onColor / Dark */ + --stroke-onColor-dark-highlight: var(--color-base-black); + --stroke-onColor-dark-default: var(--color-neutral-800); + --stroke-onColor-dark-muted: var(--color-neutral-900-56); + + /* onColor / Light */ + --stroke-onColor-light-highlight: var(--color-base-white); + --stroke-onColor-light-default: var(--color-neutral-100); + --stroke-onColor-light-muted: var(--color-neutral-100-56); + + /* Links */ + --stroke-link-primary: var(--color-brand-blue-300); + --stroke-link-secondary: var(--color-base-white); + --stroke-link-visited: var(--color-feedback-violet-300); + + /* Destructive */ + --stroke-destructive-highlight: var(--color-feedback-red-300); + --stroke-destructive-default: var(--color-feedback-red-400); + --stroke-destructive-muted: var(--color-feedback-red-400-56); + + /* Information */ + --stroke-information-highlight: var(--color-feedback-blue-300); + --stroke-information-default: var(--color-feedback-blue-400); + --stroke-information-muted: var(--color-feedback-blue-400-56); + + /* Success */ + --stroke-success-highlight: var(--color-feedback-green-300); + --stroke-success-default: var(--color-feedback-green-400); + --stroke-success-muted: var(--color-feedback-green-400-56); + + /* Warning */ + --stroke-warning-highlight: var(--color-feedback-orange-300); + --stroke-warning-default: var(--color-feedback-orange-400); + --stroke-warning-muted: var(--color-feedback-orange-400-56); + + /* ============================================ + * BACKGROUNDS + * ============================================ */ + --bg-primary: var(--color-base-black); + --bg-secondary: var(--color-neutral-900); + --bg-tertiary: var(--color-neutral-800); + + /* State */ + --bg-highlight: var(--color-neutral-700); + --bg-active: var(--color-neutral-800); + --bg-default: var(--color-neutral-900); + --bg-muted: var(--color-neutral-900-56); + --bg-inset: var(--color-base-black); + + /* Light background in dark mode */ + + /* Dark background in light mode */ + + /* Destructive */ + --bg-destructive-highlight: var(--color-feedback-red-300); + --bg-destructive-default: var(--color-feedback-red-500); + --bg-destructive-muted: var(--color-feedback-red-500-56); + --bg-destructive-softest: var(--color-feedback-red-900); + + /* Information */ + --bg-information-highlight: var(--color-feedback-blue-300); + --bg-information-default: var(--color-feedback-blue-500); + --bg-information-muted: var(--color-feedback-blue-500-56); + --bg-information-softest: var(--color-feedback-blue-900); + + /* Success */ + --bg-success-highlight: var(--color-feedback-green-300); + --bg-success-default: var(--color-feedback-green-500); + --bg-success-muted: var(--color-feedback-green-500-56); + --bg-success-softest: var(--color-feedback-green-900); + + /* Warning */ + --bg-warning-highlight: var(--color-feedback-orange-300); + --bg-warning-default: var(--color-feedback-orange-500); + --bg-warning-muted: var(--color-feedback-orange-500-56); + --bg-warning-softest: var(--color-feedback-orange-900); + + + /* ============================================ + * 3. COMPONENT TOKENS (Dark Mode) + * ============================================ */ + + /* Feedback States */ + --success: var(--color-feedback-green-500); + --success-foreground: var(--color-feedback-green-100); + --warning: var(--color-feedback-orange-500); + --warning-foreground: var(--color-feedback-orange-100); + --info: var(--color-info-500); + --info-foreground: var(--color-info-100); + --destructive: var(--color-destructive-500); + --destructive-foreground: var(--color-destructive-100); + --feature: var(--color-info-500); + --feature-foreground: var(--color-info-100); + + /* Score Colors */ + --score-low: hsl(350 89% 60%); + --score-mid: hsl(38 92% 50%); + --score-high: hsl(160 84% 39%); + --score-track: hsl(48 4% 28%); + + /* Custom Scrollbar */ + --sb-track-color: var(--color-neutral-800); + --sb-thumb-color: var(--color-neutral-900); + --sb-track-border: var(--color-neutral-800); + + /* Misc */ + --header-border: 0 0% 14.9%; + + /* ============================================ + * 4. DEPRECATED TOKENS - SHADCN (Dark Mode) + * ============================================ */ + + --background-pure: var(--color-base-black); + --background: var(--color-base-black); + --foreground: var(--color-neutral-300); + --card: var(--color-neutral-900); + --card-foreground: var(--color-neutral-300); + --popover: var(--color-base-black); + --popover-foreground: var(--color-neutral-100); + --primary: var(--color-base-white); + --primary-foreground: var(--color-base-black); + --secondary: var(--color-neutral-900); + --secondary-foreground: var(--color-neutral-300); + --muted: var(--color-neutral-900); + --muted-foreground: var(--color-neutral-100-64); + --accent: var(--color-neutral-800); + --accent-foreground: var(--color-base-white); + --destructive: var( + --color-feedback-red-500 + ); /* Note: conflicts with semantic token above */ + --border: var(--color-neutral-700); + --input: var(--color-neutral-700); + --ring: var(--color-brand-blue-600); + --chart-1: var(--color-brand-red-600); + --chart-2: var(--color-brand-green-600); + --chart-3: var(--color-brand-red-600); + --chart-4: var(--color-brand-blue-600); + --chart-5: var(--color-brand-green-600); + --sidebar: var(--color-neutral-900); + --sidebar-foreground: var(--color-base-white); + --sidebar-primary: var(--color-brand-red-600); + --sidebar-primary-foreground: var(--color-neutral-300); + --sidebar-accent: var(--color-neutral-700); + --sidebar-accent-foreground: var(--color-base-white); + --sidebar-border: var(--color-neutral-700); + --sidebar-ring: var(--color-brand-blue-600); +} diff --git a/src/components/AIChat/AIChatContainer.stories.tsx b/src/components/AIChat/AIChatContainer.stories.tsx index 7f41c015..07b81943 100644 --- a/src/components/AIChat/AIChatContainer.stories.tsx +++ b/src/components/AIChat/AIChatContainer.stories.tsx @@ -10,9 +10,6 @@ const meta: Meta = { tags: ['autodocs'], parameters: { layout: 'fullscreen', - backgrounds: { - default: 'dark', - }, }, decorators: [ (Story) => ( @@ -250,6 +247,96 @@ export const Loading: Story = { }, } +// Demonstrates the immediate avatar showing with loading state +export const ImmediateLoading: Story = { + args: { + messages: [ + { + id: '1', + role: 'user', + parts: [{ type: 'text', text: 'Hello, can you help me?' }], + }, + { + id: '2', + role: 'assistant', + parts: [], // Empty parts - will show loading indicator + }, + ], + isLoading: true, + onSendMessage: (message) => console.log('Sending message:', message), + }, +} + +// Interactive demo that shows the loading behavior in action +const LoadingSimulationComponent = () => { + const [messages, setMessages] = useState([ + { + id: '1', + role: 'user', + parts: [{ type: 'text', text: 'Can you explain how React hooks work?' }], + }, + ]) + const [isLoading, setIsLoading] = useState(false) + + const simulateAIResponse = async () => { + setIsLoading(true) + + // Step 1: Add empty assistant message immediately (shows avatar + loading dots) + const assistantMessage: ChatMessage = { + id: Date.now().toString(), + role: 'assistant', + parts: [], // Empty parts + } + setMessages((prev) => [...prev, assistantMessage]) + + // Step 2: After a short delay, start "streaming" content + await new Promise((resolve) => setTimeout(resolve, 800)) + + // Step 3: Add content to the assistant message + setMessages((prev) => + prev.map((msg) => + msg.id === assistantMessage.id + ? { + ...msg, + parts: [ + { + type: 'text', + text: 'React hooks are functions that let you use state and lifecycle features in functional components...', + }, + ], + } + : msg + ) + ) + + setIsLoading(false) + } + + const handleSendMessage = (message: string) => { + const userMessage: ChatMessage = { + id: Date.now().toString(), + role: 'user', + parts: [{ type: 'text', text: message }], + } + setMessages((prev) => [...prev, userMessage]) + + // Simulate the response after a brief moment + setTimeout(simulateAIResponse, 100) + } + + return ( + + ) +} + +export const LoadingSimulation: Story = { + render: () => , +} + export const Customized: Story = { args: { messages: openApiEditingMessages.slice(0, 3), diff --git a/src/components/AIChat/AIChatMessage.tsx b/src/components/AIChat/AIChatMessage.tsx index eaf3b8b4..26f4ffba 100644 --- a/src/components/AIChat/AIChatMessage.tsx +++ b/src/components/AIChat/AIChatMessage.tsx @@ -19,6 +19,7 @@ export interface AIChatMessageProps { message: ChatMessage className?: string components?: Partial + isLoading?: boolean } interface AvatarComponents extends BaseComponents { @@ -36,7 +37,7 @@ const defaultAvatars: DefaultComponents = { user: ({ className }) => (
@@ -50,7 +51,7 @@ const defaultAvatars: DefaultComponents = { assistant: ({ className }) => (
@@ -60,7 +61,7 @@ const defaultAvatars: DefaultComponents = { system: ({ className }) => ( @@ -92,42 +94,57 @@ export function AIChatMessage({ )}
- {message.parts.map((part, index) => { - switch (part.type) { - case 'text': - return - case 'reasoning': - return ( - - ) - case 'tool-invocation': - return ( - - ) - case 'file': - return ( - - ) - case 'source': - return ( - - ) - default: - return null - } - })} + {message.parts.length > 0 + ? message.parts.map((part, index) => { + switch (part.type) { + case 'text': + return ( + + ) + case 'reasoning': + return ( + + ) + case 'tool-invocation': + return ( + + ) + case 'file': + return ( + + ) + case 'source': + return ( + + ) + default: + return null + } + }) + : // Show loading state for empty assistant messages + isLoading && + message.role === 'assistant' && ( + + )}
diff --git a/src/components/AIChat/AIChatMessageComposer.tsx b/src/components/AIChat/AIChatMessageComposer.tsx index ec358d05..2446207f 100644 --- a/src/components/AIChat/AIChatMessageComposer.tsx +++ b/src/components/AIChat/AIChatMessageComposer.tsx @@ -113,8 +113,8 @@ export function AIChatMessageComposer({