mirror of https://github.com/usememos/memos
chore: unify colors
parent
2e474d37fd
commit
35928ce5ba
@ -0,0 +1,309 @@
|
||||
# Color System Guide
|
||||
|
||||
This document explains the color system used in the Memos application, built with OKLCH color space for better perceptual uniformity and accessibility.
|
||||
|
||||
## Overview
|
||||
|
||||
The color system supports both light and dark themes automatically through CSS custom properties. All colors are defined using OKLCH (Oklab LCH) color space, which provides better perceptual uniformity than traditional RGB/HSL.
|
||||
|
||||
## Color Categories
|
||||
|
||||
### 🎨 Primary Brand Colors
|
||||
|
||||
| Variable | Light Theme | Dark Theme | Usage |
|
||||
| ---------------------- | ------------- | --------------- | ------------------------------ |
|
||||
| `--primary` | Golden yellow | Brighter golden | Main brand color, primary CTAs |
|
||||
| `--primary-foreground` | White | White | Text on primary backgrounds |
|
||||
|
||||
**When to use:**
|
||||
|
||||
- Call-to-action buttons
|
||||
- Active navigation items
|
||||
- Important links and highlights
|
||||
- Brand elements
|
||||
|
||||
```css
|
||||
/* Example usage */
|
||||
.cta-button {
|
||||
background: var(--primary);
|
||||
color: var(--primary-foreground);
|
||||
}
|
||||
```
|
||||
|
||||
### 🔘 Secondary Colors
|
||||
|
||||
| Variable | Light Theme | Dark Theme | Usage |
|
||||
| ------------------------ | ----------- | --------------- | ----------------------------- |
|
||||
| `--secondary` | Light gray | Very light gray | Supporting actions |
|
||||
| `--secondary-foreground` | Dark gray | Dark gray | Text on secondary backgrounds |
|
||||
|
||||
**When to use:**
|
||||
|
||||
- Secondary buttons
|
||||
- Less important actions
|
||||
- Alternative navigation items
|
||||
- Subtle highlights
|
||||
|
||||
### 📄 Background & Surface Colors
|
||||
|
||||
| Variable | Light Theme | Dark Theme | Usage |
|
||||
| ---------------------- | ----------- | ----------- | --------------------------- |
|
||||
| `--background` | Near white | Dark gray | Main page background |
|
||||
| `--card` | Near white | Dark gray | Card/container backgrounds |
|
||||
| `--card-foreground` | Very dark | Near white | Text on card backgrounds |
|
||||
| `--popover` | Pure white | Darker gray | Overlay backgrounds |
|
||||
| `--popover-foreground` | Dark gray | Light gray | Text on overlay backgrounds |
|
||||
|
||||
**When to use:**
|
||||
|
||||
- Page backgrounds (`--background`)
|
||||
- Content cards and panels (`--card`)
|
||||
- Tooltips, dropdowns, modals (`--popover`)
|
||||
|
||||
### ✏️ Text & Content Colors
|
||||
|
||||
| Variable | Light Theme | Dark Theme | Usage |
|
||||
| -------------------- | ----------- | ------------ | ------------------------ |
|
||||
| `--foreground` | Dark gray | Light gray | Primary text color |
|
||||
| `--muted` | Light gray | Very dark | Subtle background areas |
|
||||
| `--muted-foreground` | Medium gray | Medium light | Secondary text, captions |
|
||||
|
||||
**When to use:**
|
||||
|
||||
- Main body text (`--foreground`)
|
||||
- Helper text, placeholders (`--muted-foreground`)
|
||||
- Disabled text states
|
||||
- Subtle background sections (`--muted`)
|
||||
|
||||
### 🎯 Interactive Elements
|
||||
|
||||
| Variable | Light Theme | Dark Theme | Usage |
|
||||
| --------------------- | ------------ | ----------- | ---------------------------- |
|
||||
| `--accent` | Light gray | Very dark | Hover states, selected items |
|
||||
| `--accent-foreground` | Dark gray | Light gray | Text on accent backgrounds |
|
||||
| `--border` | Medium light | Medium dark | Dividers, input borders |
|
||||
| `--input` | Medium light | Medium dark | Form input backgrounds |
|
||||
| `--ring` | Blue | Blue | Focus outlines |
|
||||
|
||||
**When to use:**
|
||||
|
||||
- Hover states (`--accent`)
|
||||
- Form field borders (`--border`)
|
||||
- Input field backgrounds (`--input`)
|
||||
- Focus indicators (`--ring`)
|
||||
|
||||
### ⚠️ Feedback Colors
|
||||
|
||||
| Variable | Light Theme | Dark Theme | Usage |
|
||||
| -------------------------- | ----------- | ---------- | ------------------------------- |
|
||||
| `--destructive` | Very dark | Red | Error states, dangerous actions |
|
||||
| `--destructive-foreground` | White | White | Text on destructive backgrounds |
|
||||
|
||||
**When to use:**
|
||||
|
||||
- Error messages
|
||||
- Delete buttons
|
||||
- Warning alerts
|
||||
- Validation failures
|
||||
|
||||
### 📊 Data Visualization
|
||||
|
||||
| Variable | Purpose |
|
||||
| ----------- | --------------------------------------- |
|
||||
| `--chart-1` | Primary data series (golden) |
|
||||
| `--chart-2` | Secondary data series (purple) |
|
||||
| `--chart-3` | Tertiary data series (light) |
|
||||
| `--chart-4` | Quaternary data series (purple variant) |
|
||||
| `--chart-5` | Quinary data series (golden variant) |
|
||||
|
||||
**When to use:**
|
||||
|
||||
- Charts and graphs
|
||||
- Data visualization
|
||||
- Progress indicators
|
||||
- Statistical displays
|
||||
|
||||
### 🔧 Sidebar System
|
||||
|
||||
| Variable | Usage |
|
||||
| ------------------------------ | ---------------------------- |
|
||||
| `--sidebar` | Sidebar background |
|
||||
| `--sidebar-foreground` | Sidebar text |
|
||||
| `--sidebar-primary` | Active sidebar items |
|
||||
| `--sidebar-primary-foreground` | Text on active sidebar items |
|
||||
| `--sidebar-accent` | Sidebar hover states |
|
||||
| `--sidebar-accent-foreground` | Text on sidebar hover states |
|
||||
| `--sidebar-border` | Sidebar dividers |
|
||||
| `--sidebar-ring` | Sidebar focus indicators |
|
||||
|
||||
## Best Practices
|
||||
|
||||
### ✅ Do's
|
||||
|
||||
1. **Always pair colors correctly:**
|
||||
|
||||
```css
|
||||
/* Correct */
|
||||
background: var(--primary);
|
||||
color: var(--primary-foreground);
|
||||
```
|
||||
|
||||
2. **Use semantic meaning:**
|
||||
|
||||
- Primary = main actions
|
||||
- Secondary = supporting actions
|
||||
- Destructive = dangerous/delete actions
|
||||
- Muted = less important content
|
||||
|
||||
3. **Respect the design system:**
|
||||
- Use existing color tokens instead of custom colors
|
||||
- Maintain consistency across components
|
||||
|
||||
### ❌ Don'ts
|
||||
|
||||
1. **Don't mix incompatible pairs:**
|
||||
|
||||
```css
|
||||
/* Incorrect - poor contrast */
|
||||
background: var(--primary);
|
||||
color: var(--foreground);
|
||||
```
|
||||
|
||||
2. **Don't use colors outside their intended purpose:**
|
||||
|
||||
- Don't use destructive colors for positive actions
|
||||
- Don't use primary colors for secondary elements
|
||||
|
||||
3. **Don't hardcode color values:**
|
||||
|
||||
```css
|
||||
/* Bad */
|
||||
color: #333333;
|
||||
|
||||
/* Good */
|
||||
color: var(--foreground);
|
||||
```
|
||||
|
||||
## Theme Switching
|
||||
|
||||
The color system automatically adapts between light and dark themes when the `.dark` class is applied to a parent element (typically `<html>` or `<body>`):
|
||||
|
||||
```javascript
|
||||
// Toggle dark mode
|
||||
document.documentElement.classList.toggle("dark");
|
||||
```
|
||||
|
||||
## Accessibility
|
||||
|
||||
- All color pairs meet WCAG contrast requirements
|
||||
- Focus indicators use `--ring` for consistency
|
||||
- Color is never the only means of conveying information
|
||||
|
||||
## Implementation Examples
|
||||
|
||||
### Button Variants
|
||||
|
||||
```css
|
||||
/* Primary button */
|
||||
.btn-primary {
|
||||
background: var(--primary);
|
||||
color: var(--primary-foreground);
|
||||
border: 1px solid var(--primary);
|
||||
}
|
||||
|
||||
/* Secondary button */
|
||||
.btn-secondary {
|
||||
background: var(--secondary);
|
||||
color: var(--secondary-foreground);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* Destructive button */
|
||||
.btn-destructive {
|
||||
background: var(--destructive);
|
||||
color: var(--destructive-foreground);
|
||||
border: 1px solid var(--destructive);
|
||||
}
|
||||
```
|
||||
|
||||
### Form Elements
|
||||
|
||||
```css
|
||||
/* Input field */
|
||||
.input {
|
||||
background: var(--input);
|
||||
color: var(--foreground);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
outline: 2px solid var(--ring);
|
||||
}
|
||||
```
|
||||
|
||||
### Cards and Containers
|
||||
|
||||
```css
|
||||
/* Content card */
|
||||
.card {
|
||||
background: var(--card);
|
||||
color: var(--card-foreground);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* Popover/Modal */
|
||||
.popover {
|
||||
background: var(--popover);
|
||||
color: var(--popover-foreground);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
```
|
||||
|
||||
## Color Testing
|
||||
|
||||
To ensure proper contrast and accessibility:
|
||||
|
||||
1. Test both light and dark themes
|
||||
2. Verify readability at different zoom levels
|
||||
3. Check with colorblind simulation tools
|
||||
4. Validate WCAG contrast ratios
|
||||
|
||||
## Z-Index Hierarchy
|
||||
|
||||
The application uses a structured z-index hierarchy to ensure proper layering of UI components:
|
||||
|
||||
| Component Type | Z-Index | Usage |
|
||||
| ----------------- | -------- | ------------------------------------- |
|
||||
| **Base Content** | `z-0` | Normal page content |
|
||||
| **Overlays** | `z-50` | Dialog/Sheet backgrounds |
|
||||
| **Modal Content** | `z-50` | Dialog/Sheet content |
|
||||
| **Dropdowns** | `z-[60]` | Select, DropdownMenu, Popover content |
|
||||
| **Tooltips** | `z-[70]` | Tooltip content (highest priority) |
|
||||
|
||||
### Rules
|
||||
|
||||
1. **Dialog/Sheet**: Use `z-50` for both overlay and content
|
||||
2. **Interactive Elements**: Use `z-[60]` for dropdowns inside dialogs
|
||||
3. **Tooltips**: Use `z-[70]` to appear above all other elements
|
||||
4. **Always test**: Ensure Select/DropdownMenu works inside Dialog/Sheet
|
||||
|
||||
### Example
|
||||
|
||||
```tsx
|
||||
// ✅ Correct: Select inside Dialog will appear above dialog content
|
||||
<Dialog>
|
||||
<DialogContent>
|
||||
<Select>
|
||||
<SelectContent className="z-[60]">
|
||||
{" "}
|
||||
{/* Higher than dialog */}
|
||||
<SelectItem>Option 1</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
_This color system is designed to provide a consistent, accessible, and beautiful user experience across all themes and components._
|
@ -0,0 +1,205 @@
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function DropdownMenu({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuPortal({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||
return <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuTrigger({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
||||
return <DropdownMenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuContent({ className, sideOffset = 4, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
data-slot="dropdown-menu-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-[60] max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuGroup({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||
return <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuItem({
|
||||
className,
|
||||
inset,
|
||||
variant = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
variant?: "default" | "destructive";
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Item
|
||||
data-slot="dropdown-menu-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuCheckboxItem({
|
||||
className,
|
||||
children,
|
||||
checked,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuRadioGroup({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
||||
return <DropdownMenuPrimitive.RadioGroup data-slot="dropdown-menu-radio-group" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuRadioItem({ className, children, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CircleIcon className="size-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuLabel({
|
||||
className,
|
||||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Label
|
||||
data-slot="dropdown-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn("px-2 py-1.5 text-sm font-medium data-[inset]:pl-8", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSeparator({ className, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
data-slot="dropdown-menu-separator"
|
||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
className={cn("text-muted-foreground ml-auto text-xs tracking-widest", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSub({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuSubTrigger({
|
||||
className,
|
||||
inset,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto size-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSubContent({ className, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-[60] min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuSubContent,
|
||||
};
|
Loading…
Reference in New Issue