feat(admin-ui): shadcn/ui + Tailwind v4 foundation + theme switch
This commit is contained in:
@@ -14,21 +14,36 @@
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@nstart/ui": "^0.1.3",
|
||||
"@phosphor-icons/react": "^2.1.10",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-label": "^2.1.8",
|
||||
"@radix-ui/react-popover": "^1.1.15",
|
||||
"@radix-ui/react-separator": "^1.1.8",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@tanstack/react-query": "^5.62.0",
|
||||
"@tanstack/react-router": "^1.86.0",
|
||||
"ajv": "^8.17.1",
|
||||
"ajv-formats": "^3.0.1",
|
||||
"axios": "^1.7.9",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"i18next": "^26.0.3",
|
||||
"i18next-browser-languagedetector": "^8.0.2",
|
||||
"leaflet": "^1.9.4",
|
||||
"lucide-react": "^1.14.0",
|
||||
"oidc-client-ts": "^3.5.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"react-i18next": "^17.0.2",
|
||||
"react-leaflet": "^5.0.0",
|
||||
"react-oidc-context": "^3.3.1"
|
||||
"react-oidc-context": "^3.3.1",
|
||||
"tailwind-merge": "^3.6.0",
|
||||
"tw-animate-css": "^1.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.0.0-beta.7",
|
||||
|
||||
Generated
+1035
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,11 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
|
||||
<!-- Background: rounded square в brand ultramarain. Tailwind rounded-sm = 2px → ratio 2/32 = 6.25% → r=2 для viewBox 32. -->
|
||||
<rect width="32" height="32" rx="6" ry="6" fill="#120a8f"/>
|
||||
<!-- 'O' — Ordinis. Stroke-only (open ring), match'ит open design system без heavy fills. -->
|
||||
<circle cx="16" cy="16" r="7.5" fill="none" stroke="#ffffff" stroke-width="2.5"/>
|
||||
<!-- Спутник: маленький точка на орбите вокруг 'O'. Связь с ДЗЗ-доменом. -->
|
||||
<circle cx="25.5" cy="9.5" r="1.7" fill="#ffffff"/>
|
||||
<!-- Orbit trace: тонкая дуга через спутник, opacity 0.35 чтобы не доминировала. -->
|
||||
<path d="M 8 24 Q 16 30 26 22" fill="none" stroke="#ffffff" stroke-width="0.7" stroke-linecap="round" opacity="0.35"/>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" width="32" height="32">
|
||||
<!-- Brand mark v2 (handoff redesign): rounded-square в navy/coffee tone +
|
||||
gold знак из logo-ujcvk01-dark-gold.svg. Цвета matched к --color-navy
|
||||
и --color-warn handoff palette — при 16×16 в browser tab остаётся
|
||||
читаемым на light и dark backgrounds. -->
|
||||
<rect width="200" height="200" rx="36" ry="36" fill="#3a2818"/>
|
||||
<g fill="#d4a653" transform="matrix(.085 0 0 -.085 7.55 192.45)">
|
||||
<path d="m950 1292v-512h65 65l2 266 3 265 183-327 183-328-3-60-3-59-175-108c-96-59-178-108-182-108-5-1-8 80-8 179v180h-65-65v-295c0-162 2-295 5-295 2 0 143 85 312 188l308 189 3 109 3 109-307 550c-168 303-310 554-315 560-5 5-9-193-9-503z"/>
|
||||
<path d="m577 1238-308-552 3-110 3-109 308-189c169-103 310-188 312-188 3 0 4 132 3 293l-3 292-62 3-63 3v-181c0-99-3-180-7-180-5 0-87 49-183 108l-175 108-3 61-3 61 183 328 183 328 3-267 2-268 63 3 62 3 3 503c1 276-1 502-6 502-4 0-146-248-315-552z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 909 B After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,57 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Monitor, Moon, Sun } from 'lucide-react'
|
||||
import { useTheme, type ThemePreference } from '@/stores/ThemeProvider'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
/**
|
||||
* Tri-state theme switch: Light / Dark / System.
|
||||
*
|
||||
* <p>Segmented control с тремя иконками. Активный — `--color-accent`
|
||||
* background + `--color-on-accent` foreground. Click меняет `preference`
|
||||
* в {@link ThemeProvider}, который пишет в localStorage и переключает
|
||||
* `data-theme` на html.
|
||||
*
|
||||
* <p>System icon показывает «follow OS» — auto-update при OS theme change.
|
||||
*/
|
||||
export function ThemeSwitch() {
|
||||
const { t } = useTranslation()
|
||||
const { preference, setPreference } = useTheme()
|
||||
|
||||
const items: { id: ThemePreference; label: string; icon: typeof Sun }[] = [
|
||||
{ id: 'light', label: t('theme.light'), icon: Sun },
|
||||
{ id: 'dark', label: t('theme.dark'), icon: Moon },
|
||||
{ id: 'system', label: t('theme.system'), icon: Monitor },
|
||||
]
|
||||
|
||||
return (
|
||||
<div
|
||||
role="radiogroup"
|
||||
aria-label={t('theme.label')}
|
||||
className="inline-flex items-center rounded-md border border-line bg-surface p-0.5"
|
||||
>
|
||||
{items.map((item) => {
|
||||
const Icon = item.icon
|
||||
const active = preference === item.id
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={active}
|
||||
aria-label={item.label}
|
||||
title={item.label}
|
||||
onClick={() => setPreference(item.id)}
|
||||
className={cn(
|
||||
'inline-flex h-6 w-6 items-center justify-center rounded transition-colors',
|
||||
active
|
||||
? 'bg-accent text-on-accent'
|
||||
: 'text-mute hover:text-ink hover:bg-surface-2',
|
||||
)}
|
||||
>
|
||||
<Icon size={13} strokeWidth={2.25} />
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -13,6 +13,11 @@ i18n
|
||||
'ru-RU': {
|
||||
translation: {
|
||||
'app.title': 'Ordinis',
|
||||
'theme.label': 'Тема',
|
||||
'theme.light': 'Светлая',
|
||||
'theme.dark': 'Тёмная',
|
||||
'theme.system': 'Системная',
|
||||
'nav.docs': 'Руководство',
|
||||
'updateBanner.message': 'Доступна новая версия НСИ ({{version}}). Обновите страницу, чтобы получить последние исправления.',
|
||||
'updateBanner.reload': 'Обновить',
|
||||
'updateBanner.docs': 'Что нового',
|
||||
@@ -513,6 +518,11 @@ i18n
|
||||
'en-US': {
|
||||
translation: {
|
||||
'app.title': 'Ordinis MDM',
|
||||
'theme.label': 'Theme',
|
||||
'theme.light': 'Light',
|
||||
'theme.dark': 'Dark',
|
||||
'theme.system': 'System',
|
||||
'nav.docs': 'Docs',
|
||||
'updateBanner.message': 'A new NSI version ({{version}}) is available. Reload to get the latest fixes.',
|
||||
'updateBanner.reload': 'Reload',
|
||||
'updateBanner.docs': "What's new",
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { type ClassValue, clsx } from 'clsx'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
/**
|
||||
* Tailwind class merger — combines clsx (conditional classes) + tailwind-merge
|
||||
* (resolves conflicts: e.g. `px-2 px-4` → `px-4`).
|
||||
*
|
||||
* Standard shadcn/ui utility, used by every primitive variant.
|
||||
*/
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { routeTree } from './routeTree.gen'
|
||||
import { oidcAuthConfig } from '@/auth/oidcConfig'
|
||||
import { TokenSync } from '@/auth/TokenSync'
|
||||
import { RequireAuth } from '@/auth/RequireAuth'
|
||||
import { ThemeProvider } from '@/stores/ThemeProvider'
|
||||
import './i18n'
|
||||
import './styles.css'
|
||||
|
||||
@@ -29,6 +30,7 @@ createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<AuthProvider {...oidcAuthConfig}>
|
||||
<TokenSync />
|
||||
<ThemeProvider>
|
||||
<NStartUiProvider>
|
||||
<RequireAuth>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
@@ -38,6 +40,7 @@ createRoot(document.getElementById('root')!).render(
|
||||
</QueryClientProvider>
|
||||
</RequireAuth>
|
||||
</NStartUiProvider>
|
||||
</ThemeProvider>
|
||||
</AuthProvider>
|
||||
</StrictMode>,
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import { LanguageSwitch } from '@nstart/ui'
|
||||
import { AuthBadge } from '@/auth/AuthBadge'
|
||||
import { useCanMutate } from '@/auth/useCanMutate'
|
||||
import { ThemeSwitch } from '@/components/layout/ThemeSwitch'
|
||||
import { UpdateBanner } from '@/components/version/UpdateBanner'
|
||||
import { VersionBadge } from '@/components/version/VersionBadge'
|
||||
|
||||
@@ -46,6 +47,17 @@ function RootLayout() {
|
||||
>
|
||||
{t('nav.search')}
|
||||
</Link>
|
||||
{/* Документация — внешняя страница (Docusaurus → ordinis-docs
|
||||
deployment монтируется на /docs ingress'ом). target=_blank
|
||||
чтобы не терять контекст приложения. */}
|
||||
<a
|
||||
href="/docs/"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
className="text-carbon hover:text-ultramarain"
|
||||
>
|
||||
{t('nav.docs')}
|
||||
</a>
|
||||
{canMutate && (
|
||||
<>
|
||||
<Link
|
||||
@@ -88,6 +100,7 @@ function RootLayout() {
|
||||
</nav>
|
||||
<div className="ml-auto flex items-center gap-3">
|
||||
<VersionBadge />
|
||||
<ThemeSwitch />
|
||||
<LanguageSwitch
|
||||
value={i18n.language}
|
||||
options={LANG_OPTIONS}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import { createContext, useContext, useEffect, useState, type ReactNode } from 'react'
|
||||
|
||||
/**
|
||||
* Theme: 'light' (Earth) | 'dark' (Claude-warm) | 'system' (follow prefers-color-scheme).
|
||||
*
|
||||
* <p>Persist'ится в {@code localStorage[STORAGE_KEY]}. На mount читает saved
|
||||
* preference или fallback'ает к 'system'. Меняет {@code data-theme} attr на
|
||||
* {@code <html>} → CSS custom properties в styles.css переключают палитру.
|
||||
*
|
||||
* <p>{@code system} mode — если user ничего не выбрал, следим за prefers-color-scheme
|
||||
* media query. matchMedia listener обновляет attr реально-time при OS theme change.
|
||||
*/
|
||||
|
||||
const STORAGE_KEY = 'ord-theme'
|
||||
|
||||
export type ThemePreference = 'light' | 'dark' | 'system'
|
||||
export type ResolvedTheme = 'light' | 'dark'
|
||||
|
||||
type ThemeContextValue = {
|
||||
preference: ThemePreference
|
||||
resolved: ResolvedTheme
|
||||
setPreference: (next: ThemePreference) => void
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextValue | null>(null)
|
||||
|
||||
function readSystemPreference(): ResolvedTheme {
|
||||
if (typeof window === 'undefined') return 'light'
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
||||
}
|
||||
|
||||
function readSavedPreference(): ThemePreference {
|
||||
if (typeof window === 'undefined') return 'system'
|
||||
const raw = window.localStorage.getItem(STORAGE_KEY)
|
||||
if (raw === 'light' || raw === 'dark' || raw === 'system') return raw
|
||||
return 'system'
|
||||
}
|
||||
|
||||
function applyToDom(theme: ResolvedTheme) {
|
||||
if (typeof document === 'undefined') return
|
||||
document.documentElement.setAttribute('data-theme', theme)
|
||||
// shadcn convention — добавляем .dark class тоже на случай third-party
|
||||
// компонентов которые ждут именно его. Custom variant в styles.css всё равно
|
||||
// их связывает, но эта мера держит совместимость на корню.
|
||||
if (theme === 'dark') {
|
||||
document.documentElement.classList.add('dark')
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark')
|
||||
}
|
||||
// <meta name="color-scheme"> чтобы system browser UI (scrollbars, autofill)
|
||||
// был в гармонии с темой.
|
||||
let meta = document.querySelector<HTMLMetaElement>('meta[name="color-scheme"]')
|
||||
if (!meta) {
|
||||
meta = document.createElement('meta')
|
||||
meta.name = 'color-scheme'
|
||||
document.head.appendChild(meta)
|
||||
}
|
||||
meta.content = theme
|
||||
}
|
||||
|
||||
export function ThemeProvider({ children }: { children: ReactNode }) {
|
||||
const [preference, setPreferenceState] = useState<ThemePreference>(() => readSavedPreference())
|
||||
const [systemResolved, setSystemResolved] = useState<ResolvedTheme>(() => readSystemPreference())
|
||||
|
||||
// Слушаем OS theme change при mode='system'.
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') return
|
||||
const mq = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
const handler = () => setSystemResolved(mq.matches ? 'dark' : 'light')
|
||||
mq.addEventListener('change', handler)
|
||||
return () => mq.removeEventListener('change', handler)
|
||||
}, [])
|
||||
|
||||
const resolved: ResolvedTheme = preference === 'system' ? systemResolved : preference
|
||||
|
||||
// Применяем к DOM каждый раз когда resolved меняется.
|
||||
useEffect(() => {
|
||||
applyToDom(resolved)
|
||||
}, [resolved])
|
||||
|
||||
const setPreference = (next: ThemePreference) => {
|
||||
setPreferenceState(next)
|
||||
if (typeof window !== 'undefined') {
|
||||
window.localStorage.setItem(STORAGE_KEY, next)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ preference, resolved, setPreference }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useTheme() {
|
||||
const ctx = useContext(ThemeContext)
|
||||
if (!ctx) {
|
||||
throw new Error('useTheme must be used inside <ThemeProvider>')
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
@@ -1,12 +1,187 @@
|
||||
@import "tailwindcss";
|
||||
@import "@nstart/ui/styles/fonts.css";
|
||||
@import "@nstart/ui/styles/theme.css";
|
||||
@import 'tailwindcss';
|
||||
@import 'tw-animate-css';
|
||||
@import '@nstart/ui/styles/fonts.css';
|
||||
@import '@nstart/ui/styles/theme.css';
|
||||
|
||||
/* Без @source Tailwind v4 не сгенерирует CSS для классов внутри dist/@nstart/ui */
|
||||
@source "../node_modules/@nstart/ui/dist";
|
||||
@source '../node_modules/@nstart/ui/dist';
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
/* Google Fonts — Inter / JetBrains Mono / Tektur (handoff spec).
|
||||
* @nstart fonts.css дублируется по части семейств — overlap безвреден,
|
||||
* на финальной фазе миграции (когда удалим @nstart/ui) останется только это. */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&family=Tektur:wght@500;600;700&display=swap');
|
||||
|
||||
/* === Dark mode trigger ===
|
||||
* shadcn использует .dark class по умолчанию. Handoff просит [data-theme="dark"]
|
||||
* на <html>. Custom variant связывает оба пути — компоненты с `dark:` modifier
|
||||
* срабатывают при любом из условий. */
|
||||
@custom-variant dark (&:where([data-theme='dark'], [data-theme='dark'] *));
|
||||
|
||||
/* === DESIGN TOKENS — Light theme (Earth, default :root) === */
|
||||
:root {
|
||||
--color-ink: #1a1714;
|
||||
--color-ink-2: #52483d;
|
||||
--color-mute: #8c8276;
|
||||
--color-bg: #fbf8ee;
|
||||
--color-surface: #ffffff;
|
||||
--color-surface-2: #f4eedc;
|
||||
--color-line: #e3dccb;
|
||||
--color-line-2: #efe9d8;
|
||||
--color-navy: #3a2818;
|
||||
--color-accent: #b05a2e;
|
||||
--color-accent-bg: rgb(176 90 46 / 11%);
|
||||
--color-on-accent: #ffffff;
|
||||
--color-green: #4f7038;
|
||||
--color-green-bg: #e4ead0;
|
||||
--color-warn: #a8762a;
|
||||
--color-warn-bg: #f6e8bc;
|
||||
--color-pink: #a64636;
|
||||
--color-pink-bg: #f4d9cd;
|
||||
|
||||
/* shadcn semantic tokens — re-mapped onto наши значения */
|
||||
--background: var(--color-bg);
|
||||
--foreground: var(--color-ink);
|
||||
--card: var(--color-surface);
|
||||
--card-foreground: var(--color-ink);
|
||||
--popover: var(--color-surface);
|
||||
--popover-foreground: var(--color-ink);
|
||||
--primary: var(--color-navy);
|
||||
--primary-foreground: var(--color-on-accent);
|
||||
--secondary: var(--color-surface-2);
|
||||
--secondary-foreground: var(--color-ink);
|
||||
--muted: var(--color-surface-2);
|
||||
--muted-foreground: var(--color-mute);
|
||||
--accent: var(--color-accent);
|
||||
--accent-foreground: var(--color-on-accent);
|
||||
--destructive: var(--color-pink);
|
||||
--destructive-foreground: var(--color-on-accent);
|
||||
--border: var(--color-line);
|
||||
--input: var(--color-line);
|
||||
--ring: var(--color-accent);
|
||||
}
|
||||
|
||||
[data-theme='dark'] {
|
||||
--color-ink: #f0ece4;
|
||||
--color-ink-2: #c8c1b3;
|
||||
--color-mute: #8a8474;
|
||||
--color-bg: #1c1a17;
|
||||
--color-surface: #262421;
|
||||
--color-surface-2: #2e2b27;
|
||||
--color-line: #3a3733;
|
||||
--color-line-2: #2d2a27;
|
||||
--color-navy: #d97757;
|
||||
--color-accent: #d97757;
|
||||
--color-accent-bg: rgb(217 119 87 / 16%);
|
||||
--color-on-accent: #1c1a17;
|
||||
--color-green: #6fa97d;
|
||||
--color-green-bg: rgb(111 169 125 / 18%);
|
||||
--color-warn: #d4a653;
|
||||
--color-warn-bg: rgb(212 166 83 / 18%);
|
||||
--color-pink: #d18272;
|
||||
--color-pink-bg: rgb(209 130 114 / 20%);
|
||||
|
||||
--background: var(--color-bg);
|
||||
--foreground: var(--color-ink);
|
||||
--card: var(--color-surface);
|
||||
--card-foreground: var(--color-ink);
|
||||
--popover: var(--color-surface);
|
||||
--popover-foreground: var(--color-ink);
|
||||
--primary: var(--color-navy);
|
||||
--primary-foreground: var(--color-on-accent);
|
||||
--secondary: var(--color-surface-2);
|
||||
--secondary-foreground: var(--color-ink);
|
||||
--muted: var(--color-surface-2);
|
||||
--muted-foreground: var(--color-mute);
|
||||
--accent: var(--color-accent);
|
||||
--accent-foreground: var(--color-on-accent);
|
||||
--destructive: var(--color-pink);
|
||||
--destructive-foreground: var(--color-on-accent);
|
||||
--border: var(--color-line);
|
||||
--input: var(--color-line);
|
||||
--ring: var(--color-accent);
|
||||
}
|
||||
|
||||
/* Tailwind v4 @theme inline — экспонирует CSS vars как utility classes
|
||||
* (`bg-bg`, `text-ink`, `border-line`, `bg-accent` и т.д.). */
|
||||
@theme inline {
|
||||
--color-bg: var(--color-bg);
|
||||
--color-surface: var(--color-surface);
|
||||
--color-surface-2: var(--color-surface-2);
|
||||
--color-ink: var(--color-ink);
|
||||
--color-ink-2: var(--color-ink-2);
|
||||
--color-mute: var(--color-mute);
|
||||
--color-line: var(--color-line);
|
||||
--color-line-2: var(--color-line-2);
|
||||
--color-navy: var(--color-navy);
|
||||
--color-accent: var(--color-accent);
|
||||
--color-accent-bg: var(--color-accent-bg);
|
||||
--color-on-accent: var(--color-on-accent);
|
||||
--color-green: var(--color-green);
|
||||
--color-green-bg: var(--color-green-bg);
|
||||
--color-warn: var(--color-warn);
|
||||
--color-warn-bg: var(--color-warn-bg);
|
||||
--color-pink: var(--color-pink);
|
||||
--color-pink-bg: var(--color-pink-bg);
|
||||
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
|
||||
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
--font-mono: 'JetBrains Mono', 'SF Mono', monospace;
|
||||
--font-display: 'Tektur', 'Inter', sans-serif;
|
||||
|
||||
--radius-sm: 4px;
|
||||
--radius-md: 6px;
|
||||
--radius-lg: 8px;
|
||||
}
|
||||
|
||||
html, body, #root {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--color-bg);
|
||||
color: var(--color-ink);
|
||||
font-family: var(--font-sans);
|
||||
font-feature-settings: 'ss01', 'cv11', 'cv02', 'cv03', 'cv04';
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
/* Tektur uppercase caption helper — handoff .cap class */
|
||||
.cap {
|
||||
font-family: var(--font-display);
|
||||
font-size: 10.5px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.10em;
|
||||
color: var(--color-mute);
|
||||
}
|
||||
|
||||
/* === Legacy fallback: старые компоненты используют nstart token names
|
||||
* (--color-carbon, --color-ultramarain etc). Маппим их на новые pending
|
||||
* полная миграция. === */
|
||||
:root,
|
||||
[data-theme='dark'] {
|
||||
--color-carbon: var(--color-ink);
|
||||
--color-ultramarain: var(--color-accent);
|
||||
--color-orbit: var(--color-warn);
|
||||
--color-regolith: var(--color-line);
|
||||
--color-asteroid: var(--color-mute);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user