Skip to content

Latest commit

 

History

History
695 lines (630 loc) · 31.8 KB

File metadata and controls

695 lines (630 loc) · 31.8 KB
name SolidStats
description Dark-only "gunmetal" tactical-operations design system for the SolidStats web frontend (TanStack Start + React). Single source of truth for design tokens; the Tailwind v4 @theme is generated from this file, never hand-maintained.
colors
bg-0 bg-1 surface-1 surface-2 surface-3 overlay border-1 border-2 text-primary text-muted text-subtle fg-on-accent primary primary-hover primary-active primary-weak primary-border win win-weak win-border loss loss-weak loss-border warn warn-weak warn-border info info-weak info-border chart-1 chart-2 chart-3 chart-4 chart-5 grid-line
#0A0D13
#0F131C
#151A25
#1B212F
#232B3B
rgba(6, 9, 14, 0.66)
#262E3D
#36415A
#EAEEF6
#98A2B6
#616B80
#04141A
#36C5E0
#54D3EC
#27A8C2
rgba(54, 197, 224, 0.13)
rgba(54, 197, 224, 0.40)
#3FCF8E
rgba(63, 207, 142, 0.14)
rgba(63, 207, 142, 0.38)
#FF5C6C
rgba(255, 92, 108, 0.14)
rgba(255, 92, 108, 0.38)
#F2B33D
rgba(242, 179, 61, 0.15)
rgba(242, 179, 61, 0.40)
#5B9DFF
rgba(91, 157, 255, 0.14)
rgba(91, 157, 255, 0.38)
#36C5E0
#3FCF8E
#F2B33D
#B58BFF
#FF8A5B
rgba(255, 255, 255, 0.06)
typography
fontFamilies fontWeights lineHeights letterSpacing scale roles
display body mono
'Saira', system-ui, sans-serif
'IBM Plex Sans', system-ui, sans-serif
'IBM Plex Mono', ui-monospace, monospace
regular medium semibold bold
400
500
600
700
tight snug dense normal
1.08
1.25
1.35
1.5
tight snug normal label caps
-0.02em
-0.01em
0
0.06em
0.12em
2xs xs sm base md lg xl 2xl 3xl 4xl 5xl
size lineHeight
11px
1.5
size lineHeight
12px
1.5
size lineHeight
13px
1.35
size lineHeight
14px
1.5
size lineHeight
16px
1.5
size lineHeight
18px
1.25
size lineHeight
22px
1.25
size lineHeight
28px
1.25
size lineHeight
36px
1.08
size lineHeight
48px
1.08
size lineHeight
64px
1.08
overline label h1 h2 h3 h4 body body-sm caption stat stat-xl mono
fontFamily fontSize fontWeight letterSpacing textTransform color
{typography.fontFamilies.mono}
{typography.scale.2xs}
{typography.fontWeights.medium}
{typography.letterSpacing.caps}
uppercase
{colors.text-muted}
fontFamily fontSize fontWeight letterSpacing textTransform color
{typography.fontFamilies.body}
{typography.scale.xs}
{typography.fontWeights.semibold}
{typography.letterSpacing.label}
uppercase
{colors.text-muted}
fontFamily fontSize fontWeight lineHeight letterSpacing color
{typography.fontFamilies.display}
{typography.scale.3xl}
{typography.fontWeights.bold}
{typography.lineHeights.tight}
{typography.letterSpacing.tight}
{colors.text-primary}
fontFamily fontSize fontWeight lineHeight letterSpacing color
{typography.fontFamilies.display}
{typography.scale.2xl}
{typography.fontWeights.semibold}
{typography.lineHeights.snug}
{typography.letterSpacing.snug}
{colors.text-primary}
fontFamily fontSize fontWeight lineHeight letterSpacing color
{typography.fontFamilies.display}
{typography.scale.xl}
{typography.fontWeights.semibold}
{typography.lineHeights.snug}
{typography.letterSpacing.snug}
{colors.text-primary}
fontFamily fontSize fontWeight lineHeight color
{typography.fontFamilies.body}
{typography.scale.lg}
{typography.fontWeights.semibold}
{typography.lineHeights.snug}
{colors.text-primary}
fontFamily fontSize fontWeight lineHeight color
{typography.fontFamilies.body}
{typography.scale.base}
{typography.fontWeights.regular}
{typography.lineHeights.normal}
{colors.text-primary}
fontFamily fontSize lineHeight color
{typography.fontFamilies.body}
{typography.scale.sm}
{typography.lineHeights.dense}
{colors.text-muted}
fontFamily fontSize lineHeight color
{typography.fontFamilies.body}
{typography.scale.xs}
{typography.lineHeights.normal}
{colors.text-subtle}
fontFamily fontVariantNumeric fontWeight color
{typography.fontFamilies.mono}
tabular-nums
{typography.fontWeights.medium}
{colors.text-primary}
fontFamily fontVariantNumeric fontSize fontWeight lineHeight letterSpacing color
{typography.fontFamilies.display}
tabular-nums
{typography.scale.4xl}
{typography.fontWeights.bold}
{typography.lineHeights.tight}
{typography.letterSpacing.tight}
{colors.text-primary}
fontFamily fontVariantNumeric fontSize color
{typography.fontFamilies.mono}
tabular-nums
{typography.scale.sm}
{colors.text-muted}
spacing
rounded
xs sm md lg xl full
2px
4px
6px
8px
12px
999px
elevation
sm md lg ring ring-glow
0 1px 2px rgba(0, 0, 0, 0.35)
0 6px 18px rgba(0, 0, 0, 0.45)
0 18px 48px rgba(0, 0, 0, 0.55)
0 0 0 2px {colors.bg-0}, 0 0 0 4px {colors.primary}
0 0 0 1px {colors.primary-border}, 0 0 12px rgba(54, 197, 224, 0.25)
motion
duration easing
fast base slow
120ms
170ms
260ms
out in-out
cubic-bezier(0.2, 0.7, 0.3, 1)
cubic-bezier(0.5, 0, 0.3, 1)
layout
container container-prose nav-h tabbar-h border-width breakpoints
1760px
720px
56px
60px
1px
md lg xl 2xl 3xl 4xl
768px
1024px
1280px
1536px
120rem
160rem
components
button-primary button-secondary button-ghost badge-outcome-win badge-outcome-loss badge-status-pending badge-status-approved badge-status-rejected badge-freshness card card-prose table-header table-row table-row-zebra table-cell-numeric stat-tile provenance-line badge-known badge-unknown badge-conflict inline-review-row input dialog popover
backgroundColor textColor rounded hover active focusVisible disabled
{colors.primary}
{colors.fg-on-accent}
{rounded.sm}
backgroundColor
{colors.primary-hover}
backgroundColor transform
{colors.primary-active}
translateY(1px)
boxShadow
{elevation.ring}
backgroundColor textColor opacity
{colors.surface-2}
{colors.text-subtle}
0.6
backgroundColor textColor border rounded hover active focusVisible disabled
{colors.surface-1}
{colors.text-primary}
1px solid {colors.border-1}
{rounded.sm}
backgroundColor border
{colors.surface-3}
1px solid {colors.border-2}
backgroundColor transform
{colors.surface-2}
translateY(1px)
boxShadow
{elevation.ring}
textColor opacity
{colors.text-subtle}
0.6
backgroundColor textColor rounded hover active focusVisible disabled
transparent
{colors.text-muted}
{rounded.sm}
backgroundColor textColor
{colors.surface-1}
{colors.text-primary}
backgroundColor
{colors.surface-2}
boxShadow
{elevation.ring}
textColor opacity
{colors.text-subtle}
0.6
backgroundColor textColor border rounded icon
{colors.win-weak}
{colors.win}
1px solid {colors.win-border}
{rounded.xs}
trending-up
backgroundColor textColor border rounded icon
{colors.loss-weak}
{colors.loss}
1px solid {colors.loss-border}
{rounded.xs}
trending-down
backgroundColor textColor border rounded icon
{colors.info-weak}
{colors.info}
1px solid {colors.info-border}
{rounded.xs}
clock
backgroundColor textColor border rounded icon
{colors.win-weak}
{colors.win}
1px solid {colors.win-border}
{rounded.xs}
badge-check
backgroundColor textColor border rounded icon
{colors.loss-weak}
{colors.loss}
1px solid {colors.loss-border}
{rounded.xs}
x-circle
rounded states
{rounded.full}
up-to-date stale offline reconnecting
backgroundColor textColor border icon
{colors.win-weak}
{colors.win}
1px solid {colors.win-border}
circle
backgroundColor textColor border icon
{colors.warn-weak}
{colors.warn}
1px solid {colors.warn-border}
circle-dot
backgroundColor textColor border icon
{colors.loss-weak}
{colors.loss}
1px solid {colors.loss-border}
wifi-off
backgroundColor textColor border icon
{colors.info-weak}
{colors.info}
1px solid {colors.info-border}
refresh-cw
backgroundColor border rounded hover
{colors.surface-1}
1px solid {colors.border-1}
{rounded.lg}
border
1px solid {colors.border-2}
backgroundColor border rounded
{colors.surface-1}
1px solid {colors.border-1}
{rounded.lg}
backgroundColor textColor textTransform borderBottom position
{colors.surface-2}
{colors.text-muted}
uppercase
1px solid {colors.border-1}
sticky
backgroundColor textColor borderBottom hover selected
{colors.surface-1}
{colors.text-primary}
1px solid {colors.border-1}
backgroundColor
{colors.surface-3}
backgroundColor boxShadow
{colors.primary-weak}
inset 2px 0 0 {colors.primary}
backgroundColor
{colors.bg-1}
fontVariantNumeric textAlign
tabular-nums
right
backgroundColor border rounded labelColor valueColor valueFontVariantNumeric deltaPositiveColor deltaNegativeColor
{colors.surface-1}
1px solid {colors.border-1}
{rounded.md}
{colors.text-muted}
{colors.text-primary}
tabular-nums
{colors.win}
{colors.loss}
textColor linkColor separator
{colors.text-muted}
{colors.primary}
·
backgroundColor textColor border rounded icon
{colors.surface-2}
{colors.text-muted}
1px solid {colors.border-1}
{rounded.xs}
circle-check
backgroundColor textColor border rounded icon
{colors.warn-weak}
{colors.warn}
1px solid {colors.warn-border}
{rounded.xs}
circle-help
backgroundColor textColor border rounded icon
{colors.warn-weak}
{colors.warn}
1px solid {colors.warn-border}
{rounded.xs}
triangle-alert
textColor backgroundColor icon linkColor
{colors.warn}
transparent
triangle-alert
{colors.primary}
backgroundColor textColor placeholderColor border rounded hover focusVisible disabled
{colors.surface-2}
{colors.text-primary}
{colors.text-subtle}
1px solid {colors.border-1}
{rounded.sm}
border
1px solid {colors.border-2}
border boxShadow
1px solid {colors.primary-border}
{elevation.ring-glow}
backgroundColor textColor opacity
{colors.surface-1}
{colors.text-subtle}
0.6
backgroundColor border rounded boxShadow scrim
{colors.surface-1}
1px solid {colors.border-2}
{rounded.xl}
0 18px 48px rgba(0, 0, 0, 0.55)
{colors.overlay}
backgroundColor border rounded boxShadow
{colors.surface-1}
1px solid {colors.border-2}
{rounded.lg}
0 6px 18px rgba(0, 0, 0, 0.45)

SolidStats Design System

Overview

SolidStats is the public statistics and moderation site for the SolidGames tactical-milsim community. The UI is a "tactical operations terminal": a gunmetal-ink command surface where data is the hero. It is dense, dark-only, mobile-first, and bilingual (RU + EN). It must feel instant, stable, and trustworthy before it feels decorative — the product reports, it does not sell.

This file is the single source of truth for design tokens. The production Tailwind v4 @theme, the W3C DTCG export, and any other token artifact are generated from it — never hand-maintained in parallel. Run the export as an explicit build step on every token change:

node scripts/gen-theme.mjs              # generates src/styles/theme.css (interim generator — a dumb copy of this file)
npx @google/design.md lint DESIGN.md    # structure + broken {token} refs + WCAG contrast (blocker)
npx @google/design.md diff DESIGN.md    # token drift between revisions

@google/design.md is used for lint/diff only; the official export --format css-tailwind is the future migration target, but it currently drops typography line-height, so theme.css is generated by scripts/gen-theme.mjs until that exporter is fixed.

Add --*: initial to the emitted @theme so Tailwind's stock palette is removed and only the SolidStats dark palette exists.

Locked direction (do not regenerate):

  • Dark-only layered gunmetal surfaces — no light theme.
  • One cyan signal accent, used sparingly so it stays meaningful.
  • Four semantics (win / loss / unknown·conflict / info), each with -weak + -border, never color-alone (always icon and/or label).
  • Saira display + IBM Plex Sans body + IBM Plex Mono tabular numerals (all carry Cyrillic).
  • Sharp technical radii (2–12px), hairline borders as the primary separator, restrained shadows for floating UI only, fast functional motion.

Colors

Ink ramp (surfaces)

Neutrals are blue-tinted gunmetal, not pure gray. Surfaces step up in lightness as they come forward — depth reads from the surface step + a hairline border, not from shadow.

Token Hex Role
bg-0 #0A0D13 app backdrop, deepest
bg-1 #0F131C raised app background / sticky bars / zebra rows
surface-1 #151A25 cards, panels (default content surface)
surface-2 #1B212F table header, inputs, raised-in-card
surface-3 #232B3B hover / active row
overlay rgba(6,9,14,.66) dialog scrim

Borders

Hairline border-1 is the primary structural separator — this is a table/ops product, so structure comes from 1px lines + surface steps, not heavy shadow. border-2 frames focus-within and emphasized panels.

Token Hex Role
border-1 #262E3D hairline separators
border-2 #36415A stronger frame / focus-within

Text

Token Hex Role Contrast
text-primary #EAEEF6 primary copy, stat readouts 13.8–16.7:1 — AA everywhere
text-muted #98A2B6 secondary / labels / column headers 6.3–7.6:1 — AA everywhere
text-subtle #616B80 tertiary / placeholder / disabled 3.25–3.63:1 — AA-large/UI/disabled only
fg-on-accent #04141A text on any saturated fill 9.1–9.5:1 on cyan / win — AA

text-subtle is NOT for body text. At 3.25:1 on surface-1 it passes WCAG 2.2 for large text (≥18.66px / ≥14px bold), UI-component boundaries, and the disabled state (exempt from contrast), but fails the 4.5:1 normal-text rule. Use text-muted for any meaningful sentence; reserve text-subtle for placeholders, disabled controls, and decorative captions only.

Primary — signal cyan

The single accent. It means interactive / active / brand: links, active nav, primary buttons, focus rings, selected rows, sparkline strokes. Used sparingly so it stays meaningful. #36C5E0 clears AA on every surface (7.8–9.5:1).

Token Value Role
primary #36C5E0 default interactive
primary-hover #54D3EC hover
primary-active #27A8C2 press
primary-weak rgba(54,197,224,.13) selected-row / badge tint
primary-border rgba(54,197,224,.40) tinted frame / input focus

Semantics

Green / red / amber / blue carry the palette range so the UI is never one-note. Never color-alone — every semantic is paired with a Lucide icon and/or a text label. Each token clears AA on dark (loss is the floor at 5.8:1 on surface-1).

Token Hex Meaning -weak -border
win #3FCF8E win / positive delta / approved rgba(63,207,142,.14) rgba(63,207,142,.38)
loss #FF5C6C loss / teamkill / danger / rejected rgba(255,92,108,.14) rgba(255,92,108,.38)
warn #F2B33D unknown / conflict / warning / stale rgba(242,179,61,.15) rgba(242,179,61,.40)
info #5B9DFF info / pending / neutral notice rgba(91,157,255,.14) rgba(91,157,255,.38)

Data-viz ramp (chart-1…5 + grid-line) is for sparklines and microcharts; chart-1/chart-2 reuse cyan/green so charts stay on-brand. Use grid-line (rgba(255,255,255,.06)) for axes, never a hard border.

Typography

Three families, all carrying full Cyrillic for the RU/EN interface:

  • Display — Saira (aerospace/HUD grotesk): headings and big stat readouts, 600/700, tight tracking.
  • Body — IBM Plex Sans: engineered, highly legible UI text.
  • Mono — IBM Plex Mono with tabular figures: all stats, ranks, IDs, slugs, timers, checksums — anywhere numbers must align.

The scale is px-based and dense (14px is the UI default, not 16px), ranging 2xs 11px → 5xl 64px. Weights 400/500/600/700; line-heights 1.08 (display) / 1.25 (headings) / 1.35 (dense tables) / 1.5 (body). Uppercase labels get letterSpacing.label (0.06em); brand/overline get letterSpacing.caps (0.12em).

Semantic roles (typography.roles.*) are the named recipes to apply — overline, label, h1h4, body, body-sm, caption, stat, stat-xl, mono. Headings (h1h3) are Saira; h4, body, and captions are IBM Plex Sans; stat/stat-xl/mono are tabular. Numbers are right-aligned in tables, signed for deltas (+12, −3), with explicit units.

Layout

  • Spacing uses Tailwind's stock 4px scale — no custom keys. p-0.5=2px, p-1=4px, p-2=8px, p-4=16px, p-6=24px, p-8=32px. Dense defaults: 8/12/16 dominate; 24/32 separate major regions.
  • Two containers. container (data/page) = 1760px ceiling, fluid below then centers — tables, leaderboards, stat grids, profiles spend the width. container-prose = 720px — reading content (request flows, moderation comments, about/help), capped for line length. This replaces the seed's old 1240px cap.
  • Reflow is container-driven (@container / the --container-* scale), not viewport — the device frames make viewport media queries lie. Set container-type: inline-size on the content container.
  • Breakpoints (canonical set; defined once in references/design-system.md, mirrored under layout.breakpoints): design and review at 360 · 768 · 1024 · 1280 · 1920 · 2560, with 390/414 mobile spot-checks and a 3440 ultrawide cap-check. Tailwind's md/lg/xl/2xl are kept; add --breakpoint-3xl: 120rem (1920) and --breakpoint-4xl: 160rem (2560). Large screens are first-class: ~54% of the PC-gamer audience sits at 1920 and ~40% are wider, so data surfaces use the width; ultrawide caps at 1760 + centers (turn extra width into rows/columns, not gutter).
  • Chrome: desktop top nav (nav-h 56px); mobile bottom tab bar (tabbar-h 60px). Sticky table headers and filter toolbars. Reserve space for all async content — CLS budget ≤ 0.02.

Elevation & Depth

Depth is communicated by the surface step + hairline border first, shadow second.

  • Cards on dark use border-1 + a surface step — no drop shadow.
  • Shadows are reserved for things that truly float: menus, popovers (elevation.md), dialogs and toasts (elevation.lg). elevation.sm is a barely-there lift for sticky bars.
  • Focus is always visible: elevation.ring (2px offset ring in cyan) on every interactive control; elevation.ring-glow for inputs.
  • Transparency is purposeful only: -weak tints (13–15% alpha) for badge/row fills, the overlay scrim behind dialogs, and an optional subtle backdrop-blur on the sticky top nav / bottom tab bar. Never blur content regions.

Shapes

Sharp, technical radii — rounded.xs 2px (chips) · sm 4px (inputs, buttons) · md 6px (small cards) · lg 8px (cards, panels) · xl 12px (dialogs, sheets). rounded.full (avatars, toggle pills) is the only pill-soft shape. Borders are 1px. Icons are Lucide outline, 2px stroke, currentColor, sized 16 (dense/inline) / 18–20 (buttons, nav) / 24 (section headers, empty states).

Components

All recipes live under components.* and reference base tokens via {colors.*} / {rounded.*} / {typography.*} / {elevation.*}.

  • Buttonsbutton-primary (cyan fill, fg-on-accent text), button-secondary (surface + hairline), button-ghost (transparent). All carry hover / active (press = translateY(1px)) / focus-visible (ring) / disabled (text-subtle + 0.6 opacity).
  • Badgesbadge-outcome-* (win/loss with trending icon + W/L), badge-status-* (pending/approved/rejected), and badge-freshness with the four-state vocabulary Актуально / Данные устаревают / Связь потеряна / Переподключение. Every badge pairs color with a Lucide icon — never color-alone.
  • Cardcard (surface-1 + border-1, hover brightens to border-2 for interactive cards); card-prose caps at the 720px reading width.
  • Table — sticky uppercase table-header on surface-2; table-row with surface-3 hover and a selected state that combines a primary-weak fill with an inset left-edge cyan marker (not fill-only); optional table-row-zebra on bg-1; numeric cells are right-aligned tabular mono.
  • Stat tile — display-font value (tabular), text-muted label, signed delta colored win/loss and paired with a trending icon.
  • Data-trust components — the trust layer is systemic, always-present, not a transient badge:
    • provenance-line: посчитано из N реплеев · <freshness> · Как считается, always under headline stats, with a cyan "how it's computed" link.
    • badge-known / badge-unknown / badge-conflict: the Known / Unknown / Conflict data-trust states. Unknown is the literal amber word with a circle-help icon — never 0 or alone. Conflict is amber + triangle-alert.
    • inline-review-row: a pending SteamID merge is a workflow footnote — a quiet inline amber row (на проверке + request link) inside the SteamID list it describes, never a filled banner pinned to the bottom of a stretched column.
  • Inputssurface-2 fill, hairline border that brightens on hover and goes primary-border + ring-glow on focus.
  • Floating UIdialog (rounded.xl + elevation.lg + overlay scrim), popover (rounded.lg + elevation.md).

Do's and Don'ts

Do

  • Build depth from surface steps + hairline borders; reserve shadow for floating UI.
  • Keep cyan rare — links, active state, primary action, focus, selected. If everything is cyan, nothing is.
  • Pair every semantic color with a Lucide icon and/or label. Color is never the sole signal.
  • Use tabular mono for every number that aligns (stats, ranks, IDs, timers); right-align numeric columns.
  • Make data-trust first-class: provenance line, freshness pill, honest Known/Unknown/Conflict states.
  • Spend large-screen width on data (rows/columns), keep reading content at the 720px prose width.
  • Animate transform/opacity only, honor prefers-reduced-motion, hold the CLS ≤ 0.02 budget.
  • i18n-key every string (RU + EN) and sanity-check the Russian for clipped or awkward wording.

Don't

  • Don't add a light theme, decorative gradients, blobs, nested cards, or emoji-as-icons.
  • Don't use text-subtle for body text (3.25:1 — fails AA for normal text).
  • Don't render Unknown as 0 or ; don't dress a short-lived workflow event up as "the trust layer."
  • Don't let a data table stretch past the 1760px ceiling on ultrawide — center it, or go master-detail.
  • Don't introduce arbitrary Tailwind values or invent a custom spacing scale — stay on the stock 4px grid.
  • Don't reach for shadow to separate cards, or animate layout properties (width/height/top/left/margin).
  • Don't re-hardcode breakpoints or container widths elsewhere — they live in references/design-system.md.