Skip to content
Open
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
2 changes: 2 additions & 0 deletions apps/www/src/components/demo/demo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import {
} from '../dataview-demo';
import LinearMenuDemo from '../linear-menu-demo';
import PopoverColorPicker from '../popover-color-picker';
import ThemePanelDemo from '../theme-panel-demo';
import TourDemo from '../tour-demo';
import DemoPlayground from './demo-playground';
import DemoPreview from './demo-preview';
Expand Down Expand Up @@ -88,6 +89,7 @@ export default function Demo(props: DemoProps) {
ChipInputDemo,
LinearMenuDemo,
PopoverColorPicker,
ThemePanelDemo,
TourDemo,
NextLink,
AlignCenter,
Expand Down
150 changes: 150 additions & 0 deletions apps/www/src/components/theme-panel-demo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
'use client';

import {
ACCENT_COLORS,
Avatar,
Badge,
Button,
Callout,
Checkbox,
Chip,
Flex,
GRAY_COLOR_VALUES,
Input,
PANEL_BACKGROUNDS,
Popover,
Progress,
RADII,
SCALINGS,
Select,
Separator,
Switch,
Text,
ThemePreview,
type ThemeSettings,
Tooltip,
useThemePreview
} from '@raystack/apsara';
import { useState } from 'react';

const APPEARANCES = ['light', 'dark', 'system'] as const;

/** A live control for every setting, next to a sampler of components. */
function Controls() {
const { value, resolved, setValue } = useThemePreview();

const field = <K extends keyof ThemeSettings>(
label: string,
key: K,
options: readonly string[]
) => (
<Flex direction='column' gap={2} key={key}>
<Text size='mini' variant='secondary'>
{label}
</Text>
<Select
value={value[key]}
onValueChange={next => setValue({ [key]: next as ThemeSettings[K] })}
>
<Select.Trigger style={{ width: 180 }}>
<Select.Value />
</Select.Trigger>
<Select.Content>
{options.map(option => (
<Select.Item key={option} value={option}>
{option}
</Select.Item>
))}
</Select.Content>
</Select>
</Flex>
);

return (
<Flex direction='column' gap={4} style={{ minWidth: 200 }}>
{field('Appearance', 'appearance', APPEARANCES)}
{field('Accent', 'accentColor', ACCENT_COLORS)}
{field('Gray', 'grayColor', GRAY_COLOR_VALUES)}
{field('Radius', 'radius', RADII)}
{field('Scaling', 'scaling', SCALINGS)}
{field('Panel', 'panelBackground', PANEL_BACKGROUNDS)}
{field('Reduced motion', 'reducedMotion', ['system', 'true', 'false'])}

<Separator />
<Text size='mini' variant='secondary'>
Resolved: {resolved.appearance} · {resolved.grayColor}
</Text>
</Flex>
);
}

function Sampler() {
const [checked, setChecked] = useState(true);

return (
<Flex direction='column' gap={5} style={{ flex: 1, minWidth: 260 }}>
<Flex gap={3} align='center' wrap='wrap'>
<Button>Primary</Button>
<Button variant='outline'>Outline</Button>
<Button variant='ghost' radius='full'>
radius=&quot;full&quot;
</Button>
</Flex>

<Flex gap={3} align='center' wrap='wrap'>
<Avatar fallback='AP' />
<Badge>Badge</Badge>
<Chip>Chip</Chip>
<Switch checked={checked} onCheckedChange={setChecked} />
<Checkbox defaultChecked />
</Flex>

<Input placeholder='Input' />
<Progress value={62} />

<Callout>Callouts follow the accent and the radius factor.</Callout>

<Flex gap={3}>
<Tooltip>
<Tooltip.Trigger
render={<Button variant='outline'>Tooltip</Button>}
/>
<Tooltip.Content>Portalled, and still themed</Tooltip.Content>
</Tooltip>

<Popover>
<Popover.Trigger
render={<Button variant='outline'>Popover</Button>}
/>
<Popover.Content>
<Text>
Theme values cross the portal through context, so this popup
matches the scope it was opened from.
</Text>
</Popover.Content>
</Popover>
</Flex>
</Flex>
);
}

/** `isRoot={false}`: one example on a page, not the page itself. */
export default function ThemePanelDemo() {
return (
<ThemePreview
isRoot={false}
defaultValue={{ appearance: 'light' }}
style={{
padding: 'var(--rs-space-5)',
border: '1px solid var(--rs-color-border-base-primary)',
borderRadius: 'var(--rs-radius-4)'
}}
>
<Flex gap={7} wrap='wrap' align='start'>
<Controls />
<Separator orientation='vertical' />
<Sampler />
</Flex>
</ThemePreview>
);
}
1 change: 1 addition & 0 deletions apps/www/src/content/docs/theme/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"title": "Theme",
"pages": [
"overview",
"preview",
"colors",
"typography",
"spacing",
Expand Down
8 changes: 8 additions & 0 deletions apps/www/src/content/docs/theme/overview/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ import { switcherDemo, switcherSizeDemo } from "./demo.ts";

Apsara provides a theming system built on CSS custom properties (tokens). Tokens are semantic variables that automatically resolve to appropriate values based on the active theme—so your UI adapts seamlessly when users switch between light and dark modes or when you change accent colors, without any code changes.

<Callout type="grey">
This page documents the original `Theme` component, which continues to ship
unchanged. New applications should use
[`ThemePreview`](/docs/theme/preview) — it server-renders, allows more than
one provider per page, works inside portals, and adds radius, scaling, panel
background and reduced-motion settings. It includes a migration guide.
</Callout>

## Installation

Wrap your application with the `Theme` component:
Expand Down
107 changes: 107 additions & 0 deletions apps/www/src/content/docs/theme/preview/demo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
'use client';

export const scopeDemo = {
type: 'code',
code: `
<Flex gap={5} align="start">
<ThemePreview isRoot={false} defaultValue={{ appearance: "light" }} style={{ padding: 16, borderRadius: 8 }}>
<Flex direction="column" gap={3}>
<Text>Light scope</Text>
<Button>Primary</Button>
</Flex>
</ThemePreview>

<ThemePreview isRoot={false} defaultValue={{ appearance: "dark" }} style={{ padding: 16, borderRadius: 8 }}>
<Flex direction="column" gap={3}>
<Text>Dark scope</Text>
<Button>Primary</Button>
</Flex>
</ThemePreview>
</Flex>`
};

export const accentDemo = {
type: 'code',
code: `
<Flex gap={5} align="start">
{["indigo", "orange", "mint"].map(accent => (
<ThemePreview
key={accent}
isRoot={false}
defaultValue={{ accentColor: accent }}
hasBackground={false}
>
<Flex direction="column" gap={3} align="start">
<Text>{accent}</Text>
<Button>Primary</Button>
<Badge>Badge</Badge>
</Flex>
</ThemePreview>
))}
</Flex>`
};

export const radiusDemo = {
type: 'code',
code: `
<Flex gap={5} align="start">
{["none", "small", "medium", "large", "full"].map(radius => (
<ThemePreview
key={radius}
isRoot={false}
defaultValue={{ radius }}
hasBackground={false}
>
<Flex direction="column" gap={3} align="start">
<Text>{radius}</Text>
<Button>Primary</Button>
</Flex>
</ThemePreview>
))}
</Flex>`
};

export const scalingDemo = {
type: 'code',
code: `
<Flex gap={5} align="start">
{["0.9", "1", "1.1"].map(scaling => (
<ThemePreview
key={scaling}
isRoot={false}
defaultValue={{ scaling }}
hasBackground={false}
>
<Flex direction="column" gap={3} align="start">
<Text>{scaling}x</Text>
<Button>Primary</Button>
</Flex>
</ThemePreview>
))}
</Flex>`
};

export const componentRadiusDemo = {
type: 'code',
code: `
<ThemePreview isRoot={false} defaultValue={{ radius: "large" }} hasBackground={false}>
<Flex gap={3} align="center">
{/* Follows the theme */}
<Button>Large</Button>
{/* Overrides it, without compounding */}
<Button radius="none">None</Button>
<Button radius="small">Small</Button>
<Button radius="full">Full</Button>
</Flex>
</ThemePreview>`
};

export const switcherDemo = {
type: 'code',
code: `<ThemePreviewSwitcher />`
};

export const panelDemo = {
type: 'code',
code: `<ThemePanelDemo />`
};
Loading
Loading