ev_lib_classes 0.5.0

Pure styling data (Tailwind class tables) shared by the Dioxus uikit and the TS codegen — no Dioxus.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
/* AUTO-GENERATED by `nix run .#gen` from the repo-root tokens-legacy.css — do not edit. */
/*
 * The pre-0.10 token vocabulary, layered over the current one.
 *
 * Import this instead of `tokens.css` while a consumer still writes
 * `text-main-mist`, `bg-sidebar`, `text-muted-foreground` and friends in its own
 * markup. It re-declares every old utility name and re-points the current tokens
 * at the old values, so a kit component styled `bg-card text-ink` renders exactly
 * what `bg-card text-card-foreground` rendered.
 *
 * Paired with the `legacy` Cargo feature and the `./styles/tokens-legacy.css`
 * export of `@evinvest/uikit`. Both go away together once every consumer has
 * moved; see docs/spec/accents.md for the model that replaces this.
 */

/*
 * Shared design tokens — the theme contract for the `ev_lib::uikit` (Rust) and
 * `@evinvest/uikit` (TypeScript) components. Import this file from any Tailwind v4
 * entrypoint. Single source for both ports — `@evinvest/uikit` copies it into
 * `styles/tokens.css` from its `prepare` script.
 *
 * The values below are EV's. A non-EV consumer writes its own sheet against the
 * same names; the class tables reference nothing else.
 *
 *   SCOPE      :root · .dark · .light      — a consumer defines one or both
 *   SURFACES   background · card · popover · muted · hover
 *   INK        ink · ink-mid · ink-soft
 *   LINES      border · input · ring
 *   ROLES      {role} and on-{role} for the ones that get filled:
 *              brand · primary · secondary · positive ·
 *              accent-trace · accent-debug · accent-info · accent-warn · accent-error
 *   SCALARS    radius · control-* · display-scale · page-* · shadows · fonts
 *   CHARTS     chart-1 … chart-5
 *
 * Custom properties inherit, so a scope class on a `<section>` re-themes its
 * subtree: `bg-card text-ink` is correct on both polarities with no prop. EV is
 * single-polarity, so `:root` and `.dark` carry the same block and `.light` is
 * undefined — a `Section(Light)` is then a no-op here, which is what a
 * single-polarity palette should do.
 *
 * Accents are ordered by significance, not by meaning; see docs/spec/accents.md.
 * Their values are read outside CSS (gitops mirrors them into Discord embed
 * bars), so they stay plain hex — no color-mix.
 *
 * Surface ladder — dark UI reads as unfinished when every plane is the same
 * value, so the steps below are deliberate and measured; keep them apart if you
 * retune. background #070d18 → secondary #0d1526 (1.07:1) → card #16233c (1.24:1
 * vs background) → popover #1d2c49 (1.13:1 vs card) → muted #26375c. Elevation
 * rides the same ladder via --shadow-card / -elevated / -overlay.
 */

/*
 * Toast (sonner) enter/exit motion, shared by both ports. Imported by the token
 * sheets so the lifecycle ships with the theme contract.
 */

/* ── Toast stacking (Sonner-style), shared by both ports via data-stack ──────
   Both `ev_lib::uikit` (Dioxus) and `@evinvest/uikit` (React) set data-stack on the
   toaster and drive the same machine: toasts are absolutely stacked at the
   pinned edge, pile up collapsed (front three peeking, each lifted + scaled by
   --index and clamped to --front-height), and spread to --offset on hover /
   keyboard focus (pure CSS — no JS state). Vars: the toaster sets --front-height
   + --gap; each toast sets --index, --z-index, --offset, --initial-height. React
   measures real heights; Dioxus can't (host-only), so it feeds a constant height
   — collapse is exact, expansion is uniform. The enter is a keyframe (plays on
   DOM insertion, so it works in both ports without a JS mount flip),
   `data-state=closed` slides out, and the node is dropped on the transitionend. */

[data-slot="toaster"][data-stack] [data-slot="toast"] {
  position: absolute;
  --y: translateY(0);
  opacity: 1;
  z-index: var(--z-index, 1);
  transform: var(--y) translateX(var(--swipe-x, 0px));
  /* a plain easeOut (no overshoot) so a restack settles instead of bouncing
     back past its slot */
  transition:
    transform 350ms cubic-bezier(0.21, 0.8, 0.4, 1),
    opacity 350ms,
    height 350ms;
  /* the enter is a keyframe (plays on DOM insertion in both ports — no JS mount
     flip, whose pre-paint timing the Dioxus effect couldn't drive). It animates
     the independent `translate` property, NOT `transform`, so it *composes* with
     the stack transform instead of overriding it: a toast pushed back mid-enter
     keeps repositioning smoothly instead of fighting the keyframe (rapid-fire
     lag). */
  animation: ev-toast-enter 350ms cubic-bezier(0.21, 0.8, 0.4, 1) backwards;
}
@keyframes ev-toast-enter {
  from {
    opacity: 0;
    translate: 0 var(--enter-y, 100%);
  }
  to {
    opacity: 1;
    translate: 0 0;
  }
}

/* pin to the toaster's edge; set the lift sign + the off-screen enter offset */
[data-slot="toaster"][data-stack][data-y-position="bottom"] [data-slot="toast"] {
  bottom: 0;
  --lift: -1;
  --enter-y: 100%;
}
[data-slot="toaster"][data-stack][data-y-position="top"] [data-slot="toast"] {
  top: 0;
  --lift: 1;
  --enter-y: -100%;
}

/* closing: slide back off + fade out; the node is dropped on its transitionend */
[data-slot="toaster"][data-stack] [data-slot="toast"][data-state="closed"] {
  --y: translateY(var(--enter-y));
  opacity: 0;
}

/* collapsed: back toasts lift + shrink by depth, clamp to the front height */
[data-slot="toaster"][data-stack]
  [data-slot="toast"][data-state="open"][data-front="false"] {
  --y: translateY(calc(var(--lift) * var(--gap) * var(--index)))
    scale(calc(1 - var(--index) * 0.05));
  height: var(--front-height);
}
[data-slot="toaster"][data-stack] [data-slot="toast"][data-front="false"] > * {
  opacity: 0;
  transition: opacity 300ms;
}
/* only the front three show while collapsed */
[data-slot="toaster"][data-stack] [data-slot="toast"][data-visible="false"] {
  opacity: 0;
  pointer-events: none;
}

/* expanded (hover / keyboard focus): spread to the offset, real height, content
   + overflow back */
[data-slot="toaster"][data-stack]:hover [data-slot="toast"][data-state="open"],
[data-slot="toaster"][data-stack]:focus-within
  [data-slot="toast"][data-state="open"] {
  --y: translateY(calc(var(--lift) * var(--offset)));
  height: var(--initial-height);
}
[data-slot="toaster"][data-stack]:hover [data-slot="toast"] > *,
[data-slot="toaster"][data-stack]:focus-within [data-slot="toast"] > *,
[data-slot="toaster"][data-stack]:hover [data-slot="toast"][data-visible="false"],
[data-slot="toaster"][data-stack]:focus-within
  [data-slot="toast"][data-visible="false"] {
  opacity: 1;
  pointer-events: auto;
}

/* a transparent strip over the gap so the hover area stays contiguous between
   spread toasts (otherwise the pointer falls through the gap and it collapses) */
[data-slot="toaster"][data-stack] [data-slot="toast"]::after {
  content: "";
  position: absolute;
  left: 0;
  width: 100%;
  height: calc(var(--gap) + 1px);
}
[data-slot="toaster"][data-stack][data-y-position="bottom"]
  [data-slot="toast"]::after {
  bottom: 100%;
}
[data-slot="toaster"][data-stack][data-y-position="top"]
  [data-slot="toast"]::after {
  top: 100%;
}

/* 1:1 finger tracking during a swipe — kill the transition */
[data-slot="toaster"][data-stack] [data-slot="toast"][data-swiping="true"] {
  transition: none;
}

/* auto-dismiss without a host timer: a no-op "life" animation runs for --life,
   then its `animationend` fires (the toast closes itself). Hovering/focusing the
   stack pauses just that animation (animation-play-state's 2nd slot), so the
   countdown holds — and resumes with the elapsed time preserved — while the
   enter (1st slot) keeps running. Persistent toasts set data-autodismiss=false,
   so they get no life animation. (The React port uses setTimeout instead.) */
@keyframes ev-toast-life {
  from {
    --ev-life: 0;
  }
  to {
    --ev-life: 1;
  }
}
[data-slot="toaster"][data-stack]
  [data-slot="toast"][data-autodismiss="true"][data-state="open"] {
  animation:
    ev-toast-enter 350ms cubic-bezier(0.21, 0.8, 0.4, 1) backwards,
    ev-toast-life var(--life) linear;
}
[data-slot="toaster"][data-stack]:hover
  [data-slot="toast"][data-autodismiss="true"],
[data-slot="toaster"][data-stack]:focus-within
  [data-slot="toast"][data-autodismiss="true"] {
  animation-play-state: running, paused;
}

/* reduced motion: opacity-only enter + near-instant transform (still fires
   transitionend, so exit removal works), no height animation */
@keyframes ev-toast-fade-in {
  from {
    opacity: 0;
  }
}
@media (prefers-reduced-motion: reduce) {
  [data-slot="toaster"][data-stack] [data-slot="toast"] {
    transition:
      transform 1ms linear,
      opacity 200ms;
    animation: ev-toast-fade-in 200ms;
  }
  /* keep the auto-dismiss life animation under reduced motion */
  [data-slot="toaster"][data-stack]
    [data-slot="toast"][data-autodismiss="true"][data-state="open"] {
    animation:
      ev-toast-fade-in 200ms,
      ev-toast-life var(--life) linear;
  }
}

@theme inline {
  --radius-sm: calc(var(--radius) - 4px);
  --radius-md: calc(var(--radius) - 2px);
  --radius-lg: var(--radius);
  --radius-xl: calc(var(--radius) + 4px);
  --radius-2xl: calc(var(--radius) + 8px);

  /* Depth has to be a ladder, not a single drop shadow: on a dark theme the
   * value steps alone are too close to separate a resting card from a menu
   * from a modal, so each tier gets its own spread + opacity. */
  --shadow-card: 0 1px 2px 0 rgb(0 0 0 / 0.30);
  --shadow-elevated:
    0 4px 12px -2px rgb(0 0 0 / 0.40),
    0 2px 4px -2px rgb(0 0 0 / 0.30);
  --shadow-overlay: 0 16px 48px -12px rgb(0 0 0 / 0.60);

  /* The leading var() carries its own fallback: next/font (landing) sets
   * `--font-inter` / `--font-playfair`; a Rust consumer `@font-face`s the same
   * families itself. Without the in-var fallback an undefined leading var would
   * invalidate the whole `font-family` and drop the browser to its default — so
   * it must never resolve to guaranteed-invalid. */
  --font-sans: var(--font-inter, "Inter"), ui-sans-serif, system-ui, sans-serif;
  --font-serif: var(--font-playfair, "Playfair Display"), ui-serif, Georgia, serif;
  --font-mono: var(--font-inter, "Inter"), ui-monospace, monospace;
  --font-display: var(--font-playfair, "Playfair Display"), ui-serif, Georgia, serif;

  /* surfaces */
  --color-background: var(--background);
  --color-card: var(--card);
  --color-popover: var(--popover);
  --color-muted: var(--muted);
  --color-hover: var(--hover);

  /* ink — hierarchy, loudest first */
  --color-ink: var(--ink);
  --color-ink-mid: var(--ink-mid);
  --color-ink-soft: var(--ink-soft);

  /* lines */
  --color-border: var(--border);
  --color-input: var(--input);
  --color-ring: var(--ring);

  /* roles */
  --color-brand: var(--brand);
  --color-primary: var(--primary);
  --color-on-primary: var(--on-primary);
  --color-secondary: var(--secondary);
  --color-on-secondary: var(--on-secondary);
  --color-positive: var(--positive);
  --color-on-positive: var(--on-positive);
  --color-accent-trace: var(--accent-trace);
  --color-on-accent-trace: var(--on-accent-trace);
  --color-accent-debug: var(--accent-debug);
  --color-on-accent-debug: var(--on-accent-debug);
  --color-accent-info: var(--accent-info);
  --color-on-accent-info: var(--on-accent-info);
  --color-accent-warn: var(--accent-warn);
  --color-on-accent-warn: var(--on-accent-warn);
  --color-accent-error: var(--accent-error);
  --color-on-accent-error: var(--on-accent-error);

  /* charts — distinguishable hues carrying no significance */
  --color-chart-1: var(--chart-1);
  --color-chart-2: var(--chart-2);
  --color-chart-3: var(--chart-3);
  --color-chart-4: var(--chart-4);
  --color-chart-5: var(--chart-5);
}

:root,
.dark {
  /* ── surfaces ────────────────────────────────────────────────── */
  --background: #070d18;
  --card: #16233c;
  --popover: #1d2c49;
  --muted: #26375c;
  /* the menu-item tint: a surface state, so it is named for the state */
  --hover: color-mix(in srgb, var(--ink) 8%, transparent);

  /* ── ink ─────────────────────────────────────────────────────── */
  --ink: #e6e1d3;
  --ink-mid: color-mix(in srgb, var(--ink) 82%, transparent);
  /* the most-used secondary text token, so it has to clear AA on its own */
  --ink-soft: color-mix(in srgb, var(--ink) 65%, transparent);

  /* ── lines ───────────────────────────────────────────────────── */
  --border: color-mix(in srgb, var(--ink) 14%, transparent);
  --input: color-mix(in srgb, var(--ink) 20%, transparent);
  --ring: #2a9d8f;

  /* ── roles ───────────────────────────────────────────────────── */
  --brand: #001e4e;
  /* teal, not the brand navy: navy filled a Button/Switch at ~1.1:1 against
   * these surfaces, i.e. an invisible control, and consumers hardcoded teal to
   * get around it. The brand navy still lives in --brand. */
  --primary: #2a9d8f;
  --on-primary: #070d18;
  --secondary: #0d1526;
  --on-secondary: var(--ink);
  /* valence, not significance: a success reads as good while staying quiet */
  --positive: #2e9e5b;
  --on-positive: #070d18;
  --accent-trace: #6b7fa0;
  --on-accent-trace: #070d18;
  --accent-debug: #2a9d8f;
  --on-accent-debug: #070d18;
  --accent-info: #4a90d9;
  --on-accent-info: #070d18;
  --accent-warn: #f2c94c;
  --on-accent-warn: #070d18;
  /* lifted off the brand red #da251d, which failed AA on a card. This one value
   * carries both the fill and the text; the vivid brand red is a gitops
   * constant, not a UI token. */
  --accent-error: #ef5b52;
  --on-accent-error: #070d18;

  --chart-1: #2a9d8f;
  --chart-2: #2e9e5b;
  --chart-3: #f2c94c;
  --chart-4: #e58aae;
  --chart-5: #4a90d9;

  /* ── scalars ─────────────────────────────────────────────────── */
  --radius: 0.375rem; /* 6px on mobile; bumped to 8px ≥ sm (see @media below) */
  /* control geometry, so a consumer reshapes every button/input at once */
  --control-radius: calc(var(--radius) - 2px);
  --control-py: 0.5rem;
  /* the Xl (call-to-action) button reads all three */
  --control-px: 2rem;
  --control-text: 1rem;
  /* multiplies the display type scale (uikit <Display>) */
  --display-scale: 1;
  /* the vertical rhythm of a full-bleed <Section>; `tight` is the sub-page one */
  --band-py: 2.25rem;
  --band-py-tight: 2rem;

  --sidebar-width: 14rem;

  /* page container — responsive gutter + max width (uikit <Container>) */
  --page-max: 90rem; /* 1440px */
  --page-px: 1rem; /* 16px — mobile gutter, scales up below */

  /* shell offset — viewport space reserved by whatever shell hosts the app.
     Standalone apps get 0; a hosting shell overrides on :root. Size
     viewport-bound surfaces with calc(100dvh - var(--ev-shell-offset, 0px)). */
  --ev-shell-offset: 0px;
}

/* Mobile-first scale-ups: roomier gutters + slightly larger radii on bigger
   viewports. Lives here (not per-component) so every consumer scales alike. */
@media (min-width: 640px) {
  :root {
    --radius: 0.5rem;
    --page-px: 1.5rem;
  }
}

@media (min-width: 768px) {
  :root {
    --band-py: 4rem;
    --band-py-tight: 3.5rem;
  }
}

@media (min-width: 1024px) {
  :root {
    --page-px: 3rem;
  }
}

@theme inline {
  --color-foreground: var(--ink);
  --color-muted-foreground: var(--ink-soft);
  --color-card-foreground: var(--ink);
  --color-popover-foreground: var(--ink);
  --color-primary-foreground: var(--on-primary);
  --color-secondary-foreground: var(--on-secondary);
  --color-accent: var(--hover);
  --color-accent-foreground: var(--ink);
  --color-destructive: var(--accent-error);
  --color-destructive-foreground: var(--on-accent-error);

  /* the sidebar namespace was eight aliases of tokens that already existed */
  --color-sidebar: var(--secondary);
  --color-sidebar-foreground: var(--ink);
  --color-sidebar-primary: var(--primary);
  --color-sidebar-primary-foreground: var(--on-primary);
  --color-sidebar-accent: var(--hover);
  --color-sidebar-accent-foreground: var(--ink);
  --color-sidebar-border: var(--border);
  --color-sidebar-ring: var(--ring);

  --color-main-brand: var(--brand);
  --color-main-black: var(--background);
  --color-main-surface: var(--secondary);
  --color-main-card: var(--card);
  --color-main-mist: #e6e1d3;
  --color-main-accent-t1: var(--accent-debug);
  --color-main-accent-t2: var(--positive);
  --color-main-accent-t3: var(--accent-warn);
  --color-main-accent-t4: var(--chart-4);

  --color-level-trace: var(--accent-trace);
  --color-level-debug: var(--accent-debug);
  --color-level-info: var(--accent-info);
  --color-level-warn: var(--accent-warn);
  --color-level-error: #da251d;
}

/* The seven tokens whose value moved. Everything else in `tokens.css` already
 * carries its pre-0.10 value. */
:root,
.dark {
  --ink: white;
  --ink-mid: #e6e1d3;
  --ink-soft: color-mix(in srgb, #e6e1d3 65%, transparent);
  --hover: color-mix(in srgb, #e6e1d3 8%, transparent);
  --border: color-mix(in srgb, #e6e1d3 14%, transparent);
  --input: color-mix(in srgb, #e6e1d3 20%, transparent);
  --on-secondary: white;
}