Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions docs/THEMING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# Hailstorm Theming System

## Overview

Hailstorm now supports a flexible theming system with built-in dark mode support. The theming system uses CSS custom properties that automatically adjust based on the active theme.

## Quick Start

### Using ThemeProvider

Wrap your application with the `ThemeProvider` component:

```tsx
import { ThemeProvider } from "@abusix/hailstorm";

function App() {
return (
<ThemeProvider defaultTheme="system" storageKey="hailstorm-theme">
{/* Your app content */}
</ThemeProvider>
);
}
```

### Using the Theme Hook

Access and control the theme programmatically:

```tsx
import { useTheme } from "@abusix/hailstorm";

function ThemeToggle() {
const { theme, setTheme } = useTheme();

return (
<button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
Toggle theme
</button>
);
}
```

## Semantic Color Tokens

The theming system introduces semantic color tokens that automatically adapt to the current theme:

### Core Colors

- `--color-background`: Main background color
- `--color-background-secondary`: Secondary background (cards, sections)
- `--color-background-tertiary`: Tertiary background (hover states, inputs)
- `--color-foreground`: Main text color
- `--color-foreground-muted`: Muted text
- `--color-foreground-subtle`: Subtle text (descriptions, hints)
- `--color-border`: Default border color
- `--color-border-hover`: Border color on hover
- `--color-border-focus`: Border color on focus

### Using in Components

Use these semantic tokens in your Tailwind classes:

```tsx
<div className="bg-background text-foreground border border-border">
<h1 className="text-foreground">Title</h1>
<p className="text-foreground-muted">Description</p>
</div>
```

## Migrating Components

To migrate existing components to use the theming system:

### Before
```tsx
<div className="bg-neutral-0 text-neutral-900 border-neutral-200">
Content
</div>
```

### After
```tsx
<div className="bg-background text-foreground border-border">
Content
</div>
```

## Theme Modes

The system supports three theme modes:

1. **light**: Light theme
2. **dark**: Dark theme
3. **system**: Follows the user's system preference

## Customization

You can customize theme colors by modifying the CSS variables in `src/styles/index.css`:

```css
/* Light mode (default) */
@theme {
--color-background: var(--color-neutral-0);
--color-foreground: var(--color-neutral-900);
/* ... */
}

/* Dark mode */
.dark {
--color-background: #0a0a0a;
--color-foreground: #fafafa;
/* ... */
}
```

## Compatibility with shadcn/ui

This theming system is designed to be compatible with shadcn/ui components. You can now integrate shadcn/ui components directly into Hailstorm with minimal adjustments.

## Examples

See the Dialog component's "DarkMode" story in Storybook for a live example of the theming system in action.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@
"@headlessui/tailwindcss": "^0.2.1",
"@popperjs/core": "^2.11.8",
"@tailwindcss/forms": "^0.5.9",
"@tailwindcss/vite": "^4.1.13",
"@tanstack/react-table": "^8.20.5",
"@tanstack/react-virtual": "^3.10.8",
"@tanstack/table-core": "^8.20.5",
Expand All @@ -72,6 +71,7 @@
"@storybook/react-vite": "^8.3.6",
"@svgr/cli": "8.1.0",
"@tailwindcss/postcss": "^4.1.13",
"@tailwindcss/vite": "^4.1.13",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "6.6.2",
"@testing-library/react": "^16.0.1",
Expand Down
6 changes: 3 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

56 changes: 55 additions & 1 deletion src/components/alert/alert.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Meta, StoryObj } from "@storybook/react";

import React from "react";
import React, { useState, useEffect } from "react";
import { Alert, AlertProps } from "./alert";
import { getStoryDescription, hiddenArgControl } from "../../util/storybook-utils";

Expand Down Expand Up @@ -43,3 +43,57 @@ export const OnlyTitles: Story = {
args: { children: undefined },
argTypes: { ...Intents.argTypes, children: hiddenArgControl },
};

export const DarkMode: Story = {
argTypes: {
intent: hiddenArgControl,
children: hiddenArgControl,
},
render: () => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const [theme, setTheme] = useState<"light" | "dark">("light");

// eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(() => {
document.documentElement.classList.remove("light", "dark");
document.documentElement.classList.add(theme);
}, [theme]);

return (
<div className="bg-background min-h-[400px] p-8">
<div className="mb-6">
<button
type="button"
onClick={() => setTheme(theme === "light" ? "dark" : "light")}
className="bg-background-tertiary text-foreground border-border rounded border px-4 py-2 shadow-sm"
>
Toggle {theme === "light" ? "Dark" : "Light"} Mode
</button>
</div>

<div className="space-y-4">
<Alert intent="info" title="Information">
This is an informational alert that adapts to light and dark themes.
</Alert>
<Alert intent="success" title="Success">
Operation completed successfully! The changes have been saved.
</Alert>
<Alert intent="warning" title="Warning">
Please review your settings before proceeding with this action.
</Alert>
<Alert intent="danger" title="Danger">
An error occurred while processing your request. Please try again.
</Alert>
</div>

<div className="bg-background-secondary border-border mt-6 rounded border p-4">
<p className="text-foreground-muted text-sm">
Note: Alert colors remain consistent across themes to maintain semantic
meaning. The background container demonstrates theme-aware background and
text colors.
</p>
</div>
</div>
);
},
};
4 changes: 2 additions & 2 deletions src/components/alert/alert.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const Alert = ({ title, children, intent, className, ...props }: AlertPro
<div
role="alert"
className={classNames(
"flex flex-row gap-4 rounded-lg border px-4 py-3 text-neutral-800",
"flex flex-row gap-4 rounded-lg border px-4 py-3",
alertVariants[intent],
className
)}
Expand All @@ -47,7 +47,7 @@ export const Alert = ({ title, children, intent, className, ...props }: AlertPro
<Icon className={classNames("h-4 w-4 shrink-0", iconVariants[intent])} />
<div className="grow">
<div className="text-sm font-medium">{title}</div>
{children && <div className="pt-1 text-sm text-neutral-800">{children}</div>}
{children && <div className="pt-1 text-sm opacity-90">{children}</div>}
</div>
</div>
);
Expand Down
99 changes: 98 additions & 1 deletion src/components/button/button.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react";
import React from "react";
import React, { useState, useEffect } from "react";
import { Button, ButtonProps } from "./button";
import { ChatIcon, DiagramTreeIcon, LockIcon } from "../../icons";
import { hiddenArgControl } from "../../util/storybook-utils";
Expand Down Expand Up @@ -63,3 +63,100 @@ export const Types: Story = {
</div>
),
};

export const DarkMode: Story = {
argTypes: {
variant: hiddenArgControl,
LeftIcon: hiddenArgControl,
RightIcon: hiddenArgControl,
loading: hiddenArgControl,
},
render: () => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const [theme, setTheme] = useState<"light" | "dark">("light");

// eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(() => {
document.documentElement.classList.remove("light", "dark");
document.documentElement.classList.add(theme);
}, [theme]);

return (
<div className="bg-background min-h-[400px] p-8">
<div className="mb-6">
<button
type="button"
onClick={() => setTheme(theme === "light" ? "dark" : "light")}
className="bg-background-tertiary text-foreground border-border rounded border px-4 py-2 shadow-sm"
>
Toggle {theme === "light" ? "Dark" : "Light"} Mode
</button>
</div>

<div className="space-y-4">
<div>
<h3 className="text-foreground mb-2 font-semibold">Primary Buttons</h3>
<div className="flex gap-2">
<Button variant="primary">Primary</Button>
<Button variant="primary" LeftIcon={ChatIcon}>
With Icon
</Button>
<Button variant="primary" loading>
Loading
</Button>
<Button variant="primary" disabled>
Disabled
</Button>
</div>
</div>

<div>
<h3 className="text-foreground mb-2 font-semibold">Secondary Buttons</h3>
<div className="flex gap-2">
<Button variant="secondary">Secondary</Button>
<Button variant="secondary" LeftIcon={DiagramTreeIcon}>
With Icon
</Button>
<Button variant="secondary" loading>
Loading
</Button>
<Button variant="secondary" disabled>
Disabled
</Button>
</div>
</div>

<div>
<h3 className="text-foreground mb-2 font-semibold">Minimal Buttons</h3>
<div className="flex gap-2">
<Button variant="minimal">Minimal</Button>
<Button variant="minimal" LeftIcon={LockIcon}>
With Icon
</Button>
<Button variant="minimal" loading>
Loading
</Button>
<Button variant="minimal" disabled>
Disabled
</Button>
</div>
</div>

<div>
<h3 className="text-foreground mb-2 font-semibold">Danger Buttons</h3>
<div className="flex gap-2">
<Button variant="danger">Danger</Button>
<Button variant="danger" LeftIcon={ChatIcon}>
Delete
</Button>
<Button variant="danger-secondary">Cancel</Button>
<Button variant="danger-secondary" disabled>
Disabled
</Button>
</div>
</div>
</div>
</div>
);
},
};
10 changes: 5 additions & 5 deletions src/components/button/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,20 @@ const buttonVariants = {
primary:
"bg-primary-500 text-neutral-0 hover:bg-primary-600 active:bg-primary-600 focus:ring-2 focus:ring-primary-200 focus:bg-primary-600 disabled:bg-primary-200 fill-neutral-0",
secondary:
"text-neutral-700 bg-neutral-0 border border-neutral-400 hover:border-neutral-600 hover:text-neutral-800 active:bg-neutral-100 focus:ring-2 focus:ring-primary-200 focus:text-neutral-800 disabled:text-neutral-500 disabled:border-neutral-300 disabled:bg-neutral-0 fill-neutral-0",
"text-foreground bg-background border border-border hover:border-border-hover hover:text-foreground active:bg-background-tertiary focus:ring-2 focus:ring-primary-200 focus:text-foreground disabled:text-foreground-subtle disabled:border-border disabled:bg-background fill-foreground-muted",
minimal:
"text-neutral-700 hover:bg-neutral-100 hover:text-neutral-800 active:bg-neutral-200 focus:ring-2 focus:ring-primary-200 focus:text-neutral-800 disabled:text-neutral-500 disabled:bg-neutral-0 fill-neutral-0 underline",
"text-foreground hover:bg-background-tertiary hover:text-foreground active:bg-background-secondary focus:ring-2 focus:ring-primary-200 focus:text-foreground disabled:text-foreground-subtle disabled:bg-background fill-foreground-muted underline",
danger: "text-neutral-0 bg-danger-500 hover:bg-danger-500 active:bg-danger-700 focus:ring-2 focus:ring-danger-100 focus:bg-danger-600 disabled:bg-danger-100 fill-neutral-0",
"danger-secondary":
"bg-neutral-0 text-danger-500 border border-danger-400 hover:bg-danger-50 hover:text-danger-600 active:border-danger-700 active:text-danger-700 active:bg-danger-100 focus:ring-2 focus:ring-danger-100 focus:text-danger-600 disabled:border-danger-100 disabled:text-danger-100 disabled:bg-neutral-0 fill-danger-600 disabled:fill-danger-100",
"bg-background text-danger-500 border border-danger-400 hover:bg-danger-50 hover:text-danger-600 active:border-danger-700 active:text-danger-700 active:bg-danger-100 focus:ring-2 focus:ring-danger-100 focus:text-danger-600 disabled:border-danger-100 disabled:text-danger-100 disabled:bg-background fill-danger-600 disabled:fill-danger-100",
};

const iconVariants = {
primary: "text-neutral-0",
secondary:
"fill-neutral-600 group-hover:text-neutral-700 group-focus:text-neutral-700 group-disabled:text-neutral-400",
"fill-foreground-muted group-hover:text-foreground group-focus:text-foreground group-disabled:text-foreground-subtle",
minimal:
"fill-neutral-600 group-hover:text-neutral-700 group-focus:text-neutral-700 group-disabled:text-neutral-400",
"fill-foreground-muted group-hover:text-foreground group-focus:text-foreground group-disabled:text-foreground-subtle",
danger: "",
"danger-secondary": "",
};
Expand Down
Loading