feat: design system, theme, and layout improvements
Update color tokens, motion, spacing, typography. Enhance ThemeProvider and ThemeSwitcher. Refine layout components (Header, Sidebar, Navbar, MobileBottomNav, DashboardLayout). CSS overhaul in index.css. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
29a40f2dcf
commit
8ab0da6041
12 changed files with 1008 additions and 368 deletions
|
|
@ -1,6 +1,7 @@
|
|||
import type { ReactNode } from 'react';
|
||||
import { Header } from './Header';
|
||||
import { Sidebar } from './Sidebar';
|
||||
import { MobileBottomNav } from './MobileBottomNav';
|
||||
import { AnnouncementBanner } from '../feedback/AnnouncementBanner';
|
||||
import { GlobalPlayer } from '@/features/player/components/GlobalPlayer';
|
||||
import { useQueueSync } from '@/features/player/hooks/useQueueSync';
|
||||
|
|
@ -14,12 +15,21 @@ interface DashboardLayoutProps {
|
|||
|
||||
/**
|
||||
* Layout principal "App Shell" - Veza Professional V2
|
||||
*
|
||||
*
|
||||
* Architecture:
|
||||
* - Body: Fixed viewport (overflow-hidden)
|
||||
* - Sidebar: Fixed left, z-index high
|
||||
* - Sidebar: Fixed left, z-index high (desktop only)
|
||||
* - MobileBottomNav: Fixed bottom (mobile only, lg:hidden)
|
||||
* - Main: Scrollable container independent of window
|
||||
* - Header: Sticky top within Main
|
||||
* - Player: Fixed above bottom nav (mobile) / above content (desktop)
|
||||
*
|
||||
* Z-index hierarchy (bottom to top):
|
||||
* z-raised (10) — main content
|
||||
* z-40 — MobileBottomNav
|
||||
* z-player — GlobalPlayer (z-sticky = 200)
|
||||
* z-sidebar — Sidebar (95)
|
||||
* z-sticky — Header (200)
|
||||
*/
|
||||
export function DashboardLayout({ children }: DashboardLayoutProps) {
|
||||
const { sidebarOpen } = useUIStore();
|
||||
|
|
@ -30,7 +40,7 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
|
|||
{/* 1. Global Background (Fixed z-0) */}
|
||||
<AstralBackground />
|
||||
|
||||
{/* 2. Fixed Sidebar (z-90) */}
|
||||
{/* 2. Fixed Sidebar — desktop only (z-90) */}
|
||||
<Sidebar />
|
||||
|
||||
{/* 3. Main Content Area (The only thing that scrolls) */}
|
||||
|
|
@ -44,7 +54,7 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
|
|||
{/* Header is part of the flow but stays at top */}
|
||||
<Header />
|
||||
|
||||
{/* Scrollable Content Container */}
|
||||
{/* Scrollable Content Container — bottom padding accounts for player + mobile nav */}
|
||||
<main
|
||||
id="main-content"
|
||||
className="flex-1 overflow-y-auto overflow-x-hidden pt-main pb-main px-4 md:px-8 custom-scrollbar"
|
||||
|
|
@ -56,11 +66,14 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
|
|||
</div>
|
||||
</main>
|
||||
|
||||
{/* Floating Player: wrapper constrains width to main area; GlobalPlayer is fixed inside */}
|
||||
<div className="absolute bottom-0 left-0 right-0 z-50 w-full min-w-0" aria-label="Player bar container">
|
||||
{/* Floating Player — sits above MobileBottomNav on mobile */}
|
||||
<div className="absolute bottom-0 left-0 right-0 z-player w-full min-w-0" aria-label="Player bar container">
|
||||
<GlobalPlayer />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 4. Mobile Bottom Navigation — fixed, below player, above content */}
|
||||
<MobileBottomNav />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -59,7 +59,7 @@ export function Header(_props: HeaderProps) {
|
|||
return (
|
||||
<header data-testid="app-header" className="fixed top-0 left-0 right-0 h-header z-[var(--sumi-z-sticky)] pointer-events-none">
|
||||
<div className={cn(
|
||||
'absolute top-0 right-0 h-header bg-[var(--sumi-glass-bg)] backdrop-blur-[12px] border-b border-[var(--sumi-border-faint)] flex items-center justify-between px-4 md:px-6 pointer-events-auto transition-all duration-[var(--sumi-duration-fast)]',
|
||||
'absolute top-0 right-0 h-header bg-transparent backdrop-blur-[8px] flex items-center justify-between px-4 md:px-6 pointer-events-auto transition-all duration-300',
|
||||
sidebarOpen ? 'left-header-expanded' : 'left-header-collapsed',
|
||||
'max-lg:left-0'
|
||||
)}>
|
||||
|
|
@ -67,12 +67,23 @@ export function Header(_props: HeaderProps) {
|
|||
{/* Mobile Sidebar Toggle */}
|
||||
<button
|
||||
onClick={() => setSidebarOpen(!sidebarOpen)}
|
||||
className="lg:hidden p-2 rounded-lg hover:bg-muted/50 text-muted-foreground hover:text-foreground mr-2 transition-colors duration-[var(--duration-fast)]"
|
||||
className="lg:hidden p-2 rounded-lg hover:bg-muted/50 text-muted-foreground hover:text-foreground mr-2 transition-colors duration-[var(--sumi-duration-fast)]"
|
||||
aria-label="Toggle sidebar"
|
||||
>
|
||||
<Menu className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
{/* Search — Spotify-style: pill shape, smooth focus expansion */}
|
||||
{/* Mobile Search Button — navigates to search page */}
|
||||
<button
|
||||
onClick={() => navigate('/search')}
|
||||
className="md:hidden flex-1 flex items-center gap-2 h-9 px-3.5 rounded-full bg-[var(--sumi-surface-subtle)] text-muted-foreground/50 text-sm mr-2 transition-colors duration-[var(--sumi-duration-fast)] active:bg-[var(--sumi-bg-hover)]"
|
||||
aria-label={t('header.searchAriaLabel')}
|
||||
>
|
||||
<Search className="w-4 h-4 shrink-0" />
|
||||
<span className="truncate">{t('header.searchPlaceholder')}</span>
|
||||
</button>
|
||||
|
||||
{/* Search — Spotify-style: pill shape, smooth focus expansion (desktop) */}
|
||||
<div className="flex-1 max-w-lg relative hidden md:block">
|
||||
<div
|
||||
role="search"
|
||||
|
|
@ -85,12 +96,12 @@ export function Header(_props: HeaderProps) {
|
|||
placeholder={t('header.searchPlaceholder')}
|
||||
aria-label={t('header.searchAriaLabel')}
|
||||
className={cn(
|
||||
'w-full h-10 pl-10 pr-20 rounded-full text-sm text-foreground',
|
||||
'bg-[var(--sumi-surface-subtle)] border border-transparent',
|
||||
'placeholder:text-muted-foreground/50',
|
||||
'focus:outline-none focus:bg-[var(--sumi-surface-card)] focus:border-[var(--sumi-border-accent)] focus:shadow-[var(--sumi-shadow-glow)]',
|
||||
'hover:bg-[var(--sumi-bg-hover)]',
|
||||
'transition-all duration-[var(--sumi-duration-normal)] ease-[var(--sumi-ease-out)]',
|
||||
'w-full h-10 pl-10 pr-20 rounded-sm text-sm text-foreground font-heading',
|
||||
'bg-transparent border-b border-[var(--sumi-border-faint)] border-t-0 border-l-0 border-r-0',
|
||||
'placeholder:text-muted-foreground/30',
|
||||
'focus:outline-none focus:border-b-[var(--sumi-accent)] focus:bg-transparent',
|
||||
'hover:border-b-[var(--sumi-border-default)]',
|
||||
'transition-all duration-300',
|
||||
)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
|
|
@ -109,9 +120,9 @@ export function Header(_props: HeaderProps) {
|
|||
{/* Right Actions */}
|
||||
<div className="flex items-center gap-1 ml-2">
|
||||
|
||||
<div className="hidden xl:flex items-center gap-2 mr-2 px-2.5 py-1 rounded-full bg-muted/30 text-muted-foreground">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-primary shrink-0" />
|
||||
<span className="text-xs">{t('header.online')}</span>
|
||||
<div className="hidden xl:flex items-center gap-2 mr-2 px-2 py-1 text-muted-foreground/40">
|
||||
<span className="w-1 h-1 rounded-full bg-[var(--sumi-sage)] shrink-0" />
|
||||
<span className="text-[10px] tracking-[0.1em] font-heading" style={{ fontWeight: 300 }}>{t('header.online')}</span>
|
||||
</div>
|
||||
|
||||
<NotificationMenu />
|
||||
|
|
@ -151,7 +162,7 @@ export function Header(_props: HeaderProps) {
|
|||
|
||||
{isUserMenuOpen && (
|
||||
<FocusTrap active={isUserMenuOpen} onEscape={() => setIsUserMenuOpen(false)}>
|
||||
<div className="absolute right-0 top-full mt-2 w-64 bg-[var(--sumi-surface-elevated)] backdrop-blur-2xl border border-[var(--sumi-glass-border)] rounded-2xl shadow-2xl p-1.5 z-50 animate-scaleIn origin-top-right ring-1 ring-white/5">
|
||||
<div className="absolute right-0 top-full mt-2 w-64 bg-[var(--sumi-surface-elevated)] backdrop-blur-2xl border border-[var(--sumi-border-faint)] rounded-sm shadow-2xl p-1.5 z-50 animate-ink-reveal origin-top-right">
|
||||
<div className="px-3 py-3 border-b border-[var(--sumi-border-faint)] mb-1">
|
||||
<p className="text-sm font-semibold text-foreground truncate">{user?.username}</p>
|
||||
<p className="text-xs text-muted-foreground truncate mt-0.5">{user?.email}</p>
|
||||
|
|
|
|||
|
|
@ -1,21 +1,26 @@
|
|||
import { useLocation, Link } from 'react-router-dom';
|
||||
import { Home, Search, Layers, MessageSquare, User } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const navItems = [
|
||||
{ id: 'home', label: 'Home', icon: Home, path: '/dashboard' },
|
||||
{ id: 'search', label: 'Search', icon: Search, path: '/search' },
|
||||
{ id: 'library', label: 'Library', icon: Layers, path: '/library' },
|
||||
{ id: 'chat', label: 'Chat', icon: MessageSquare, path: '/chat' },
|
||||
{ id: 'profile', label: 'Profile', icon: User, path: '/settings' },
|
||||
{ id: 'home', labelKey: 'nav.items.dashboard', icon: Home, path: '/dashboard' },
|
||||
{ id: 'search', labelKey: 'nav.items.discover', icon: Search, path: '/discover' },
|
||||
{ id: 'library', labelKey: 'nav.items.tracks', icon: Layers, path: '/library' },
|
||||
{ id: 'chat', labelKey: 'nav.items.chat', icon: MessageSquare, path: '/chat' },
|
||||
{ id: 'profile', labelKey: 'nav.settings', icon: User, path: '/settings' },
|
||||
];
|
||||
|
||||
export function MobileBottomNav() {
|
||||
const location = useLocation();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<nav className="fixed bottom-0 left-0 right-0 z-40 bg-background/95 backdrop-blur-lg border-t border-border/50 lg:hidden">
|
||||
<div className="flex items-center justify-around px-2 py-1">
|
||||
<nav
|
||||
className="fixed bottom-0 left-0 right-0 z-40 bg-[var(--sumi-glass-bg)] backdrop-blur-xl border-t border-[var(--sumi-border-faint)] lg:hidden"
|
||||
aria-label="Mobile navigation"
|
||||
>
|
||||
<div className="flex items-center justify-around px-1 py-1">
|
||||
{navItems.map((item) => {
|
||||
const isActive = location.pathname.startsWith(item.path);
|
||||
const Icon = item.icon;
|
||||
|
|
@ -24,8 +29,9 @@ export function MobileBottomNav() {
|
|||
<Link
|
||||
key={item.id}
|
||||
to={item.path}
|
||||
aria-current={isActive ? 'page' : undefined}
|
||||
className={cn(
|
||||
'relative flex flex-col items-center justify-center gap-0.5 py-2 px-3 min-w-12 min-h-12 rounded-xl transition-colors',
|
||||
'relative flex flex-col items-center justify-center gap-0.5 py-2 px-3 min-w-12 min-h-12 rounded-xl transition-colors duration-[var(--sumi-duration-fast)]',
|
||||
isActive
|
||||
? 'text-primary'
|
||||
: 'text-muted-foreground active:text-foreground',
|
||||
|
|
@ -34,10 +40,10 @@ export function MobileBottomNav() {
|
|||
<Icon className={cn('h-5 w-5', isActive && 'fill-primary/20')} />
|
||||
{/* text-[10px] exception: Tailwind has no scale below text-xs (12px); 10px is standard for mobile bottom nav labels */}
|
||||
<span className="text-[10px] font-medium leading-none">
|
||||
{item.label}
|
||||
{t(item.labelKey)}
|
||||
</span>
|
||||
{isActive && (
|
||||
<span className="absolute top-0 left-1/2 -translate-x-1/2 w-4 h-0.5 rounded-full bg-primary" />
|
||||
<span className="absolute top-0.5 left-1/2 -translate-x-1/2 w-5 h-0.5 rounded-full bg-primary" />
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -185,7 +185,7 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, onLogout }) => {
|
|||
<div className="w-9 h-9 rounded-full bg-border p-px hover:ring-2 hover:ring-border transition-all">
|
||||
<div className="w-full h-full rounded-full overflow-hidden">
|
||||
<img
|
||||
src="https://picsum.photos/100/100"
|
||||
src=""
|
||||
alt="Avatar"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -2,10 +2,11 @@ import React, { useMemo, useState, useEffect } from 'react';
|
|||
import { useLocation, Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Home, Users, Disc, Radio, Settings, LogOut, ShoppingBag, Music2,
|
||||
Home, Users, Radio, Settings, LogOut, ShoppingBag, Music2,
|
||||
BarChart2, Shield, Box, MessageSquare,
|
||||
Layers, Heart, ListMusic, CreditCard, DollarSign, Terminal,
|
||||
Heart, ListMusic, CreditCard, DollarSign, Terminal,
|
||||
ChevronLeft, ChevronRight, Compass, Headphones,
|
||||
Cloud, Crown, Share2, GraduationCap, HelpCircle,
|
||||
} from 'lucide-react';
|
||||
import { NavItem } from '../../types';
|
||||
import { useUIStore } from '@/stores/ui';
|
||||
|
|
@ -27,6 +28,7 @@ const sectionKeys: Record<string, string> = {
|
|||
connect: 'nav.sections.connect',
|
||||
library: 'nav.sections.library',
|
||||
more: 'nav.sections.more',
|
||||
tools: 'nav.sections.tools',
|
||||
system: 'nav.sections.system',
|
||||
};
|
||||
|
||||
|
|
@ -47,19 +49,25 @@ const iconMap: Record<string, React.ReactNode> = {
|
|||
sell: <DollarSign className="w-4 h-4" />,
|
||||
wishlist: <Heart className="w-4 h-4" />,
|
||||
purchases: <CreditCard className="w-4 h-4" />,
|
||||
cloud: <Cloud className="w-4 h-4" />,
|
||||
subscription: <Crown className="w-4 h-4" />,
|
||||
distribution: <Share2 className="w-4 h-4" />,
|
||||
education: <GraduationCap className="w-4 h-4" />,
|
||||
support: <HelpCircle className="w-4 h-4" />,
|
||||
developer: <Terminal className="w-4 h-4" />,
|
||||
admin: <Shield className="w-4 h-4" />,
|
||||
};
|
||||
|
||||
// Badge data — static
|
||||
const badgeMap: Record<string, number> = { live: 3, chat: 12 };
|
||||
// Badge data — empty until connected to real notification/count APIs
|
||||
const badgeMap: Record<string, number> = {};
|
||||
|
||||
// Navigation structure — streamlined for music platform UX
|
||||
const navStructure: { sectionKey: string; itemIds: string[] }[] = [
|
||||
{ sectionKey: 'home', itemIds: ['dashboard', 'discover', 'feed'] },
|
||||
{ sectionKey: 'library', itemIds: ['tracks', 'playlists', 'favoris'] },
|
||||
{ sectionKey: 'library', itemIds: ['tracks', 'playlists', 'favoris', 'cloud'] },
|
||||
{ sectionKey: 'connect', itemIds: ['live', 'chat', 'social'] },
|
||||
{ sectionKey: 'more', itemIds: ['marketplace', 'analytics', 'sell', 'purchases'] },
|
||||
{ sectionKey: 'more', itemIds: ['marketplace', 'analytics', 'sell', 'purchases', 'subscription'] },
|
||||
{ sectionKey: 'tools', itemIds: ['distribution', 'education', 'support'] },
|
||||
];
|
||||
|
||||
function buildNavItems(t: (key: string) => string): { section: string; items: NavItem[] }[] {
|
||||
|
|
@ -75,24 +83,26 @@ function buildNavItems(t: (key: string) => string): { section: string; items: Na
|
|||
}
|
||||
|
||||
const routeMap: Record<string, string> = {
|
||||
dashboard: '/dashboard', discover: '/search', feed: '/feed',
|
||||
dashboard: '/dashboard', discover: '/discover', feed: '/feed',
|
||||
tracks: '/library', playlists: '/playlists', favoris: '/playlists/favoris',
|
||||
gear: '/gear', live: '/live', chat: '/chat', social: '/social',
|
||||
marketplace: '/marketplace', analytics: '/analytics',
|
||||
sell: '/sell', wishlist: '/wishlist', purchases: '/purchases',
|
||||
cloud: '/cloud', subscription: '/subscription',
|
||||
distribution: '/distribution', education: '/education', support: '/support',
|
||||
developer: '/developer', admin: '/admin', settings: '/settings',
|
||||
};
|
||||
|
||||
const navItemBaseClasses = cn(
|
||||
'w-full flex items-center px-3 py-2 rounded-lg text-sm font-medium transition-all duration-200 ease-[cubic-bezier(0.34,1.56,0.64,1)] group relative',
|
||||
'hover:scale-[1.02] active:scale-[0.98]',
|
||||
'w-full flex items-center px-3 py-2 text-sm transition-all duration-300 ease-out group relative',
|
||||
'border-l-2 border-l-transparent rounded-none rounded-r-sm',
|
||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 focus-visible:ring-offset-2 focus-visible:ring-offset-background'
|
||||
);
|
||||
|
||||
const navItemInactiveClasses =
|
||||
'text-muted-foreground hover:text-foreground hover:bg-sidebar-accent active:bg-sidebar-accent/80';
|
||||
'text-muted-foreground/60 hover:text-foreground hover:border-l-[var(--sumi-border-strong)] font-light';
|
||||
|
||||
const navItemActiveClasses = 'bg-primary/12 text-primary font-semibold rounded-lg shadow-[0_0_12px_-3px] shadow-primary/20';
|
||||
const navItemActiveClasses = 'text-foreground font-normal border-l-[var(--sumi-accent)] bg-gradient-to-r from-[var(--sumi-accent-subtle)] to-transparent';
|
||||
|
||||
const LG_BREAKPOINT = 1024;
|
||||
|
||||
|
|
@ -169,24 +179,22 @@ export const Sidebar: React.FC<SidebarProps> = ({ currentView }) => {
|
|||
aria-label="Main sidebar"
|
||||
>
|
||||
{/* Header — Veza branding */}
|
||||
<div className="px-4 py-4 flex items-center gap-3 relative">
|
||||
<div className="w-8 h-8 rounded-xl bg-gradient-to-br from-primary to-primary/70 flex items-center justify-center flex-shrink-0 shadow-md shadow-primary/25 transition-transform duration-300 ease-[cubic-bezier(0.34,1.56,0.64,1)] hover:scale-105">
|
||||
<span className="text-primary-foreground font-bold text-base leading-none select-none" aria-hidden="true">V</span>
|
||||
<div className="px-4 py-5 flex items-center gap-3 relative">
|
||||
{/* Hanko seal — 判子 */}
|
||||
<div className="w-9 h-9 bg-[var(--sumi-accent)] rounded-sm flex items-center justify-center flex-shrink-0 hanko-seal transition-transform duration-300 hover:scale-105">
|
||||
<span className="text-[var(--sumi-bg-base)] font-heading text-lg leading-none select-none relative z-10 font-semibold" aria-hidden="true">V</span>
|
||||
</div>
|
||||
|
||||
<div className={cn(
|
||||
'transition-all duration-300 ease-[cubic-bezier(0.34,1.56,0.64,1)] overflow-hidden min-w-0',
|
||||
'transition-all duration-300 ease-out overflow-hidden min-w-0',
|
||||
sidebarOpen ? 'opacity-100 max-w-[160px]' : 'max-w-0 opacity-0'
|
||||
)}>
|
||||
<h2 className="text-sm font-bold text-foreground truncate tracking-wide">
|
||||
veza
|
||||
<h2 className="text-base font-heading text-foreground truncate tracking-[0.15em]" style={{ fontWeight: 300 }}>
|
||||
VEZA
|
||||
</h2>
|
||||
<div className="flex items-center gap-1.5 mt-0.5">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 shrink-0 animate-pulse" aria-hidden="true" />
|
||||
<span className="text-xs text-muted-foreground truncate">
|
||||
{t('nav.status.connected', 'Connected')}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-[9px] text-muted-foreground/40 tracking-[0.2em] font-heading" style={{ fontWeight: 300 }}>
|
||||
墨 STREAMING
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
|
|
@ -215,21 +223,22 @@ export const Sidebar: React.FC<SidebarProps> = ({ currentView }) => {
|
|||
{idx > 0 && (
|
||||
sidebarOpen ? (
|
||||
<div
|
||||
className="h-px bg-border/50 mx-3 my-1.5 transition-opacity duration-300"
|
||||
className="brush-divider mx-3 my-3 transition-opacity duration-300"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex justify-center my-2" aria-hidden="true">
|
||||
<span className="w-1 h-1 rounded-full bg-muted-foreground/40" />
|
||||
<div className="flex justify-center my-3" aria-hidden="true">
|
||||
<span className="w-0.5 h-3 rounded-full bg-muted-foreground/15" />
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
<h3
|
||||
className={cn(
|
||||
'text-xs font-medium text-muted-foreground mb-2 px-3 transition-all duration-[var(--sumi-duration-normal)] uppercase tracking-wider',
|
||||
'text-[10px] text-muted-foreground/40 mb-2 px-3 transition-all duration-[var(--sumi-duration-normal)] uppercase tracking-[0.2em] font-heading',
|
||||
!sidebarOpen && 'opacity-0 h-0 overflow-hidden mb-0 px-0'
|
||||
)}
|
||||
style={{ fontWeight: 300 }}
|
||||
id={`sidebar-section-${group.section.replace(/\s+/g, '-').toLowerCase()}`}
|
||||
>
|
||||
{group.section}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { createContext, useContext, useEffect, useState } from 'react';
|
||||
import { createContext, useContext, useEffect, useRef, useState } from 'react';
|
||||
|
||||
export type Theme = 'dark' | 'light' | 'system';
|
||||
export type Contrast = 'normal' | 'high';
|
||||
|
|
@ -10,8 +10,72 @@ const STORAGE_KEYS = {
|
|||
density: 'veza-density',
|
||||
accentHue: 'veza-accent-hue',
|
||||
fontSize: 'veza-font-size',
|
||||
circadian: 'veza-circadian',
|
||||
eco: 'veza-eco',
|
||||
} as const;
|
||||
|
||||
// ═══ SUMI v4 — Shu vermillion base hue (朱 — 7° in HSL) ═══
|
||||
const SHU_HUE = 11;
|
||||
const SHU_SAT = 72;
|
||||
const SHU_LIT = 42;
|
||||
|
||||
// ═══ SUMI v4 — Circadian shifts ═══
|
||||
interface CircadianShift {
|
||||
warmth: number;
|
||||
brightness: number;
|
||||
}
|
||||
|
||||
function getCircadianShift(hour: number): CircadianShift {
|
||||
if (hour >= 5 && hour < 8) {
|
||||
return { warmth: 3, brightness: 1.02 };
|
||||
}
|
||||
if (hour >= 8 && hour < 17) {
|
||||
return { warmth: 0, brightness: 1.0 };
|
||||
}
|
||||
if (hour >= 17 && hour < 20) {
|
||||
return { warmth: 4, brightness: 0.98 };
|
||||
}
|
||||
return { warmth: 1, brightness: 0.95 };
|
||||
}
|
||||
|
||||
function applyCircadian(root: HTMLElement) {
|
||||
const hour = new Date().getHours();
|
||||
const shift = getCircadianShift(hour);
|
||||
root.style.setProperty('--sumi-circadian-warmth', `${shift.warmth}deg`);
|
||||
root.style.setProperty('--sumi-circadian-brightness', `${shift.brightness}`);
|
||||
}
|
||||
|
||||
// ═══ SUMI v4 — Patina calculation ═══
|
||||
export interface PatinaData {
|
||||
accountAgeDays: number;
|
||||
totalPlayTimeHours: number;
|
||||
totalUploads: number;
|
||||
totalMessages: number;
|
||||
totalExchanges: number;
|
||||
}
|
||||
|
||||
function calculatePatinaLevel(data: PatinaData): number {
|
||||
const ageScore = Math.min(data.accountAgeDays / 365, 1) * 25;
|
||||
const playScore = Math.min(data.totalPlayTimeHours / 500, 1) * 25;
|
||||
const createScore = Math.min(data.totalUploads / 20, 1) * 25;
|
||||
const socialScore = Math.min(
|
||||
(data.totalMessages + data.totalExchanges * 5) / 200, 1
|
||||
) * 25;
|
||||
const score = ageScore + playScore + createScore + socialScore;
|
||||
|
||||
if (score >= 85) return 4;
|
||||
if (score >= 60) return 3;
|
||||
if (score >= 35) return 2;
|
||||
if (score >= 15) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
function applyPatina(root: HTMLElement, level: number) {
|
||||
root.setAttribute('data-patina', String(level));
|
||||
const warmth = level * 2.5;
|
||||
root.style.setProperty('--sumi-patina-warmth', `${warmth}deg`);
|
||||
}
|
||||
|
||||
type ThemeProviderProps = {
|
||||
children: React.ReactNode;
|
||||
defaultTheme?: Theme;
|
||||
|
|
@ -29,6 +93,12 @@ type ThemeProviderState = {
|
|||
setAccentHue: (hue: number) => void;
|
||||
fontSize: number;
|
||||
setFontSize: (size: number) => void;
|
||||
circadianEnabled: boolean;
|
||||
setCircadianEnabled: (enabled: boolean) => void;
|
||||
ecoMode: boolean;
|
||||
setEcoMode: (eco: boolean) => void;
|
||||
patinaLevel: number;
|
||||
applyPatinaData: (data: PatinaData) => void;
|
||||
};
|
||||
|
||||
const initialState: ThemeProviderState = {
|
||||
|
|
@ -38,10 +108,16 @@ const initialState: ThemeProviderState = {
|
|||
setContrast: () => null,
|
||||
density: 'comfortable',
|
||||
setDensity: () => null,
|
||||
accentHue: 220,
|
||||
accentHue: SHU_HUE,
|
||||
setAccentHue: () => null,
|
||||
fontSize: 16,
|
||||
setFontSize: () => null,
|
||||
circadianEnabled: true,
|
||||
setCircadianEnabled: () => null,
|
||||
ecoMode: false,
|
||||
setEcoMode: () => null,
|
||||
patinaLevel: 0,
|
||||
applyPatinaData: () => null,
|
||||
};
|
||||
|
||||
const ThemeContext = createContext<ThemeProviderState>(initialState);
|
||||
|
|
@ -74,15 +150,24 @@ export function ThemeProvider({
|
|||
)
|
||||
);
|
||||
const [accentHue, setAccentHueState] = useState<number>(
|
||||
() => loadStored(STORAGE_KEYS.accentHue, 220, (s) => Math.min(360, Math.max(0, parseInt(s, 10) || 220)))
|
||||
() => loadStored(STORAGE_KEYS.accentHue, SHU_HUE, (s) => Math.min(360, Math.max(0, parseInt(s, 10) || SHU_HUE)))
|
||||
);
|
||||
const [fontSize, setFontSizeState] = useState<number>(
|
||||
() => loadStored(STORAGE_KEYS.fontSize, 16, (s) => Math.min(20, Math.max(14, parseInt(s, 10) || 16)))
|
||||
);
|
||||
const [circadianEnabled, setCircadianEnabledState] = useState<boolean>(
|
||||
() => loadStored(STORAGE_KEYS.circadian, true, (s) => s !== 'false')
|
||||
);
|
||||
const [ecoMode, setEcoModeState] = useState<boolean>(
|
||||
() => loadStored(STORAGE_KEYS.eco, false, (s) => s === 'true')
|
||||
);
|
||||
const [patinaLevel, setPatinaLevel] = useState<number>(0);
|
||||
|
||||
const circadianInterval = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
|
||||
// ═══ Theme ═══
|
||||
useEffect(() => {
|
||||
const root = window.document.documentElement;
|
||||
|
||||
if (theme === 'system') {
|
||||
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
root.setAttribute('data-theme', systemTheme);
|
||||
|
|
@ -102,33 +187,105 @@ export function ThemeProvider({
|
|||
return () => mediaQuery.removeEventListener('change', handleChange);
|
||||
}, [theme]);
|
||||
|
||||
// ═══ Contrast ═══
|
||||
useEffect(() => {
|
||||
const root = window.document.documentElement;
|
||||
root.setAttribute('data-contrast', contrast);
|
||||
window.document.documentElement.setAttribute('data-contrast', contrast);
|
||||
}, [contrast]);
|
||||
|
||||
// ═══ Density ═══
|
||||
useEffect(() => {
|
||||
const root = window.document.documentElement;
|
||||
root.setAttribute('data-density', density);
|
||||
window.document.documentElement.setAttribute('data-density', density);
|
||||
}, [density]);
|
||||
|
||||
// ═══ Accent hue — shifts around shu vermillion base ═══
|
||||
useEffect(() => {
|
||||
const root = window.document.documentElement;
|
||||
root.style.setProperty('--sumi-accent', `hsl(${accentHue}, 55%, 55%)`);
|
||||
root.style.setProperty('--sumi-accent-hover', `hsl(${accentHue}, 55%, 62%)`);
|
||||
root.style.setProperty('--sumi-accent-active', `hsl(${accentHue}, 55%, 48%)`);
|
||||
root.style.setProperty('--sumi-accent-muted', `hsl(${accentHue}, 55%, 55%, 0.2)`);
|
||||
root.style.setProperty('--sumi-accent-subtle', `hsl(${accentHue}, 55%, 55%, 0.12)`);
|
||||
root.style.setProperty('--sumi-accent-emphasis', `hsl(${accentHue}, 55%, 45%)`);
|
||||
root.style.setProperty('--primary', `hsl(${accentHue}, 55%, 55%)`);
|
||||
root.style.setProperty('--sumi-border-focus', `hsl(${accentHue}, 55%, 55%, 0.5)`);
|
||||
root.style.setProperty('--sumi-border-accent', `hsl(${accentHue}, 55%, 55%, 0.3)`);
|
||||
if (accentHue === SHU_HUE) {
|
||||
// Default shu vermillion — use the CSS-defined hex values (more precise)
|
||||
root.style.removeProperty('--sumi-accent');
|
||||
root.style.removeProperty('--sumi-accent-hover');
|
||||
root.style.removeProperty('--sumi-accent-active');
|
||||
root.style.removeProperty('--sumi-accent-muted');
|
||||
root.style.removeProperty('--sumi-accent-subtle');
|
||||
root.style.removeProperty('--sumi-accent-emphasis');
|
||||
root.style.removeProperty('--primary');
|
||||
root.style.removeProperty('--sumi-border-focus');
|
||||
root.style.removeProperty('--sumi-border-accent');
|
||||
} else {
|
||||
// Custom hue — generate HSL variants
|
||||
root.style.setProperty('--sumi-accent', `hsl(${accentHue}, ${SHU_SAT}%, ${SHU_LIT}%)`);
|
||||
root.style.setProperty('--sumi-accent-hover', `hsl(${accentHue}, ${SHU_SAT}%, ${SHU_LIT + 7}%)`);
|
||||
root.style.setProperty('--sumi-accent-active', `hsl(${accentHue}, ${SHU_SAT}%, ${SHU_LIT - 7}%)`);
|
||||
root.style.setProperty('--sumi-accent-muted', `hsl(${accentHue}, ${SHU_SAT}%, ${SHU_LIT}%, 0.2)`);
|
||||
root.style.setProperty('--sumi-accent-subtle', `hsl(${accentHue}, ${SHU_SAT}%, ${SHU_LIT}%, 0.1)`);
|
||||
root.style.setProperty('--sumi-accent-emphasis', `hsl(${accentHue}, ${SHU_SAT}%, ${SHU_LIT - 12}%)`);
|
||||
root.style.setProperty('--primary', `hsl(${accentHue}, ${SHU_SAT}%, ${SHU_LIT}%)`);
|
||||
root.style.setProperty('--sumi-border-focus', `hsl(${accentHue}, ${SHU_SAT}%, ${SHU_LIT}%, 0.5)`);
|
||||
root.style.setProperty('--sumi-border-accent', `hsl(${accentHue}, ${SHU_SAT}%, ${SHU_LIT}%, 0.3)`);
|
||||
}
|
||||
}, [accentHue]);
|
||||
|
||||
// ═══ Font size ═══
|
||||
useEffect(() => {
|
||||
window.document.documentElement.style.setProperty('--sumi-font-size-base', `${fontSize}px`);
|
||||
}, [fontSize]);
|
||||
|
||||
// ═══ SUMI v4 — Circadian rhythm ═══
|
||||
useEffect(() => {
|
||||
const root = window.document.documentElement;
|
||||
root.style.setProperty('--sumi-font-size-base', `${fontSize}px`);
|
||||
}, [fontSize]);
|
||||
|
||||
if (!circadianEnabled || ecoMode) {
|
||||
root.style.setProperty('--sumi-circadian-warmth', '0deg');
|
||||
root.style.setProperty('--sumi-circadian-brightness', '1');
|
||||
if (circadianInterval.current) {
|
||||
clearInterval(circadianInterval.current);
|
||||
circadianInterval.current = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply immediately
|
||||
applyCircadian(root);
|
||||
|
||||
// Recalculate every 30 minutes
|
||||
circadianInterval.current = setInterval(() => applyCircadian(root), 30 * 60 * 1000);
|
||||
|
||||
return () => {
|
||||
if (circadianInterval.current) {
|
||||
clearInterval(circadianInterval.current);
|
||||
circadianInterval.current = null;
|
||||
}
|
||||
};
|
||||
}, [circadianEnabled, ecoMode]);
|
||||
|
||||
// ═══ SUMI v4 — Eco mode ═══
|
||||
useEffect(() => {
|
||||
window.document.documentElement.setAttribute('data-eco', String(ecoMode));
|
||||
}, [ecoMode]);
|
||||
|
||||
// ═══ SUMI v4 — Auto eco on battery or Save-Data ═══
|
||||
useEffect(() => {
|
||||
// Respect Save-Data header preference
|
||||
const conn = (navigator as any).connection;
|
||||
if (conn?.saveData) {
|
||||
setEcoModeState(true);
|
||||
localStorage.setItem(STORAGE_KEYS.eco, 'true');
|
||||
}
|
||||
|
||||
// Auto eco on low battery
|
||||
if ('getBattery' in navigator) {
|
||||
(navigator as any).getBattery().then((battery: any) => {
|
||||
const checkBattery = () => {
|
||||
if (battery.level < 0.2 && !battery.charging) {
|
||||
setEcoModeState(true);
|
||||
}
|
||||
};
|
||||
battery.addEventListener('levelchange', checkBattery);
|
||||
battery.addEventListener('chargingchange', checkBattery);
|
||||
checkBattery();
|
||||
}).catch(() => { /* Battery API not available */ });
|
||||
}
|
||||
}, []);
|
||||
|
||||
const value: ThemeProviderState = {
|
||||
theme,
|
||||
|
|
@ -158,6 +315,22 @@ export function ThemeProvider({
|
|||
localStorage.setItem(STORAGE_KEYS.fontSize, String(clamped));
|
||||
setFontSizeState(clamped);
|
||||
},
|
||||
circadianEnabled,
|
||||
setCircadianEnabled: (enabled) => {
|
||||
localStorage.setItem(STORAGE_KEYS.circadian, String(enabled));
|
||||
setCircadianEnabledState(enabled);
|
||||
},
|
||||
ecoMode,
|
||||
setEcoMode: (eco) => {
|
||||
localStorage.setItem(STORAGE_KEYS.eco, String(eco));
|
||||
setEcoModeState(eco);
|
||||
},
|
||||
patinaLevel,
|
||||
applyPatinaData: (data: PatinaData) => {
|
||||
const level = calculatePatinaLevel(data);
|
||||
setPatinaLevel(level);
|
||||
applyPatina(window.document.documentElement, level);
|
||||
},
|
||||
};
|
||||
|
||||
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
|
||||
|
|
|
|||
|
|
@ -7,32 +7,32 @@ interface ThemeSwitcherProps {
|
|||
|
||||
const themes = [
|
||||
{
|
||||
id: 'cyber',
|
||||
name: 'Cyber',
|
||||
colors: ['#7c9dd6', '#d4634a'],
|
||||
description: 'Indigo & Vermillion',
|
||||
gradient: 'linear-gradient(135deg, #7c9dd6 0%, #d4634a 100%)',
|
||||
id: 'bokuseki',
|
||||
name: '墨跡 Bokuseki',
|
||||
colors: ['#b83a1e', '#8b2500'],
|
||||
description: 'Ink Traces — shu vermillion',
|
||||
gradient: 'linear-gradient(135deg, #b83a1e 0%, #8b2500 100%)',
|
||||
},
|
||||
{
|
||||
id: 'ocean',
|
||||
name: 'Ocean',
|
||||
colors: ['#7a9e6c', '#8eb280'],
|
||||
description: 'Sage & Moss',
|
||||
gradient: 'linear-gradient(135deg, #7a9e6c 0%, #8eb280 100%)',
|
||||
id: 'kin',
|
||||
name: '金 Kin',
|
||||
colors: ['#b8860b', '#8b6a08'],
|
||||
description: 'Gold Leaf — kinpaku',
|
||||
gradient: 'linear-gradient(135deg, #b8860b 0%, #8b6a08 100%)',
|
||||
},
|
||||
{
|
||||
id: 'forest',
|
||||
name: 'Forest',
|
||||
colors: ['#c9a84c', '#d6b860'],
|
||||
description: 'Gold & Amber',
|
||||
gradient: 'linear-gradient(135deg, #c9a84c 0%, #d6b860 100%)',
|
||||
id: 'ai',
|
||||
name: '藍 Ai',
|
||||
colors: ['#2a4e68', '#1a3548'],
|
||||
description: 'Indigo — deep blue',
|
||||
gradient: 'linear-gradient(135deg, #2a4e68 0%, #1a3548 100%)',
|
||||
},
|
||||
{
|
||||
id: 'sunset',
|
||||
name: 'Sunset',
|
||||
colors: ['#e0a0b8', '#c840a0'],
|
||||
description: 'Sakura & Magenta',
|
||||
gradient: 'linear-gradient(135deg, #e0a0b8 0%, #c840a0 100%)',
|
||||
id: 'matcha',
|
||||
name: '抹茶 Matcha',
|
||||
colors: ['#4f6840', '#3a5030'],
|
||||
description: 'Green tea — moss',
|
||||
gradient: 'linear-gradient(135deg, #4f6840 0%, #3a5030 100%)',
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -43,13 +43,13 @@ export function ThemeSwitcher({
|
|||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-10 h-10 rounded-xl bg-primary flex items-center justify-center">
|
||||
<div className="w-10 h-10 rounded bg-primary flex items-center justify-center">
|
||||
<Palette className="w-5 h-5 text-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-bold text-foreground">Color Theme</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Choose your visual style
|
||||
<h3 className="text-xl text-foreground" style={{ fontWeight: 300 }}>Color Theme</h3>
|
||||
<p className="text-sm text-muted-foreground" style={{ fontWeight: 300 }}>
|
||||
Choose your ink palette
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -59,7 +59,7 @@ export function ThemeSwitcher({
|
|||
<button
|
||||
key={theme.id}
|
||||
onClick={() => onThemeChange(theme.id)}
|
||||
className={`group relative p-8 rounded-2xl border-2 transition-all duration-[var(--sumi-duration-normal)] text-left overflow-hidden ${
|
||||
className={`group relative p-8 rounded border-2 transition-all duration-[var(--sumi-duration-normal)] text-left overflow-hidden ${
|
||||
currentTheme === theme.id
|
||||
? 'border-primary bg-primary/10 shadow-lg shadow-primary/20'
|
||||
: 'border-white/10 bg-muted/50 hover:border-white/30 hover:bg-muted'
|
||||
|
|
@ -78,7 +78,7 @@ export function ThemeSwitcher({
|
|||
{theme.colors.map((color, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="w-10 h-10 rounded-lg shadow-lg transition-opacity group-hover:opacity-80"
|
||||
className="w-10 h-10 rounded shadow-lg transition-opacity group-hover:opacity-80"
|
||||
style={{ backgroundColor: color }}
|
||||
/>
|
||||
))}
|
||||
|
|
@ -86,10 +86,10 @@ export function ThemeSwitcher({
|
|||
</div>
|
||||
|
||||
<div className="mb-2">
|
||||
<div className="font-bold text-lg text-foreground mb-1">
|
||||
<div className="text-lg text-foreground mb-1" style={{ fontWeight: 400 }}>
|
||||
{theme.name}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
<div className="text-sm text-muted-foreground" style={{ fontWeight: 300 }}>
|
||||
{theme.description}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -97,7 +97,7 @@ export function ThemeSwitcher({
|
|||
{currentTheme === theme.id && (
|
||||
<div className="flex items-center gap-2 text-muted-foreground font-mono text-sm animate-slide-in-left">
|
||||
<div className="w-2 h-2 rounded-full bg-primary animate-glow-pulse" />
|
||||
Active Theme
|
||||
Active
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,64 +4,86 @@
|
|||
@custom-variant dark (&:is([data-theme="dark"] *));
|
||||
|
||||
/* ╔══════════════════════════════════════════════════════════════════════════╗
|
||||
║ SUMI DESIGN SYSTEM v2.0 — "L'encre et la lumière" ║
|
||||
║ SUMI DESIGN SYSTEM v4.0 — "Lavis d'encre" (墨の濃淡) ║
|
||||
║ VEZA × TALAS — Single Source of Truth ║
|
||||
║ Ref: apps/web/docs/DESIGN_SYSTEM_REFERENCE.md ║
|
||||
║ Ink wash aesthetic — contrast, wabi-sabi, ma (間) ║
|
||||
╚══════════════════════════════════════════════════════════════════════════╝ */
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
DARK THEME (default) — Ink on void
|
||||
DARK THEME (default) — Sumi ink on void (墨の闇)
|
||||
═══════════════════════════════════════════════════════════════════════════ */
|
||||
:root, [data-theme="dark"] {
|
||||
/* ═══ BACKGROUNDS ═══ */
|
||||
--sumi-bg-void: #0c0c0f;
|
||||
--sumi-bg-base: #121215;
|
||||
--sumi-bg-raised: #1a1a1f;
|
||||
--sumi-bg-overlay: #222228;
|
||||
--sumi-bg-hover: #2a2a31;
|
||||
--sumi-bg-active: #32323a;
|
||||
--sumi-bg-wash: #18181d;
|
||||
/* ═══ BACKGROUNDS — Ink dilutions (墨の濃淡) ═══ */
|
||||
--sumi-bg-void: #0d0d0b;
|
||||
--sumi-bg-base: #13110f;
|
||||
--sumi-bg-raised: #1a1714;
|
||||
--sumi-bg-overlay: #242018;
|
||||
--sumi-bg-hover: #2e2a22;
|
||||
--sumi-bg-active: #383228;
|
||||
--sumi-bg-wash: #17140f;
|
||||
|
||||
/* ═══ SURFACES ═══ */
|
||||
--sumi-surface-inset: #101013;
|
||||
--sumi-surface-subtle: #1e1e24;
|
||||
--sumi-surface-card: #1a1a1f;
|
||||
--sumi-surface-elevated: #242430;
|
||||
/* ═══ SURFACES — Ink planes (墨面) ═══ */
|
||||
--sumi-surface-inset: #0f0d0b;
|
||||
--sumi-surface-subtle: #1e1a15;
|
||||
--sumi-surface-card: #1a1714;
|
||||
--sumi-surface-elevated: #242018;
|
||||
|
||||
/* ═══ BORDERS ═══ */
|
||||
--sumi-border-faint: rgba(255,255,255, 0.06);
|
||||
--sumi-border-default: rgba(255,255,255, 0.10);
|
||||
--sumi-border-strong: rgba(255,255,255, 0.16);
|
||||
--sumi-border-focus: rgba(139,170,220, 0.50);
|
||||
--sumi-border-accent: rgba(139,170,220, 0.30);
|
||||
/* ═══ BORDERS — Ink lines (筆線) ═══ */
|
||||
--sumi-border-faint: rgba(232,224,208, 0.03);
|
||||
--sumi-border-default: rgba(232,224,208, 0.06);
|
||||
--sumi-border-strong: rgba(232,224,208, 0.10);
|
||||
--sumi-border-focus: rgba(184,58,30, 0.50);
|
||||
--sumi-border-accent: rgba(184,58,30, 0.25);
|
||||
|
||||
/* ═══ TEXT ═══ */
|
||||
--sumi-text-primary: #f0ede8;
|
||||
--sumi-text-secondary: #a8a4a0;
|
||||
--sumi-text-tertiary: #706c68;
|
||||
--sumi-text-disabled: #4a4844;
|
||||
--sumi-text-inverse: #121215;
|
||||
--sumi-text-link: #8baade;
|
||||
/* ═══ TEXT — Ink on washi (墨と和紙) ═══ */
|
||||
--sumi-text-primary: #e8e0d0;
|
||||
--sumi-text-secondary: #9e9688;
|
||||
--sumi-text-tertiary: #6b6560;
|
||||
--sumi-text-disabled: #3d3930;
|
||||
--sumi-text-inverse: #13110f;
|
||||
--sumi-text-link: #b83a1e;
|
||||
|
||||
/* ═══ PIGMENTS — Les 4 Pigments d'accent ═══ */
|
||||
--sumi-accent: #7c9dd6;
|
||||
--sumi-accent-hover: #93afe0;
|
||||
--sumi-accent-active: #6b8dc6;
|
||||
--sumi-accent-muted: rgba(124,157,214, 0.20);
|
||||
--sumi-accent-subtle: rgba(124,157,214, 0.12);
|
||||
--sumi-accent-emphasis: #5a7fba;
|
||||
/* ═══ PIGMENTS — Shu vermillion (朱) + Kin gold (金) ═══ */
|
||||
--sumi-accent: #b83a1e;
|
||||
--sumi-accent-hover: #c84a2e;
|
||||
--sumi-accent-active: #8b2500;
|
||||
--sumi-accent-muted: rgba(184,58,30, 0.18);
|
||||
--sumi-accent-subtle: rgba(184,58,30, 0.06);
|
||||
--sumi-accent-emphasis: #8b2500;
|
||||
|
||||
--sumi-vermillion: #d4634a;
|
||||
--sumi-vermillion-hover: #de7a64;
|
||||
--sumi-vermillion-subtle: rgba(212,99,74, 0.12);
|
||||
--sumi-vermillion: #a04050;
|
||||
--sumi-vermillion-hover: #b05060;
|
||||
--sumi-vermillion-subtle: rgba(160,64,80, 0.10);
|
||||
|
||||
--sumi-sage: #7a9e6c;
|
||||
--sumi-sage-hover: #8eb280;
|
||||
--sumi-sage-subtle: rgba(122,158,108, 0.12);
|
||||
--sumi-sage: #4f6840;
|
||||
--sumi-sage-hover: #5f7850;
|
||||
--sumi-sage-subtle: rgba(79,104,64, 0.10);
|
||||
|
||||
--sumi-gold: #c9a84c;
|
||||
--sumi-gold-hover: #d6b860;
|
||||
--sumi-gold-subtle: rgba(201,168,76, 0.12);
|
||||
--sumi-gold: #b8860b;
|
||||
--sumi-gold-hover: #c8960b;
|
||||
--sumi-gold-subtle: rgba(184,134,11, 0.10);
|
||||
|
||||
/* ═══ 金 — KIN (GOLD LEAF) ═══ */
|
||||
--sumi-kin: #b8860b;
|
||||
--sumi-kin-hover: #c8960b;
|
||||
--sumi-kin-subtle: rgba(184,134,11, 0.08);
|
||||
|
||||
/* ═══ PATINA (SUMI v4) ═══ */
|
||||
--sumi-patina-green: #5A8A72;
|
||||
--sumi-patina-warmth: 0deg;
|
||||
--sumi-grain-opacity: 0.05;
|
||||
|
||||
/* ═══ 墨の六色 — Six Tones of Ink ═══ */
|
||||
--sumi-ink-kuro: #0d0d0b; /* 黒 pure black */
|
||||
--sumi-ink-sumi: #1a1714; /* 墨 ink */
|
||||
--sumi-ink-usuzumi: #3d3930; /* 薄墨 light ink */
|
||||
--sumi-ink-hai: #6b6560; /* 灰 ash */
|
||||
--sumi-ink-gin: #9e9688; /* 銀 silver */
|
||||
--sumi-ink-kasumi: #c4bba8; /* 霞 mist */
|
||||
|
||||
/* ═══ CIRCADIAN (SUMI v4) ═══ */
|
||||
--sumi-circadian-warmth: 0deg;
|
||||
--sumi-circadian-brightness: 1;
|
||||
|
||||
/* ═══ SEMANTIC ═══ */
|
||||
--sumi-success: var(--sumi-sage);
|
||||
|
|
@ -71,12 +93,12 @@
|
|||
--sumi-error: var(--sumi-vermillion);
|
||||
--sumi-error-subtle: var(--sumi-vermillion-subtle);
|
||||
--sumi-info: var(--sumi-accent);
|
||||
--sumi-live: #e05a5a;
|
||||
--sumi-live: #a04050;
|
||||
--sumi-online: var(--sumi-sage);
|
||||
|
||||
/* ═══ TYPOGRAPHY ═══ */
|
||||
--sumi-font-body: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
--sumi-font-heading: 'Space Grotesk', 'Inter', sans-serif;
|
||||
--sumi-font-heading: 'Noto Serif JP', Georgia, serif;
|
||||
--sumi-font-mono: 'JetBrains Mono', 'SF Mono', 'Consolas', monospace;
|
||||
--sumi-font-serif: 'Noto Serif JP', Georgia, serif;
|
||||
|
||||
|
|
@ -103,8 +125,9 @@
|
|||
--sumi-tracking-normal: 0;
|
||||
--sumi-tracking-wide: 0.025em;
|
||||
--sumi-tracking-wider: 0.05em;
|
||||
--sumi-tracking-widest: 0.1em;
|
||||
--sumi-tracking-widest: 0.2em;
|
||||
|
||||
--sumi-weight-extralight: 200;
|
||||
--sumi-weight-light: 300;
|
||||
--sumi-weight-regular: 400;
|
||||
--sumi-weight-medium: 500;
|
||||
|
|
@ -128,33 +151,34 @@
|
|||
--sumi-space-20: 80px;
|
||||
|
||||
/* ═══ RADIUS ═══ */
|
||||
--sumi-radius-xs: 2px;
|
||||
--sumi-radius-sm: 4px;
|
||||
--sumi-radius-md: 6px;
|
||||
--sumi-radius-lg: 12px;
|
||||
--sumi-radius-xl: 16px;
|
||||
--sumi-radius-2xl: 20px;
|
||||
--sumi-radius-xs: 1px;
|
||||
--sumi-radius-sm: 2px;
|
||||
--sumi-radius-md: 2px;
|
||||
--sumi-radius-lg: 4px;
|
||||
--sumi-radius-xl: 6px;
|
||||
--sumi-radius-2xl: 8px;
|
||||
--sumi-radius-full: 9999px;
|
||||
|
||||
/* ═══ SHADOWS ═══ */
|
||||
--sumi-shadow-xs: 0 1px 2px rgba(0,0,0,0.30);
|
||||
--sumi-shadow-sm: 0 2px 4px rgba(0,0,0,0.25), 0 1px 2px rgba(0,0,0,0.20);
|
||||
--sumi-shadow-md: 0 4px 12px rgba(0,0,0,0.30), 0 2px 4px rgba(0,0,0,0.15);
|
||||
--sumi-shadow-lg: 0 8px 24px rgba(0,0,0,0.35), 0 4px 8px rgba(0,0,0,0.20);
|
||||
--sumi-shadow-xl: 0 16px 48px rgba(0,0,0,0.40), 0 8px 16px rgba(0,0,0,0.20);
|
||||
--sumi-shadow-2xl: 0 24px 64px rgba(0,0,0,0.50);
|
||||
--sumi-shadow-glow: 0 0 0 3px rgba(124,157,214,0.25);
|
||||
--sumi-shadow-glow-lg: 0 0 20px rgba(124,157,214,0.15);
|
||||
/* ═══ SHADOWS — Ink diffusion (滲み) ═══ */
|
||||
--sumi-shadow-xs: 0 1px 3px rgba(13,13,11,0.20);
|
||||
--sumi-shadow-sm: 0 2px 8px rgba(13,13,11,0.15), 0 1px 3px rgba(13,13,11,0.12);
|
||||
--sumi-shadow-md: 0 4px 20px rgba(13,13,11,0.15), 0 2px 6px rgba(13,13,11,0.10);
|
||||
--sumi-shadow-lg: 0 8px 35px rgba(13,13,11,0.18), 0 4px 12px rgba(13,13,11,0.10);
|
||||
--sumi-shadow-xl: 0 16px 55px rgba(13,13,11,0.22), 0 8px 20px rgba(13,13,11,0.12);
|
||||
--sumi-shadow-2xl: 0 24px 75px rgba(13,13,11,0.30);
|
||||
--sumi-shadow-glow: 0 0 0 3px rgba(184,58,30, 0.20);
|
||||
--sumi-shadow-glow-lg: 0 0 20px rgba(184,58,30, 0.10);
|
||||
--sumi-shadow-kin: 0 0 16px rgba(184,134,11, 0.15);
|
||||
|
||||
/* ═══ GLASS ═══ */
|
||||
--sumi-glass-bg: rgba(18,18,21, 0.80);
|
||||
--sumi-glass-border: rgba(255,255,255, 0.08);
|
||||
/* ═══ GLASS — Shoji screen (障子) ═══ */
|
||||
--sumi-glass-bg: rgba(19,17,15, 0.80);
|
||||
--sumi-glass-border: rgba(232,224,208, 0.04);
|
||||
--sumi-glass-blur: 12px;
|
||||
|
||||
/* ═══ SCROLLBAR ═══ */
|
||||
--sumi-scrollbar-track: transparent;
|
||||
--sumi-scrollbar-thumb: rgba(255,255,255, 0.10);
|
||||
--sumi-scrollbar-hover: rgba(255,255,255, 0.18);
|
||||
--sumi-scrollbar-thumb: rgba(232,224,208, 0.06);
|
||||
--sumi-scrollbar-hover: rgba(232,224,208, 0.12);
|
||||
|
||||
/* ═══ MOTION ═══ */
|
||||
--sumi-duration-instant: 75ms;
|
||||
|
|
@ -204,10 +228,12 @@
|
|||
--sumi-player-height: 80px;
|
||||
|
||||
/* ═══ CONTEXTUAL ACCENTS (feature-specific) ═══ */
|
||||
--graffiti-magenta: #c840a0;
|
||||
--gaming-gold: #d4b040;
|
||||
--terminal-green: #3eaa5e;
|
||||
--sakura: #e0a0b8;
|
||||
--graffiti-magenta: #a04050;
|
||||
--gaming-gold: #b8860b;
|
||||
--terminal-green: #4f6840;
|
||||
--sakura: #d4a0b0;
|
||||
--sumi-shu: #b83a1e; /* 朱 — hanko seal vermillion */
|
||||
--sumi-ai: #2a4e68; /* 藍 — indigo */
|
||||
|
||||
/* ═══ SHADCN/RADIX SEMANTIC MAPPING ═══ */
|
||||
--background: var(--sumi-bg-base);
|
||||
|
|
@ -283,7 +309,8 @@
|
|||
/* App shell — header, main offsets and margins (sidebar-driven) */
|
||||
--header-height: 4rem;
|
||||
--main-offset-top: 5rem;
|
||||
--main-offset-bottom: 9rem;
|
||||
--main-offset-bottom: 7.5rem;
|
||||
--mobile-bottom-nav-height: 4rem;
|
||||
--main-margin-left-expanded: 18rem;
|
||||
--main-margin-left-collapsed: 7rem;
|
||||
--header-left-expanded: 18rem;
|
||||
|
|
@ -301,104 +328,115 @@
|
|||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
LIGHT THEME — Washi Paper (和紙)
|
||||
LIGHT THEME — Washi Paper (和紙) — Ink on warm paper
|
||||
═══════════════════════════════════════════════════════════════════════════ */
|
||||
[data-theme="light"] {
|
||||
--sumi-bg-void: #f0ece4;
|
||||
--sumi-bg-base: #f6f3ed;
|
||||
--sumi-bg-raised: #ffffff;
|
||||
--sumi-bg-overlay: #ffffff;
|
||||
--sumi-bg-hover: #ede9e1;
|
||||
--sumi-bg-active: #e4e0d8;
|
||||
--sumi-bg-wash: #f8f6f1;
|
||||
--sumi-bg-void: #e8e0cf;
|
||||
--sumi-bg-base: #f0ebe0;
|
||||
--sumi-bg-raised: #f0ebe0;
|
||||
--sumi-bg-overlay: #f0ebe0;
|
||||
--sumi-bg-hover: #e4dccb;
|
||||
--sumi-bg-active: #ddd4c0;
|
||||
--sumi-bg-wash: #ece5d6;
|
||||
|
||||
--sumi-surface-inset: #ebe7df;
|
||||
--sumi-surface-subtle: #f2eee6;
|
||||
--sumi-surface-card: #ffffff;
|
||||
--sumi-surface-elevated: #ffffff;
|
||||
--sumi-surface-inset: #e0d8c8;
|
||||
--sumi-surface-subtle: #e8e0cf;
|
||||
--sumi-surface-card: #f0ebe0;
|
||||
--sumi-surface-elevated: #f5f0e5;
|
||||
|
||||
--sumi-border-faint: rgba(0,0,0, 0.05);
|
||||
--sumi-border-default: rgba(0,0,0, 0.10);
|
||||
--sumi-border-strong: rgba(0,0,0, 0.18);
|
||||
--sumi-border-focus: rgba(80,110,170, 0.45);
|
||||
--sumi-border-accent: rgba(80,110,170, 0.25);
|
||||
--sumi-border-faint: rgba(26,23,20, 0.04);
|
||||
--sumi-border-default: rgba(26,23,20, 0.06);
|
||||
--sumi-border-strong: rgba(26,23,20, 0.12);
|
||||
--sumi-border-focus: rgba(139,37,0, 0.45);
|
||||
--sumi-border-accent: rgba(139,37,0, 0.20);
|
||||
|
||||
--sumi-text-primary: #1a1816;
|
||||
--sumi-text-secondary: #5c5854;
|
||||
--sumi-text-tertiary: #8a8580;
|
||||
--sumi-text-disabled: #b5b0aa;
|
||||
--sumi-text-inverse: #f0ede8;
|
||||
--sumi-text-link: #4a6fa5;
|
||||
--sumi-text-primary: #1a1714;
|
||||
--sumi-text-secondary: #3d3930;
|
||||
--sumi-text-tertiary: #6b6560;
|
||||
--sumi-text-disabled: #c4bba8;
|
||||
--sumi-text-inverse: #e8e0d0;
|
||||
--sumi-text-link: #8b2500;
|
||||
|
||||
--sumi-accent: #4a6fa5;
|
||||
--sumi-accent-hover: #3a5f95;
|
||||
--sumi-accent-active: #5a7fb5;
|
||||
--sumi-accent-subtle: rgba(74,111,165, 0.12);
|
||||
--sumi-accent-muted: rgba(74,111,165, 0.20);
|
||||
--sumi-accent-emphasis: #3d5f90;
|
||||
--sumi-accent: #8b2500;
|
||||
--sumi-accent-hover: #7a2000;
|
||||
--sumi-accent-active: #9b3510;
|
||||
--sumi-accent-subtle: rgba(139,37,0, 0.08);
|
||||
--sumi-accent-muted: rgba(139,37,0, 0.15);
|
||||
--sumi-accent-emphasis: #6a1c00;
|
||||
|
||||
--sumi-vermillion: #b84a35;
|
||||
--sumi-vermillion-hover: #a03e2e;
|
||||
--sumi-vermillion-subtle: rgba(184,74,53, 0.12);
|
||||
--sumi-vermillion: #803040;
|
||||
--sumi-vermillion-hover: #702838;
|
||||
--sumi-vermillion-subtle: rgba(128,48,64, 0.10);
|
||||
|
||||
--sumi-sage: #5a7e4e;
|
||||
--sumi-sage-hover: #4d6e42;
|
||||
--sumi-sage-subtle: rgba(90,126,78, 0.12);
|
||||
--sumi-sage: #3d5530;
|
||||
--sumi-sage-hover: #304828;
|
||||
--sumi-sage-subtle: rgba(61,85,48, 0.10);
|
||||
|
||||
--sumi-gold: #9a7d2e;
|
||||
--sumi-gold-hover: #8a6d20;
|
||||
--sumi-gold-subtle: rgba(154,125,46, 0.12);
|
||||
--sumi-gold: #9a7208;
|
||||
--sumi-gold-hover: #886008;
|
||||
--sumi-gold-subtle: rgba(154,114,8, 0.10);
|
||||
|
||||
--sumi-live: #c84040;
|
||||
--sumi-kin: #9a7208;
|
||||
--sumi-kin-hover: #886008;
|
||||
--sumi-kin-subtle: rgba(154,114,8, 0.06);
|
||||
|
||||
--sumi-shadow-xs: 0 1px 2px rgba(0,0,0,0.05);
|
||||
--sumi-shadow-sm: 0 2px 4px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.04);
|
||||
--sumi-shadow-md: 0 4px 12px rgba(0,0,0,0.08), 0 2px 4px rgba(0,0,0,0.04);
|
||||
--sumi-shadow-lg: 0 8px 24px rgba(0,0,0,0.10), 0 4px 8px rgba(0,0,0,0.05);
|
||||
--sumi-shadow-xl: 0 16px 48px rgba(0,0,0,0.12), 0 8px 16px rgba(0,0,0,0.06);
|
||||
--sumi-shadow-2xl: 0 24px 64px rgba(0,0,0,0.15);
|
||||
--sumi-shadow-glow: 0 0 0 3px rgba(74,111,165,0.25);
|
||||
--sumi-live: #803040;
|
||||
|
||||
--sumi-glass-bg: rgba(255,255,255, 0.85);
|
||||
--sumi-glass-border: rgba(0,0,0, 0.06);
|
||||
--sumi-shadow-xs: 0 1px 3px rgba(26,23,20,0.04);
|
||||
--sumi-shadow-sm: 0 2px 8px rgba(26,23,20,0.05), 0 1px 3px rgba(26,23,20,0.03);
|
||||
--sumi-shadow-md: 0 4px 20px rgba(26,23,20,0.06), 0 2px 6px rgba(26,23,20,0.04);
|
||||
--sumi-shadow-lg: 0 8px 35px rgba(26,23,20,0.08), 0 4px 12px rgba(26,23,20,0.04);
|
||||
--sumi-shadow-xl: 0 16px 55px rgba(26,23,20,0.10), 0 8px 20px rgba(26,23,20,0.05);
|
||||
--sumi-shadow-2xl: 0 24px 75px rgba(26,23,20,0.12);
|
||||
--sumi-shadow-glow: 0 0 0 3px rgba(139,37,0, 0.18);
|
||||
--sumi-shadow-kin: 0 0 16px rgba(154,114,8, 0.12);
|
||||
|
||||
--sumi-scrollbar-thumb: rgba(0,0,0, 0.12);
|
||||
--sumi-scrollbar-hover: rgba(0,0,0, 0.22);
|
||||
--sumi-glass-bg: rgba(240,235,224, 0.85);
|
||||
--sumi-glass-border: rgba(26,23,20, 0.04);
|
||||
|
||||
--primary-foreground: #ffffff;
|
||||
--sumi-scrollbar-thumb: rgba(26,23,20, 0.08);
|
||||
--sumi-scrollbar-hover: rgba(26,23,20, 0.16);
|
||||
|
||||
--sumi-grain-opacity: 0.06;
|
||||
|
||||
--primary-foreground: #f0ebe0;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
HIGH CONTRAST MODE — WCAG AA 4.5:1+
|
||||
HIGH CONTRAST MODE — 極墨 (Extreme Ink) — WCAG AA 4.5:1+
|
||||
═══════════════════════════════════════════════════════════════════════════ */
|
||||
[data-theme="dark"][data-contrast="high"] {
|
||||
--sumi-bg-void: #000000;
|
||||
--sumi-bg-base: #000000;
|
||||
--sumi-bg-raised: #0a0a0a;
|
||||
--sumi-bg-overlay: #111111;
|
||||
--sumi-bg-hover: #1a1a1a;
|
||||
--sumi-bg-active: #222222;
|
||||
--sumi-bg-base: #050504;
|
||||
--sumi-bg-raised: #0d0c0a;
|
||||
--sumi-bg-overlay: #141210;
|
||||
--sumi-bg-hover: #1e1c18;
|
||||
--sumi-bg-active: #262420;
|
||||
--sumi-text-primary: #ffffff;
|
||||
--sumi-text-secondary: #d0d0d0;
|
||||
--sumi-text-tertiary: #b0b0b0;
|
||||
--sumi-border-default: rgba(255,255,255,0.30);
|
||||
--sumi-border-faint: rgba(255,255,255,0.15);
|
||||
--sumi-border-strong: rgba(255,255,255,0.50);
|
||||
--sumi-text-secondary: #d5cfc2;
|
||||
--sumi-text-tertiary: #b0a898;
|
||||
--sumi-border-default: rgba(232,224,208,0.25);
|
||||
--sumi-border-faint: rgba(232,224,208,0.12);
|
||||
--sumi-border-strong: rgba(232,224,208,0.40);
|
||||
--sumi-accent: #d85030;
|
||||
--sumi-accent-hover: #e86040;
|
||||
}
|
||||
|
||||
[data-theme="light"][data-contrast="high"] {
|
||||
--sumi-bg-void: #ffffff;
|
||||
--sumi-bg-base: #ffffff;
|
||||
--sumi-bg-raised: #f5f5f5;
|
||||
--sumi-bg-overlay: #eeeeee;
|
||||
--sumi-bg-hover: #e0e0e0;
|
||||
--sumi-bg-active: #d0d0d0;
|
||||
--sumi-bg-base: #fdfcf8;
|
||||
--sumi-bg-raised: #f8f5ee;
|
||||
--sumi-bg-overlay: #f0ece2;
|
||||
--sumi-bg-hover: #e5e0d5;
|
||||
--sumi-bg-active: #d8d2c5;
|
||||
--sumi-text-primary: #000000;
|
||||
--sumi-text-secondary: #333333;
|
||||
--sumi-text-tertiary: #555555;
|
||||
--sumi-border-default: rgba(0,0,0,0.30);
|
||||
--sumi-border-faint: rgba(0,0,0,0.15);
|
||||
--sumi-border-strong: rgba(0,0,0,0.50);
|
||||
--sumi-text-secondary: #1a1714;
|
||||
--sumi-text-tertiary: #3d3930;
|
||||
--sumi-border-default: rgba(26,23,20,0.25);
|
||||
--sumi-border-faint: rgba(26,23,20,0.12);
|
||||
--sumi-border-strong: rgba(26,23,20,0.40);
|
||||
--sumi-accent: #7a1800;
|
||||
--sumi-accent-hover: #601200;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
|
|
@ -481,7 +519,10 @@
|
|||
--color-gaming-gold: var(--sumi-gold);
|
||||
--color-terminal-green: #3eaa5e;
|
||||
--color-graffiti-magenta: #c840a0;
|
||||
--color-sakura: #e0a0b8;
|
||||
--color-sakura: #d090a8;
|
||||
--color-sumi-shu: var(--sumi-shu);
|
||||
--color-sumi-kin: var(--sumi-kin);
|
||||
--color-sumi-ai: var(--sumi-ai);
|
||||
|
||||
/* Chart colors */
|
||||
--color-chart-1: var(--chart-1);
|
||||
|
|
@ -540,19 +581,70 @@
|
|||
font-feature-settings: "cv02", "cv03", "cv04", "cv11";
|
||||
}
|
||||
|
||||
/* Paper grain texture — barely perceptible washi feeling */
|
||||
body::before {
|
||||
/* ═══ SUMI v4 — Washi fiber grain (和紙繊維) ═══
|
||||
Anisotropic noise simulating washi paper fiber direction.
|
||||
baseFrequency 0.4x0.9 = horizontal fibers. 5 octaves = fine detail.
|
||||
GPU-composited via position:fixed. */
|
||||
body::after {
|
||||
content: '';
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background:
|
||||
url("data:image/svg+xml,%3Csvg viewBox='0 0 400 400' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E");
|
||||
opacity: 0.012;
|
||||
url("data:image/svg+xml,%3Csvg viewBox='0 0 512 512' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='w'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.4 0.9' numOctaves='5' stitchTiles='stitch' seed='2'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23w)'/%3E%3C/svg%3E");
|
||||
opacity: var(--sumi-grain-opacity, 0.05);
|
||||
pointer-events: none;
|
||||
z-index: 9998;
|
||||
mix-blend-mode: overlay;
|
||||
}
|
||||
|
||||
/* ═══ SUMI v4 — Subtle ink wash vignette (墨暈し) ═══
|
||||
Very faint radial light at top-center, like a distant lamp on canvas. */
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: radial-gradient(ellipse at 30% 20%, rgba(210,195,168, 0.04) 0%, transparent 60%),
|
||||
radial-gradient(ellipse at 70% 80%, rgba(180,165,135, 0.03) 0%, transparent 50%);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* ═══ SUMI v4 — Circadian + Patina filter ═══
|
||||
Applied on #root to avoid affecting the grain layer.
|
||||
hue-rotate shifts warmth. brightness adjusts for time of day.
|
||||
Transition is 5 min (300s) so changes are imperceptible. */
|
||||
#root {
|
||||
filter:
|
||||
hue-rotate(calc(var(--sumi-circadian-warmth) + var(--sumi-patina-warmth, 0deg)))
|
||||
brightness(var(--sumi-circadian-brightness));
|
||||
transition: filter 300s linear;
|
||||
}
|
||||
|
||||
/* Eco mode — disable all cosmetic layers */
|
||||
[data-eco="true"] body::after { display: none; }
|
||||
[data-eco="true"] body::before { display: none; }
|
||||
[data-eco="true"] #root { filter: none; transition: none; }
|
||||
|
||||
/* Respect reduced motion */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
body::after { display: none; }
|
||||
body::before { display: none; }
|
||||
#root { filter: none; transition: none; }
|
||||
}
|
||||
|
||||
/* ═══ SUMI v4 — Patina levels (墨の歳月) ═══ */
|
||||
[data-patina="1"] { --sumi-grain-opacity: 0.06; }
|
||||
[data-patina="2"] { --sumi-grain-opacity: 0.07; }
|
||||
[data-patina="3"] {
|
||||
--sumi-grain-opacity: 0.08;
|
||||
--sumi-border-default: rgba(184, 58, 30, 0.05);
|
||||
}
|
||||
[data-patina="4"] {
|
||||
--sumi-grain-opacity: 0.09;
|
||||
--sumi-border-default: rgba(184, 58, 30, 0.08);
|
||||
--sumi-shadow-sm: 0 2px 8px rgba(184, 58, 30, 0.08), 0 1px 3px rgba(13,13,11,0.12);
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: var(--primary);
|
||||
color: var(--primary-foreground);
|
||||
|
|
@ -587,16 +679,16 @@
|
|||
|
||||
/* Typography — headings use Space Grotesk */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
@apply font-heading font-semibold tracking-tight text-foreground;
|
||||
@apply font-heading tracking-tight text-foreground;
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
h1 { @apply text-4xl md:text-5xl; }
|
||||
h2 { @apply text-3xl md:text-4xl; }
|
||||
h3 { @apply text-2xl md:text-3xl; }
|
||||
h4 { @apply text-xl md:text-2xl; }
|
||||
h5 { @apply text-lg md:text-xl; }
|
||||
h6 { @apply text-base md:text-lg; }
|
||||
h1 { @apply text-4xl md:text-5xl; font-weight: 200; }
|
||||
h2 { @apply text-3xl md:text-4xl; font-weight: 300; }
|
||||
h3 { @apply text-2xl md:text-3xl; font-weight: 400; }
|
||||
h4 { @apply text-xl md:text-2xl; font-weight: 400; }
|
||||
h5 { @apply text-lg md:text-xl; font-weight: 500; }
|
||||
h6 { @apply text-base md:text-lg; font-weight: 500; }
|
||||
|
||||
p {
|
||||
@apply text-muted-foreground leading-relaxed;
|
||||
|
|
@ -654,6 +746,7 @@
|
|||
.z-sidebar { z-index: var(--sidebar-z-index); }
|
||||
.z-sidebar-overlay { z-index: var(--sidebar-overlay-z-index); }
|
||||
.z-player { z-index: var(--player-z-index); }
|
||||
.h-mobile-nav { height: var(--mobile-bottom-nav-height); }
|
||||
|
||||
/* App shell — header / main offsets */
|
||||
.h-header { height: var(--header-height); }
|
||||
|
|
@ -700,11 +793,11 @@
|
|||
box-shadow: var(--sidebar-active-indicator);
|
||||
}
|
||||
|
||||
/* Glass Effect */
|
||||
/* Glass Effect — Shoji screen (障子) */
|
||||
.glass, .sumi-glass {
|
||||
background: var(--sumi-glass-bg);
|
||||
backdrop-filter: blur(var(--sumi-glass-blur));
|
||||
-webkit-backdrop-filter: blur(var(--sumi-glass-blur));
|
||||
backdrop-filter: blur(var(--sumi-glass-blur)) saturate(0.85);
|
||||
-webkit-backdrop-filter: blur(var(--sumi-glass-blur)) saturate(0.85);
|
||||
border: 1px solid var(--sumi-glass-border);
|
||||
}
|
||||
|
||||
|
|
@ -712,22 +805,22 @@
|
|||
.font-heading { font-family: var(--sumi-font-heading); }
|
||||
.font-serif { font-family: var(--sumi-font-serif); }
|
||||
|
||||
.text-display { @apply text-4xl font-bold tracking-tight; }
|
||||
.text-heading-1 { @apply text-3xl font-semibold tracking-tight; }
|
||||
.text-heading-2 { @apply text-2xl font-semibold; }
|
||||
.text-heading-3 { @apply text-xl font-medium; }
|
||||
.text-heading-4 { @apply text-lg font-medium; }
|
||||
.text-display { @apply text-4xl tracking-tight; font-weight: 200; }
|
||||
.text-heading-1 { @apply text-3xl tracking-tight; font-weight: 300; }
|
||||
.text-heading-2 { @apply text-2xl; font-weight: 400; }
|
||||
.text-heading-3 { @apply text-xl; font-weight: 400; }
|
||||
.text-heading-4 { @apply text-lg; font-weight: 400; }
|
||||
.text-body-lg { @apply text-base leading-relaxed; }
|
||||
.text-body { @apply text-sm leading-relaxed; }
|
||||
.text-caption { @apply text-xs text-muted-foreground; }
|
||||
.text-label { @apply text-xs font-medium uppercase tracking-wider text-muted-foreground; }
|
||||
|
||||
/* SUMI typography classes (from design system) */
|
||||
.sumi-display { font-family: var(--sumi-font-heading); font-size: var(--sumi-text-4xl); font-weight: var(--sumi-weight-bold); line-height: var(--sumi-leading-tight); letter-spacing: var(--sumi-tracking-tighter); }
|
||||
.sumi-h1 { font-family: var(--sumi-font-heading); font-size: var(--sumi-text-3xl); font-weight: var(--sumi-weight-semibold); line-height: var(--sumi-leading-tight); letter-spacing: var(--sumi-tracking-tight); }
|
||||
.sumi-h2 { font-family: var(--sumi-font-heading); font-size: var(--sumi-text-2xl); font-weight: var(--sumi-weight-semibold); line-height: var(--sumi-leading-snug); letter-spacing: var(--sumi-tracking-tight); }
|
||||
.sumi-h3 { font-family: var(--sumi-font-heading); font-size: var(--sumi-text-xl); font-weight: var(--sumi-weight-medium); line-height: var(--sumi-leading-snug); }
|
||||
.sumi-h4 { font-family: var(--sumi-font-heading); font-size: var(--sumi-text-lg); font-weight: var(--sumi-weight-medium); line-height: var(--sumi-leading-snug); }
|
||||
.sumi-display { font-family: var(--sumi-font-heading); font-size: var(--sumi-text-4xl); font-weight: var(--sumi-weight-extralight); line-height: var(--sumi-leading-tight); letter-spacing: var(--sumi-tracking-tighter); }
|
||||
.sumi-h1 { font-family: var(--sumi-font-heading); font-size: var(--sumi-text-3xl); font-weight: var(--sumi-weight-light); line-height: var(--sumi-leading-tight); letter-spacing: var(--sumi-tracking-tight); }
|
||||
.sumi-h2 { font-family: var(--sumi-font-heading); font-size: var(--sumi-text-2xl); font-weight: var(--sumi-weight-regular); line-height: var(--sumi-leading-snug); letter-spacing: var(--sumi-tracking-tight); }
|
||||
.sumi-h3 { font-family: var(--sumi-font-heading); font-size: var(--sumi-text-xl); font-weight: var(--sumi-weight-regular); line-height: var(--sumi-leading-snug); }
|
||||
.sumi-h4 { font-family: var(--sumi-font-heading); font-size: var(--sumi-text-lg); font-weight: var(--sumi-weight-regular); line-height: var(--sumi-leading-snug); }
|
||||
.sumi-body-lg { font-size: var(--sumi-text-md); font-weight: var(--sumi-weight-regular); line-height: var(--sumi-leading-relaxed); }
|
||||
.sumi-body { font-size: var(--sumi-text-base); font-weight: var(--sumi-weight-regular); line-height: var(--sumi-leading-normal); }
|
||||
.sumi-body-sm { font-size: var(--sumi-text-sm); font-weight: var(--sumi-weight-regular); line-height: var(--sumi-leading-normal); }
|
||||
|
|
@ -758,19 +851,74 @@
|
|||
.animate-vinyl-spin.paused { animation-play-state: paused; }
|
||||
.animate-slide-in-right { animation: slide-in-right var(--sumi-duration-normal) var(--sumi-ease-spring); }
|
||||
.animate-subtle-float { animation: subtle-float 6s ease-in-out infinite; }
|
||||
.animate-card-enter { animation: card-enter var(--sumi-duration-slow) var(--sumi-ease-out) both; }
|
||||
|
||||
/* Now-playing EQ bars container */
|
||||
.eq-bars {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 2px;
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
}
|
||||
.eq-bars > span {
|
||||
display: block;
|
||||
width: 3px;
|
||||
border-radius: 1px;
|
||||
background: currentColor;
|
||||
}
|
||||
.eq-bars > span:nth-child(1) { animation: eq-bar-1 0.8s ease-in-out infinite; }
|
||||
.eq-bars > span:nth-child(2) { animation: eq-bar-2 0.7s ease-in-out infinite; }
|
||||
.eq-bars > span:nth-child(3) { animation: eq-bar-3 0.9s ease-in-out infinite; }
|
||||
.eq-bars.paused > span { animation-play-state: paused; height: 30%; }
|
||||
|
||||
/* Gradient text animation */
|
||||
.animate-gradient-text {
|
||||
background-size: 200% auto;
|
||||
animation: gradient-shift 3s ease-in-out infinite;
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
/* Section divider — brush stroke (筆致) */
|
||||
.section-divider {
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg,
|
||||
transparent 0%,
|
||||
var(--sumi-border-faint) 8%,
|
||||
var(--sumi-border-strong) 25%,
|
||||
var(--sumi-border-strong) 75%,
|
||||
var(--sumi-border-faint) 92%,
|
||||
transparent 100%
|
||||
);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Gold leaf line — 金箔 accent divider */
|
||||
.gold-line {
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, transparent 0%, rgba(184,134,11,0.4) 30%, rgba(184,134,11,0.15) 70%, transparent 100%);
|
||||
}
|
||||
|
||||
/* Player bar entrance */
|
||||
.player-bar-entrance {
|
||||
animation: player-bar-entrance var(--sumi-duration-slower) var(--sumi-ease-spring) both;
|
||||
}
|
||||
|
||||
/* Spotify-style progress bar */
|
||||
/* Progress bar — ink flow */
|
||||
.progress-bar-animated {
|
||||
background: linear-gradient(90deg, var(--sumi-accent) 0%, var(--sumi-accent-hover) 50%, var(--sumi-accent) 100%);
|
||||
background-size: 200% 100%;
|
||||
animation: progress-shimmer 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Sumi-e animation utilities */
|
||||
.animate-ink-drop { animation: sumi-ink-drop var(--sumi-duration-slow) var(--sumi-ease-spring); }
|
||||
.animate-ink-spread { animation: sumi-ink-spread var(--sumi-duration-slower) var(--sumi-ease-out); }
|
||||
.animate-brush-stroke { animation: sumi-brush-stroke var(--sumi-duration-slow) var(--sumi-ease-out); }
|
||||
.animate-ink-reveal { animation: sumi-ink-reveal var(--sumi-duration-slower) var(--sumi-ease-out) both; }
|
||||
|
||||
/* Discord-style hover card lift */
|
||||
.hover-lift {
|
||||
transition: transform var(--sumi-duration-normal) var(--sumi-ease-out),
|
||||
|
|
@ -808,25 +956,174 @@
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Ink Wash Texture (hero/feature sections) */
|
||||
/* Ink Wash Texture — sumi-nagashi (墨流し) */
|
||||
.sumi-wash-texture { position: relative; }
|
||||
.sumi-wash-texture::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background:
|
||||
radial-gradient(ellipse at 20% 50%, var(--sumi-accent-subtle) 0%, transparent 60%),
|
||||
radial-gradient(ellipse at 80% 20%, rgba(201,168,76,0.04) 0%, transparent 50%);
|
||||
radial-gradient(ellipse at 15% 60%, var(--sumi-accent-subtle) 0%, transparent 55%),
|
||||
radial-gradient(ellipse at 85% 25%, rgba(255,255,255,0.015) 0%, transparent 45%),
|
||||
radial-gradient(ellipse at 50% 90%, rgba(184,58,30,0.03) 0%, transparent 50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Noise texture utility */
|
||||
/* Ink bleed hover — bokashi (暈し) */
|
||||
.sumi-ink-bleed { position: relative; overflow: hidden; }
|
||||
.sumi-ink-bleed::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(circle at var(--mouse-x, 50%) var(--mouse-y, 50%),
|
||||
rgba(184,58,30, 0.06) 0%,
|
||||
transparent 55%);
|
||||
opacity: 0;
|
||||
transition: opacity var(--sumi-duration-normal) var(--sumi-ease-default);
|
||||
pointer-events: none;
|
||||
}
|
||||
.sumi-ink-bleed:hover::after { opacity: 1; }
|
||||
|
||||
/* Hanko stamp — subtle seal impression (判子) */
|
||||
.sumi-hanko {
|
||||
transform: rotate(-1.5deg);
|
||||
box-shadow:
|
||||
1px 1px 0 rgba(184,58,30, 0.2),
|
||||
-0.5px -0.5px 0 rgba(0,0,0, 0.08);
|
||||
}
|
||||
|
||||
/* Notan — dramatic light/dark contrast container (濃淡) */
|
||||
.sumi-notan {
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
var(--sumi-bg-void) 0%,
|
||||
var(--sumi-bg-base) 30%,
|
||||
var(--sumi-bg-raised) 100%
|
||||
);
|
||||
}
|
||||
|
||||
/* Bokashi gradient wash — for hero backgrounds (暈し) */
|
||||
.sumi-bokashi {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--sumi-bg-void) 0%,
|
||||
var(--sumi-bg-base) 40%,
|
||||
var(--sumi-bg-wash) 60%,
|
||||
var(--sumi-bg-void) 100%
|
||||
);
|
||||
}
|
||||
|
||||
/* Sumi-nagashi marble background — floating ink (墨流し) */
|
||||
.sumi-nagashi {
|
||||
background:
|
||||
radial-gradient(ellipse at 20% 50%, var(--sumi-bg-wash) 0%, transparent 50%),
|
||||
radial-gradient(ellipse at 80% 50%, var(--sumi-bg-raised) 0%, transparent 40%),
|
||||
radial-gradient(ellipse at 50% 80%, rgba(184,58,30,0.02) 0%, transparent 45%),
|
||||
var(--sumi-bg-void);
|
||||
background-size: 200% 200%;
|
||||
}
|
||||
.sumi-nagashi.animate { animation: sumi-nagashi 25s ease-in-out infinite; }
|
||||
|
||||
/* Ink card — atmospheric container with wash background */
|
||||
.ink-card {
|
||||
position: relative;
|
||||
border-radius: var(--sumi-radius-sm);
|
||||
overflow: hidden;
|
||||
}
|
||||
.ink-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -20px;
|
||||
background:
|
||||
radial-gradient(ellipse at 25% 25%, rgba(232,224,208, 0.035) 0%, transparent 55%),
|
||||
radial-gradient(ellipse at 75% 75%, rgba(232,224,208, 0.025) 0%, transparent 50%);
|
||||
filter: blur(8px);
|
||||
pointer-events: none;
|
||||
}
|
||||
.ink-card::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0; left: 5%; right: 10%;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, transparent 0%, var(--sumi-border-default) 20%, var(--sumi-border-faint) 60%, transparent 100%);
|
||||
pointer-events: none;
|
||||
}
|
||||
.ink-card > * { position: relative; z-index: 1; }
|
||||
|
||||
/* Ghost kanji — large decorative character */
|
||||
.ghost-kanji {
|
||||
position: absolute;
|
||||
font-family: var(--sumi-font-heading);
|
||||
font-weight: 200;
|
||||
opacity: 0.03;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
line-height: 1;
|
||||
z-index: 0;
|
||||
}
|
||||
[data-theme="light"] .ghost-kanji { opacity: 0.05; }
|
||||
|
||||
/* Brush underline — vermillion stroke accent */
|
||||
.brush-underline::after {
|
||||
content: '';
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
height: 2px;
|
||||
width: min(120px, 40%);
|
||||
background: linear-gradient(90deg, var(--sumi-accent) 0%, transparent 100%);
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
/* Hanko seal — logo mark */
|
||||
.hanko-seal {
|
||||
position: relative;
|
||||
transform: rotate(-2deg);
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(184,58,30, 0.25),
|
||||
0 2px 8px rgba(184,58,30, 0.12);
|
||||
overflow: hidden;
|
||||
}
|
||||
.hanko-seal::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='50' height='50'%3E%3Cfilter id='n'%3E%3CfeTurbulence baseFrequency='0.8' numOctaves='3'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.15'/%3E%3C/svg%3E");
|
||||
mix-blend-mode: overlay;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Brush stroke divider — tapered ink line */
|
||||
.brush-divider {
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg,
|
||||
var(--sumi-border-strong) 0%,
|
||||
var(--sumi-border-default) 40%,
|
||||
var(--sumi-border-faint) 70%,
|
||||
transparent 100%);
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
/* Vertical brush stroke border — for sidebar/panel edges */
|
||||
.sumi-stroke-border-left {
|
||||
border-left: 2px solid var(--sumi-border-strong);
|
||||
border-image: linear-gradient(
|
||||
to bottom,
|
||||
transparent 0%,
|
||||
var(--sumi-border-default) 10%,
|
||||
var(--sumi-border-strong) 30%,
|
||||
var(--sumi-border-strong) 70%,
|
||||
var(--sumi-border-default) 90%,
|
||||
transparent 100%
|
||||
) 1;
|
||||
}
|
||||
|
||||
/* Noise texture utility — washi fiber (和紙) */
|
||||
.noise { position: relative; }
|
||||
.noise::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.7' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.04'/%3E%3C/svg%3E");
|
||||
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 512 512' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.35 0.85' numOctaves='5' stitchTiles='stitch' seed='3'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.05'/%3E%3C/svg%3E");
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
mix-blend-mode: overlay;
|
||||
|
|
@ -844,6 +1141,12 @@
|
|||
.progress-bar-animated { animation: none; }
|
||||
.hover-lift { transition: none; }
|
||||
.hover-lift:hover { transform: none; }
|
||||
.animate-ink-drop { animation: none; }
|
||||
.animate-ink-spread { animation: none; }
|
||||
.animate-brush-stroke { animation: none; }
|
||||
.animate-ink-reveal { animation: none; }
|
||||
.sumi-nagashi.animate { animation: none; }
|
||||
.sumi-ink-bleed::after { display: none; }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -993,16 +1296,16 @@
|
|||
to { opacity: 1; transform: translateY(0) scale(1); }
|
||||
}
|
||||
|
||||
/* Spotify-style content reveal */
|
||||
/* Ink content reveal — emerges from darkness */
|
||||
@keyframes content-reveal {
|
||||
from { opacity: 0; transform: translateY(16px) scale(0.99); filter: blur(4px); }
|
||||
from { opacity: 0; transform: translateY(16px) scale(0.99); filter: blur(6px); }
|
||||
to { opacity: 1; transform: translateY(0) scale(1); filter: blur(0); }
|
||||
}
|
||||
|
||||
/* Discord-style hover glow */
|
||||
/* Sumi ink glow — subtle vermillion breathe */
|
||||
@keyframes glow-breathe {
|
||||
0%, 100% { box-shadow: 0 0 0 0 rgba(124,157,214, 0); }
|
||||
50% { box-shadow: 0 0 20px 2px rgba(124,157,214, 0.15); }
|
||||
0%, 100% { box-shadow: 0 0 0 0 rgba(184,58,30, 0); }
|
||||
50% { box-shadow: 0 0 20px 2px rgba(184,58,30, 0.10); }
|
||||
}
|
||||
|
||||
/* Vinyl spin for now-playing */
|
||||
|
|
@ -1039,6 +1342,96 @@
|
|||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
/* Now-playing EQ bars — 3-bar audio visualizer */
|
||||
@keyframes eq-bar-1 {
|
||||
0%, 100% { height: 30%; }
|
||||
25% { height: 80%; }
|
||||
50% { height: 50%; }
|
||||
75% { height: 100%; }
|
||||
}
|
||||
@keyframes eq-bar-2 {
|
||||
0%, 100% { height: 50%; }
|
||||
20% { height: 100%; }
|
||||
45% { height: 30%; }
|
||||
70% { height: 80%; }
|
||||
}
|
||||
@keyframes eq-bar-3 {
|
||||
0%, 100% { height: 60%; }
|
||||
30% { height: 40%; }
|
||||
55% { height: 100%; }
|
||||
80% { height: 45%; }
|
||||
}
|
||||
|
||||
/* Gradient text shimmer (for headings) */
|
||||
@keyframes gradient-shift {
|
||||
0% { background-position: 0% 50%; }
|
||||
50% { background-position: 100% 50%; }
|
||||
100% { background-position: 0% 50%; }
|
||||
}
|
||||
|
||||
/* Gentle card entrance — stagger-friendly */
|
||||
@keyframes card-enter {
|
||||
from { opacity: 0; transform: translateY(12px) scale(0.97); }
|
||||
to { opacity: 1; transform: translateY(0) scale(1); }
|
||||
}
|
||||
|
||||
/* Ripple expand for empty states */
|
||||
@keyframes ripple-expand {
|
||||
0% { transform: scale(0.8); opacity: 0.4; }
|
||||
50% { transform: scale(1.15); opacity: 0.15; }
|
||||
100% { transform: scale(0.8); opacity: 0.4; }
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
SUMI-E ANIMATIONS — 墨絵 Ink Wash Effects
|
||||
═══════════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* Ink drop — element appears like ink hitting wet paper */
|
||||
@keyframes sumi-ink-drop {
|
||||
0% { transform: scale(0); opacity: 0; border-radius: 50%; }
|
||||
35% { transform: scale(1.08); opacity: 0.85; border-radius: 48% 52% 50% 50%; }
|
||||
55% { transform: scale(0.96); opacity: 1; }
|
||||
75% { transform: scale(1.02); }
|
||||
100% { transform: scale(1); opacity: 1; border-radius: inherit; }
|
||||
}
|
||||
|
||||
/* Ink spread — circular reveal like ink spreading on washi */
|
||||
@keyframes sumi-ink-spread {
|
||||
0% { clip-path: circle(0% at 50% 50%); filter: blur(6px); opacity: 0; }
|
||||
40% { filter: blur(2px); opacity: 0.8; }
|
||||
100% { clip-path: circle(100% at 50% 50%); filter: blur(0); opacity: 1; }
|
||||
}
|
||||
|
||||
/* Brush stroke reveal — horizontal wipe like a calligraphy stroke */
|
||||
@keyframes sumi-brush-stroke {
|
||||
0% { clip-path: inset(0 100% 0 0); opacity: 0.4; }
|
||||
60% { clip-path: inset(0 8% 0 0); opacity: 0.9; }
|
||||
100% { clip-path: inset(0 0 0 0); opacity: 1; }
|
||||
}
|
||||
|
||||
/* Ink reveal — content emerges from darkness like ink on wet paper */
|
||||
@keyframes sumi-ink-reveal {
|
||||
0% { opacity: 0; transform: translateY(12px); filter: blur(8px) brightness(0.7); }
|
||||
40% { filter: blur(3px) brightness(0.9); }
|
||||
100% { opacity: 1; transform: translateY(0); filter: blur(0) brightness(1); }
|
||||
}
|
||||
|
||||
/* Sumi-nagashi — floating ink marble drift */
|
||||
@keyframes sumi-nagashi {
|
||||
0% { background-position: 0% 50%; }
|
||||
25% { background-position: 100% 25%; }
|
||||
50% { background-position: 100% 75%; }
|
||||
75% { background-position: 0% 60%; }
|
||||
100% { background-position: 0% 50%; }
|
||||
}
|
||||
|
||||
/* Ink pulse — like a droplet ripple in an ink stone (硯) */
|
||||
@keyframes sumi-ink-pulse {
|
||||
0% { box-shadow: 0 0 0 0 rgba(184,58,30, 0.25); }
|
||||
50% { box-shadow: 0 0 0 8px rgba(184,58,30, 0); }
|
||||
100% { box-shadow: 0 0 0 0 rgba(184,58,30, 0); }
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
REDUCED MOTION
|
||||
═══════════════════════════════════════════════════════════════════════════ */
|
||||
|
|
|
|||
|
|
@ -1,72 +1,82 @@
|
|||
/**
|
||||
* SUMI Design System v2.0 — Color Tokens
|
||||
* "L'encre et la lumière" — Ink and Light
|
||||
* SUMI Design System v4.0 — Color Tokens
|
||||
* "Lavis d'encre" — Ink Wash (墨の濃淡)
|
||||
*
|
||||
* Named after traditional Japanese ink tones:
|
||||
* Kuro (黒), Sumi (墨), Usuzumi (薄墨), Hai (灰), Gin (銀), Kasumi (霞)
|
||||
* Shiro (白), Kinari (生成), Kinu (絹), Torinoko (鳥の子), Cha (茶)
|
||||
*
|
||||
* Source of truth: apps/web/src/index.css
|
||||
* These tokens mirror the CSS custom properties for use in TypeScript.
|
||||
*/
|
||||
|
||||
// ═══ DARK THEME (default) ═══
|
||||
// ═══ DARK THEME (default) — Ink on void (墨の闇) ═══
|
||||
|
||||
export const backgrounds = {
|
||||
void: '#0c0c0f',
|
||||
base: '#121215',
|
||||
raised: '#1a1a1f',
|
||||
overlay: '#222228',
|
||||
hover: '#2a2a31',
|
||||
active: '#32323a',
|
||||
wash: '#18181d',
|
||||
void: '#0d0d0b',
|
||||
base: '#13110f',
|
||||
raised: '#1a1714',
|
||||
overlay: '#242018',
|
||||
hover: '#2e2a22',
|
||||
active: '#383228',
|
||||
wash: '#17140f',
|
||||
} as const;
|
||||
|
||||
export const surfaces = {
|
||||
inset: '#101013',
|
||||
subtle: '#1e1e24',
|
||||
card: '#1a1a1f',
|
||||
elevated: '#242430',
|
||||
inset: '#0f0d0b',
|
||||
subtle: '#1e1a15',
|
||||
card: '#1a1714',
|
||||
elevated: '#242018',
|
||||
} as const;
|
||||
|
||||
export const borders = {
|
||||
faint: 'rgba(255,255,255, 0.06)',
|
||||
default: 'rgba(255,255,255, 0.10)',
|
||||
strong: 'rgba(255,255,255, 0.16)',
|
||||
focus: 'rgba(139,170,220, 0.50)',
|
||||
accent: 'rgba(139,170,220, 0.30)',
|
||||
faint: 'rgba(232,224,208, 0.03)',
|
||||
default: 'rgba(232,224,208, 0.06)',
|
||||
strong: 'rgba(232,224,208, 0.10)',
|
||||
focus: 'rgba(184,58,30, 0.50)',
|
||||
accent: 'rgba(184,58,30, 0.25)',
|
||||
} as const;
|
||||
|
||||
export const text = {
|
||||
primary: '#f0ede8',
|
||||
secondary: '#a8a4a0',
|
||||
tertiary: '#706c68',
|
||||
disabled: '#4a4844',
|
||||
inverse: '#121215',
|
||||
link: '#8baade',
|
||||
primary: '#e8e0d0',
|
||||
secondary: '#9e9688',
|
||||
tertiary: '#6b6560',
|
||||
disabled: '#3d3930',
|
||||
inverse: '#13110f',
|
||||
link: '#b83a1e',
|
||||
} as const;
|
||||
|
||||
// ═══ PIGMENTS — The 4 accent pigments ═══
|
||||
// ═══ PIGMENTS — Shu vermillion primary (朱) + Kin gold (金) ═══
|
||||
|
||||
export const pigments = {
|
||||
accent: {
|
||||
base: '#7c9dd6',
|
||||
hover: '#93afe0',
|
||||
active: '#6b8dc6',
|
||||
muted: 'rgba(124,157,214, 0.20)',
|
||||
subtle: 'rgba(124,157,214, 0.12)',
|
||||
emphasis: '#5a7fba',
|
||||
base: '#b83a1e',
|
||||
hover: '#c84a2e',
|
||||
active: '#8b2500',
|
||||
muted: 'rgba(184,58,30, 0.18)',
|
||||
subtle: 'rgba(184,58,30, 0.06)',
|
||||
emphasis: '#8b2500',
|
||||
},
|
||||
vermillion: {
|
||||
base: '#d4634a',
|
||||
hover: '#de7a64',
|
||||
subtle: 'rgba(212,99,74, 0.12)',
|
||||
base: '#a04050',
|
||||
hover: '#b05060',
|
||||
subtle: 'rgba(160,64,80, 0.10)',
|
||||
},
|
||||
sage: {
|
||||
base: '#7a9e6c',
|
||||
hover: '#8eb280',
|
||||
subtle: 'rgba(122,158,108, 0.12)',
|
||||
base: '#4f6840',
|
||||
hover: '#5f7850',
|
||||
subtle: 'rgba(79,104,64, 0.10)',
|
||||
},
|
||||
gold: {
|
||||
base: '#c9a84c',
|
||||
hover: '#d6b860',
|
||||
subtle: 'rgba(201,168,76, 0.12)',
|
||||
base: '#b8860b',
|
||||
hover: '#c8960b',
|
||||
subtle: 'rgba(184,134,11, 0.10)',
|
||||
},
|
||||
kin: {
|
||||
base: '#b8860b',
|
||||
glow: '0 0 8px rgba(184,134,11, 0.2)',
|
||||
},
|
||||
patinaGreen: {
|
||||
base: '#5A8A72',
|
||||
},
|
||||
} as const;
|
||||
|
||||
|
|
@ -80,66 +90,88 @@ export const semantic = {
|
|||
error: pigments.vermillion.base,
|
||||
errorSubtle: pigments.vermillion.subtle,
|
||||
info: pigments.accent.base,
|
||||
live: '#e05a5a',
|
||||
live: '#a04050',
|
||||
online: pigments.sage.base,
|
||||
} as const;
|
||||
|
||||
// ═══ GLASS ═══
|
||||
// ═══ GLASS — Shoji screen (障子) ═══
|
||||
|
||||
export const glass = {
|
||||
bg: 'rgba(18,18,21, 0.80)',
|
||||
border: 'rgba(255,255,255, 0.08)',
|
||||
bg: 'rgba(19,17,15, 0.80)',
|
||||
border: 'rgba(232,224,208, 0.04)',
|
||||
blur: '12px',
|
||||
} as const;
|
||||
|
||||
// ═══ SHADOWS ═══
|
||||
// ═══ SHADOWS — Ink diffusion (滲み) ═══
|
||||
|
||||
export const shadows = {
|
||||
xs: '0 1px 2px rgba(0,0,0,0.30)',
|
||||
sm: '0 2px 4px rgba(0,0,0,0.25), 0 1px 2px rgba(0,0,0,0.20)',
|
||||
md: '0 4px 12px rgba(0,0,0,0.30), 0 2px 4px rgba(0,0,0,0.15)',
|
||||
lg: '0 8px 24px rgba(0,0,0,0.35), 0 4px 8px rgba(0,0,0,0.20)',
|
||||
xl: '0 16px 48px rgba(0,0,0,0.40), 0 8px 16px rgba(0,0,0,0.20)',
|
||||
'2xl': '0 24px 64px rgba(0,0,0,0.50)',
|
||||
glow: '0 0 0 3px rgba(124,157,214,0.25)',
|
||||
glowLg: '0 0 20px rgba(124,157,214,0.15)',
|
||||
xs: '0 1px 3px rgba(13,13,11,0.20)',
|
||||
sm: '0 2px 8px rgba(13,13,11,0.15), 0 1px 3px rgba(13,13,11,0.12)',
|
||||
md: '0 4px 20px rgba(13,13,11,0.15), 0 2px 6px rgba(13,13,11,0.10)',
|
||||
lg: '0 8px 35px rgba(13,13,11,0.18), 0 4px 12px rgba(13,13,11,0.10)',
|
||||
xl: '0 16px 55px rgba(13,13,11,0.22), 0 8px 20px rgba(13,13,11,0.12)',
|
||||
'2xl': '0 24px 75px rgba(13,13,11,0.30)',
|
||||
glow: '0 0 0 3px rgba(184,58,30, 0.20)',
|
||||
glowLg: '0 0 20px rgba(184,58,30, 0.10)',
|
||||
kin: '0 0 16px rgba(184,134,11, 0.15)',
|
||||
} as const;
|
||||
|
||||
// ═══ LIGHT THEME ═══
|
||||
// ═══ LIGHT THEME — Washi paper (和紙) ═══
|
||||
|
||||
export const lightTheme = {
|
||||
backgrounds: {
|
||||
void: '#f5f2ed',
|
||||
base: '#faf8f5',
|
||||
raised: '#ffffff',
|
||||
overlay: '#f0ede8',
|
||||
hover: '#e8e4df',
|
||||
active: '#ddd9d3',
|
||||
wash: '#f7f5f0',
|
||||
void: '#e8e0cf',
|
||||
base: '#f0ebe0',
|
||||
raised: '#f0ebe0',
|
||||
overlay: '#f0ebe0',
|
||||
hover: '#e4dccb',
|
||||
active: '#ddd4c0',
|
||||
wash: '#ece5d6',
|
||||
},
|
||||
surfaces: {
|
||||
inset: '#ede9e4',
|
||||
subtle: '#f5f2ed',
|
||||
card: '#ffffff',
|
||||
elevated: '#ffffff',
|
||||
inset: '#e0d8c8',
|
||||
subtle: '#e8e0cf',
|
||||
card: '#f0ebe0',
|
||||
elevated: '#f5f0e5',
|
||||
},
|
||||
borders: {
|
||||
faint: 'rgba(0,0,0, 0.06)',
|
||||
default: 'rgba(0,0,0, 0.10)',
|
||||
strong: 'rgba(0,0,0, 0.18)',
|
||||
focus: 'rgba(90,127,186, 0.50)',
|
||||
accent: 'rgba(90,127,186, 0.25)',
|
||||
faint: 'rgba(26,23,20, 0.04)',
|
||||
default: 'rgba(26,23,20, 0.06)',
|
||||
strong: 'rgba(26,23,20, 0.12)',
|
||||
focus: 'rgba(139,37,0, 0.45)',
|
||||
accent: 'rgba(139,37,0, 0.20)',
|
||||
},
|
||||
text: {
|
||||
primary: '#1a1a1f',
|
||||
secondary: '#5c5854',
|
||||
tertiary: '#8a8680',
|
||||
disabled: '#b8b4b0',
|
||||
inverse: '#f0ede8',
|
||||
link: '#5a7fba',
|
||||
primary: '#1a1714',
|
||||
secondary: '#3d3930',
|
||||
tertiary: '#6b6560',
|
||||
disabled: '#c4bba8',
|
||||
inverse: '#e8e0d0',
|
||||
link: '#8b2500',
|
||||
},
|
||||
} as const;
|
||||
|
||||
// ═══ INK TONES — 墨の六色 ═══
|
||||
|
||||
export const inkTones = {
|
||||
kuro: '#0d0d0b',
|
||||
sumi: '#1a1714',
|
||||
usuzumi: '#3d3930',
|
||||
hai: '#6b6560',
|
||||
gin: '#9e9688',
|
||||
kasumi: '#c4bba8',
|
||||
} as const;
|
||||
|
||||
// ═══ WASHI TONES — 和紙の色 ═══
|
||||
|
||||
export const washiTones = {
|
||||
shiro: '#f0ebe0',
|
||||
kinari: '#e8e0cf',
|
||||
kinu: '#ddd4c0',
|
||||
torinoko: '#d0c5ad',
|
||||
cha: '#c4b698',
|
||||
} as const;
|
||||
|
||||
// ═══ ALL COLORS ═══
|
||||
|
||||
export const colors = {
|
||||
|
|
@ -152,6 +184,8 @@ export const colors = {
|
|||
glass,
|
||||
shadows,
|
||||
lightTheme,
|
||||
inkTones,
|
||||
washiTones,
|
||||
} as const;
|
||||
|
||||
export type SumiColor = typeof colors;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* SUMI Design System v2.0 — Motion & Animation Tokens
|
||||
* SUMI Design System v4.0 — Motion & Animation Tokens
|
||||
*
|
||||
* Source of truth: apps/web/src/index.css
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* SUMI Design System v2.0 — Spacing & Layout Tokens
|
||||
* SUMI Design System v4.0 — Spacing & Layout Tokens
|
||||
*
|
||||
* Source of truth: apps/web/src/index.css
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
/**
|
||||
* SUMI Design System v2.0 — Typography Tokens
|
||||
* SUMI Design System v4.0 — Typography Tokens
|
||||
* "Lavis d'encre" — Brush calligraphy meets clean sans
|
||||
*
|
||||
* Source of truth: apps/web/src/index.css
|
||||
*/
|
||||
|
||||
export const fontFamilies = {
|
||||
body: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
|
||||
heading: "'Space Grotesk', 'Inter', sans-serif",
|
||||
heading: "'Noto Serif JP', Georgia, serif",
|
||||
mono: "'JetBrains Mono', 'SF Mono', 'Consolas', monospace",
|
||||
serif: "'Noto Serif JP', Georgia, serif",
|
||||
} as const;
|
||||
|
|
|
|||
Loading…
Reference in a new issue