@evinvest/uikit 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/styles/tokens.css CHANGED
@@ -1,23 +1,41 @@
1
1
  /*
2
2
  * Shared design tokens — the theme contract for the `ev_lib::uikit` (Rust) and
3
3
  * `@evinvest/uikit` (TypeScript) components. Import this file from any Tailwind v4
4
- * entrypoint. Single source for both ports — `@evinvest/uikit` copies this file
5
- * into `styles/tokens.css` from its `prepare` script.
4
+ * entrypoint. Single source for both ports — `@evinvest/uikit` copies it into
5
+ * `styles/tokens.css` from its `prepare` script.
6
6
  *
7
- * Structure:
8
- * @theme inline {} — wires values into Tailwind utilities (processed by
9
- * Tailwind v4; ignored as unknown at-rule by browsers)
10
- * :root {} — raw CSS custom properties; work everywhere
11
- * @keyframes / rules — toast (sonner) enter/exit motion at the tail; shipped
12
- * here so the lifecycle is self-contained (see below)
7
+ * The values below are EV's. A non-EV consumer writes its own sheet against the
8
+ * same names; the class tables reference nothing else.
9
+ *
10
+ * SCOPE :root · .dark · .light — a consumer defines one or both
11
+ * SURFACES background · card · popover · muted · hover
12
+ * INK ink · ink-mid · ink-soft
13
+ * LINES border · input · ring
14
+ * ROLES {role} and on-{role} for the ones that get filled:
15
+ * brand · primary · secondary · positive ·
16
+ * accent-trace · accent-debug · accent-info · accent-warn · accent-error
17
+ * SCALARS radius · control-* · display-scale · page-* · shadows · fonts
18
+ * CHARTS chart-1 … chart-5
19
+ *
20
+ * Custom properties inherit, so a scope class on a `<section>` re-themes its
21
+ * subtree: `bg-card text-ink` is correct on both polarities with no prop. EV is
22
+ * single-polarity, so `:root` and `.dark` carry the same block and `.light` is
23
+ * undefined — a `Section(Light)` is then a no-op here, which is what a
24
+ * single-polarity palette should do.
25
+ *
26
+ * Accents are ordered by significance, not by meaning; see docs/spec/accents.md.
27
+ * Their values are read outside CSS (gitops mirrors them into Discord embed
28
+ * bars), so they stay plain hex — no color-mix.
13
29
  *
14
30
  * Surface ladder — dark UI reads as unfinished when every plane is the same
15
31
  * value, so the steps below are deliberate and measured; keep them apart if you
16
- * retune. background #070d18 → surface #0d1526 (1.07:1) → card #16233c (1.24:1
32
+ * retune. background #070d18 → secondary #0d1526 (1.07:1) → card #16233c (1.24:1
17
33
  * vs background) → popover #1d2c49 (1.13:1 vs card) → muted #26375c. Elevation
18
34
  * rides the same ladder via --shadow-card / -elevated / -overlay.
19
35
  */
20
36
 
37
+ @import "./motion.css";
38
+
21
39
  @theme inline {
22
40
  --radius-sm: calc(var(--radius) - 4px);
23
41
  --radius-md: calc(var(--radius) - 2px);
@@ -35,127 +53,133 @@
35
53
  --shadow-overlay: 0 16px 48px -12px rgb(0 0 0 / 0.60);
36
54
 
37
55
  /* The leading var() carries its own fallback: next/font (landing) sets
38
- * `--font-inter` / `--font-playfair`; a Rust consumer renders `uikit::Fonts`,
39
- * which `@font-face`s the same families. Without the in-var fallback an
40
- * undefined leading var would invalidate the whole `font-family` and drop the
41
- * browser to its default — so it must never resolve to guaranteed-invalid. */
56
+ * `--font-inter` / `--font-playfair`; a Rust consumer `@font-face`s the same
57
+ * families itself. Without the in-var fallback an undefined leading var would
58
+ * invalidate the whole `font-family` and drop the browser to its default — so
59
+ * it must never resolve to guaranteed-invalid. */
42
60
  --font-sans: var(--font-inter, "Inter"), ui-sans-serif, system-ui, sans-serif;
43
61
  --font-serif: var(--font-playfair, "Playfair Display"), ui-serif, Georgia, serif;
44
62
  --font-mono: var(--font-inter, "Inter"), ui-monospace, monospace;
63
+ --font-display: var(--font-playfair, "Playfair Display"), ui-serif, Georgia, serif;
45
64
 
46
- /* shadcn semantic utilities */
65
+ /* surfaces */
47
66
  --color-background: var(--background);
48
- --color-foreground: var(--foreground);
49
67
  --color-card: var(--card);
50
- --color-card-foreground: var(--card-foreground);
51
68
  --color-popover: var(--popover);
52
- --color-popover-foreground: var(--popover-foreground);
53
- --color-primary: var(--primary);
54
- --color-primary-foreground: var(--primary-foreground);
55
- --color-secondary: var(--secondary);
56
- --color-secondary-foreground: var(--secondary-foreground);
57
69
  --color-muted: var(--muted);
58
- --color-muted-foreground: var(--muted-foreground);
59
- --color-accent: var(--accent);
60
- --color-accent-foreground: var(--accent-foreground);
61
- --color-destructive: var(--destructive);
62
- --color-destructive-foreground: var(--destructive-foreground);
70
+ --color-hover: var(--hover);
71
+
72
+ /* ink — hierarchy, loudest first */
73
+ --color-ink: var(--ink);
74
+ --color-ink-mid: var(--ink-mid);
75
+ --color-ink-soft: var(--ink-soft);
76
+
77
+ /* lines */
63
78
  --color-border: var(--border);
64
79
  --color-input: var(--input);
65
80
  --color-ring: var(--ring);
66
81
 
67
- /* sidebar utilities (PC) */
68
- --color-sidebar: var(--sidebar);
69
- --color-sidebar-foreground: var(--sidebar-foreground);
70
- --color-sidebar-primary: var(--sidebar-primary);
71
- --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
72
- --color-sidebar-accent: var(--sidebar-accent);
73
- --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
74
- --color-sidebar-border: var(--sidebar-border);
75
- --color-sidebar-ring: var(--sidebar-ring);
76
-
77
- /*
78
- * Brand palette — navy-led institutional base + tiered Vietnam-inspired accents.
79
- * Named by ROLE, not hue. Tier hierarchy:
80
- * t1 (Ha Long Teal) — recurring workhorse: labels, nav, secondary numbers
81
- * t2 (Jungle Green) — positive financial signals: yields, growth %, FDI
82
- * t3 (Rice Gold) — headline return figures: IRR, ROI, appreciation
83
- * t4 (Lotus Pink) — ≤1 per section; the punctuation jewel
84
- */
85
- --color-main-brand: #001e4e;
86
- --color-main-black: #070d18;
87
- /* these two are what consumers reach for (`bg-main-surface`, `bg-main-card`),
88
- * and they REFERENCE the semantic tokens rather than repeating them. Literal
89
- * copies drift the moment only one side is retuned, and that is not
90
- * hypothetical: a client that overrode `--card` alone got its `bg-card`
91
- * surfaces on the new palette and its `bg-main-card` ones — profile,
92
- * settings, wallet — left on the old, so half the app changed colour. */
93
- --color-main-surface: var(--secondary);
94
- --color-main-card: var(--card);
95
- --color-main-mist: #e6e1d3;
96
- --color-main-accent-t1: #2a9d8f;
97
- --color-main-accent-t2: #2e9e5b;
98
- --color-main-accent-t3: #f2c94c;
99
- --color-main-accent-t4: #e58aae;
100
-
101
- /*
102
- * Tracing levels — the one place a severity gets a hue. Read outside CSS too
103
- * (gitops mirrors these into Discord embed bars), so: plain hex, no color-mix.
104
- */
105
- --color-level-trace: #6b7fa0;
106
- --color-level-debug: #2a9d8f;
107
- --color-level-info: #4a90d9;
108
- --color-level-warn: #f2c94c;
109
- --color-level-error: #da251d;
110
- }
111
-
112
- :root {
113
- /* ── shadcn semantic tokens ──────────────────────────────────── */
82
+ /* roles */
83
+ --color-brand: var(--brand);
84
+ --color-primary: var(--primary);
85
+ --color-on-primary: var(--on-primary);
86
+ --color-secondary: var(--secondary);
87
+ --color-on-secondary: var(--on-secondary);
88
+ --color-positive: var(--positive);
89
+ --color-on-positive: var(--on-positive);
90
+ --color-accent-trace: var(--accent-trace);
91
+ --color-on-accent-trace: var(--on-accent-trace);
92
+ --color-accent-debug: var(--accent-debug);
93
+ --color-on-accent-debug: var(--on-accent-debug);
94
+ --color-accent-info: var(--accent-info);
95
+ --color-on-accent-info: var(--on-accent-info);
96
+ --color-accent-warn: var(--accent-warn);
97
+ --color-on-accent-warn: var(--on-accent-warn);
98
+ --color-accent-error: var(--accent-error);
99
+ --color-on-accent-error: var(--on-accent-error);
100
+
101
+ /* charts — distinguishable hues carrying no significance */
102
+ --color-chart-1: var(--chart-1);
103
+ --color-chart-2: var(--chart-2);
104
+ --color-chart-3: var(--chart-3);
105
+ --color-chart-4: var(--chart-4);
106
+ --color-chart-5: var(--chart-5);
107
+ }
108
+
109
+ :root,
110
+ .dark {
111
+ /* ── surfaces ────────────────────────────────────────────────── */
114
112
  --background: #070d18;
115
- --foreground: white;
116
113
  --card: #16233c;
117
- --card-foreground: white;
118
114
  --popover: #1d2c49;
119
- --popover-foreground: white;
115
+ --muted: #26375c;
116
+ /* the menu-item tint: a surface state, so it is named for the state */
117
+ --hover: color-mix(in srgb, var(--ink) 8%, transparent);
118
+
119
+ /* ── ink ─────────────────────────────────────────────────────── */
120
+ --ink: #e6e1d3;
121
+ --ink-mid: color-mix(in srgb, var(--ink) 82%, transparent);
122
+ /* the most-used secondary text token, so it has to clear AA on its own */
123
+ --ink-soft: color-mix(in srgb, var(--ink) 65%, transparent);
124
+
125
+ /* ── lines ───────────────────────────────────────────────────── */
126
+ --border: color-mix(in srgb, var(--ink) 14%, transparent);
127
+ --input: color-mix(in srgb, var(--ink) 20%, transparent);
128
+ --ring: #2a9d8f;
129
+
130
+ /* ── roles ───────────────────────────────────────────────────── */
131
+ --brand: #001e4e;
120
132
  /* teal, not the brand navy: navy filled a Button/Switch at ~1.1:1 against
121
133
  * these surfaces, i.e. an invisible control, and consumers hardcoded teal to
122
- * get around it. The brand navy still lives in --color-main-brand. */
134
+ * get around it. The brand navy still lives in --brand. */
123
135
  --primary: #2a9d8f;
124
- --primary-foreground: #070d18;
136
+ --on-primary: #070d18;
125
137
  --secondary: #0d1526;
126
- --secondary-foreground: white;
127
- --muted: #26375c;
128
- /* the most-used secondary text token, so it has to clear AA on its own */
129
- --muted-foreground: color-mix(in srgb, #e6e1d3 65%, transparent);
130
- --accent: color-mix(in srgb, #e6e1d3 8%, transparent);
131
- --accent-foreground: white;
132
- /* lifted off the brand red: this is the loss/negative signal in a financial
133
- * product, and the brand value failed AA on a card */
134
- --destructive: #ef5b52;
135
- --destructive-foreground: #070d18;
136
- --border: color-mix(in srgb, #e6e1d3 14%, transparent);
137
- --input: color-mix(in srgb, #e6e1d3 20%, transparent);
138
- --ring: #2a9d8f;
138
+ --on-secondary: var(--ink);
139
+ /* valence, not significance: a success reads as good while staying quiet */
140
+ --positive: #2e9e5b;
141
+ --on-positive: #070d18;
142
+ --accent-trace: #6b7fa0;
143
+ --on-accent-trace: #070d18;
144
+ --accent-debug: #2a9d8f;
145
+ --on-accent-debug: #070d18;
146
+ --accent-info: #4a90d9;
147
+ --on-accent-info: #070d18;
148
+ --accent-warn: #f2c94c;
149
+ --on-accent-warn: #070d18;
150
+ /* lifted off the brand red #da251d, which failed AA on a card. This one value
151
+ * carries both the fill and the text; the vivid brand red is a gitops
152
+ * constant, not a UI token. */
153
+ --accent-error: #ef5b52;
154
+ --on-accent-error: #070d18;
155
+
156
+ --chart-1: #2a9d8f;
157
+ --chart-2: #2e9e5b;
158
+ --chart-3: #f2c94c;
159
+ --chart-4: #e58aae;
160
+ --chart-5: #4a90d9;
161
+
162
+ /* ── scalars ─────────────────────────────────────────────────── */
139
163
  --radius: 0.375rem; /* 6px on mobile; bumped to 8px ≥ sm (see @media below) */
164
+ /* control geometry, so a consumer reshapes every button/input at once */
165
+ --control-radius: calc(var(--radius) - 2px);
166
+ --control-py: 0.5rem;
167
+ /* the Xl (call-to-action) button reads all three */
168
+ --control-px: 2rem;
169
+ --control-text: 1rem;
170
+ /* multiplies the display type scale (uikit <Display>) */
171
+ --display-scale: 1;
172
+ /* the vertical rhythm of a full-bleed <Section>; `tight` is the sub-page one */
173
+ --band-py: 2.25rem;
174
+ --band-py-tight: 2rem;
140
175
 
141
- /* ── sidebar (PC nav) ───────────────────────────────────────── */
142
- /* the rail sits one step under the cards it frames, so it tracks --secondary
143
- on the ladder rather than --card */
144
- --sidebar: #0d1526;
145
- --sidebar-foreground: white;
146
- --sidebar-primary: #2a9d8f;
147
- --sidebar-primary-foreground: #070d18;
148
- --sidebar-accent: color-mix(in srgb, #e6e1d3 8%, transparent);
149
- --sidebar-accent-foreground: white;
150
- --sidebar-border: color-mix(in srgb, #e6e1d3 14%, transparent);
151
- --sidebar-ring: #2a9d8f;
152
176
  --sidebar-width: 14rem;
153
177
 
154
- /* ── page container — responsive gutter + max width (uikit <Container>) ─ */
178
+ /* page container — responsive gutter + max width (uikit <Container>) */
155
179
  --page-max: 90rem; /* 1440px */
156
180
  --page-px: 1rem; /* 16px — mobile gutter, scales up below */
157
181
 
158
- /* ── shell offset — viewport space reserved by whatever shell hosts the app.
182
+ /* shell offset — viewport space reserved by whatever shell hosts the app.
159
183
  Standalone apps get 0; a hosting shell overrides on :root. Size
160
184
  viewport-bound surfaces with calc(100dvh - var(--ev-shell-offset, 0px)). */
161
185
  --ev-shell-offset: 0px;
@@ -170,176 +194,15 @@
170
194
  }
171
195
  }
172
196
 
173
- @media (min-width: 1024px) {
197
+ @media (min-width: 768px) {
174
198
  :root {
175
- --page-px: 3rem;
176
- }
177
- }
178
-
179
- /* ── Toast stacking (Sonner-style), shared by both ports via data-stack ──────
180
- Both `ev_lib::uikit` (Dioxus) and `@evinvest/uikit` (React) set data-stack on the
181
- toaster and drive the same machine: toasts are absolutely stacked at the
182
- pinned edge, pile up collapsed (front three peeking, each lifted + scaled by
183
- --index and clamped to --front-height), and spread to --offset on hover /
184
- keyboard focus (pure CSS — no JS state). Vars: the toaster sets --front-height
185
- + --gap; each toast sets --index, --z-index, --offset, --initial-height. React
186
- measures real heights; Dioxus can't (host-only), so it feeds a constant height
187
- — collapse is exact, expansion is uniform. The enter is a keyframe (plays on
188
- DOM insertion, so it works in both ports without a JS mount flip),
189
- `data-state=closed` slides out, and the node is dropped on the transitionend. */
190
-
191
- [data-slot="toaster"][data-stack] [data-slot="toast"] {
192
- position: absolute;
193
- --y: translateY(0);
194
- opacity: 1;
195
- z-index: var(--z-index, 1);
196
- transform: var(--y) translateX(var(--swipe-x, 0px));
197
- /* a plain easeOut (no overshoot) so a restack settles instead of bouncing
198
- back past its slot */
199
- transition:
200
- transform 350ms cubic-bezier(0.21, 0.8, 0.4, 1),
201
- opacity 350ms,
202
- height 350ms;
203
- /* the enter is a keyframe (plays on DOM insertion in both ports — no JS mount
204
- flip, whose pre-paint timing the Dioxus effect couldn't drive). It animates
205
- the independent `translate` property, NOT `transform`, so it *composes* with
206
- the stack transform instead of overriding it: a toast pushed back mid-enter
207
- keeps repositioning smoothly instead of fighting the keyframe (rapid-fire
208
- lag). */
209
- animation: ev-toast-enter 350ms cubic-bezier(0.21, 0.8, 0.4, 1) backwards;
210
- }
211
- @keyframes ev-toast-enter {
212
- from {
213
- opacity: 0;
214
- translate: 0 var(--enter-y, 100%);
215
- }
216
- to {
217
- opacity: 1;
218
- translate: 0 0;
199
+ --band-py: 4rem;
200
+ --band-py-tight: 3.5rem;
219
201
  }
220
202
  }
221
203
 
222
- /* pin to the toaster's edge; set the lift sign + the off-screen enter offset */
223
- [data-slot="toaster"][data-stack][data-y-position="bottom"] [data-slot="toast"] {
224
- bottom: 0;
225
- --lift: -1;
226
- --enter-y: 100%;
227
- }
228
- [data-slot="toaster"][data-stack][data-y-position="top"] [data-slot="toast"] {
229
- top: 0;
230
- --lift: 1;
231
- --enter-y: -100%;
232
- }
233
-
234
- /* closing: slide back off + fade out; the node is dropped on its transitionend */
235
- [data-slot="toaster"][data-stack] [data-slot="toast"][data-state="closed"] {
236
- --y: translateY(var(--enter-y));
237
- opacity: 0;
238
- }
239
-
240
- /* collapsed: back toasts lift + shrink by depth, clamp to the front height */
241
- [data-slot="toaster"][data-stack]
242
- [data-slot="toast"][data-state="open"][data-front="false"] {
243
- --y: translateY(calc(var(--lift) * var(--gap) * var(--index)))
244
- scale(calc(1 - var(--index) * 0.05));
245
- height: var(--front-height);
246
- }
247
- [data-slot="toaster"][data-stack] [data-slot="toast"][data-front="false"] > * {
248
- opacity: 0;
249
- transition: opacity 300ms;
250
- }
251
- /* only the front three show while collapsed */
252
- [data-slot="toaster"][data-stack] [data-slot="toast"][data-visible="false"] {
253
- opacity: 0;
254
- pointer-events: none;
255
- }
256
-
257
- /* expanded (hover / keyboard focus): spread to the offset, real height, content
258
- + overflow back */
259
- [data-slot="toaster"][data-stack]:hover [data-slot="toast"][data-state="open"],
260
- [data-slot="toaster"][data-stack]:focus-within
261
- [data-slot="toast"][data-state="open"] {
262
- --y: translateY(calc(var(--lift) * var(--offset)));
263
- height: var(--initial-height);
264
- }
265
- [data-slot="toaster"][data-stack]:hover [data-slot="toast"] > *,
266
- [data-slot="toaster"][data-stack]:focus-within [data-slot="toast"] > *,
267
- [data-slot="toaster"][data-stack]:hover [data-slot="toast"][data-visible="false"],
268
- [data-slot="toaster"][data-stack]:focus-within
269
- [data-slot="toast"][data-visible="false"] {
270
- opacity: 1;
271
- pointer-events: auto;
272
- }
273
-
274
- /* a transparent strip over the gap so the hover area stays contiguous between
275
- spread toasts (otherwise the pointer falls through the gap and it collapses) */
276
- [data-slot="toaster"][data-stack] [data-slot="toast"]::after {
277
- content: "";
278
- position: absolute;
279
- left: 0;
280
- width: 100%;
281
- height: calc(var(--gap) + 1px);
282
- }
283
- [data-slot="toaster"][data-stack][data-y-position="bottom"]
284
- [data-slot="toast"]::after {
285
- bottom: 100%;
286
- }
287
- [data-slot="toaster"][data-stack][data-y-position="top"]
288
- [data-slot="toast"]::after {
289
- top: 100%;
290
- }
291
-
292
- /* 1:1 finger tracking during a swipe — kill the transition */
293
- [data-slot="toaster"][data-stack] [data-slot="toast"][data-swiping="true"] {
294
- transition: none;
295
- }
296
-
297
- /* auto-dismiss without a host timer: a no-op "life" animation runs for --life,
298
- then its `animationend` fires (the toast closes itself). Hovering/focusing the
299
- stack pauses just that animation (animation-play-state's 2nd slot), so the
300
- countdown holds — and resumes with the elapsed time preserved — while the
301
- enter (1st slot) keeps running. Persistent toasts set data-autodismiss=false,
302
- so they get no life animation. (The React port uses setTimeout instead.) */
303
- @keyframes ev-toast-life {
304
- from {
305
- --ev-life: 0;
306
- }
307
- to {
308
- --ev-life: 1;
309
- }
310
- }
311
- [data-slot="toaster"][data-stack]
312
- [data-slot="toast"][data-autodismiss="true"][data-state="open"] {
313
- animation:
314
- ev-toast-enter 350ms cubic-bezier(0.21, 0.8, 0.4, 1) backwards,
315
- ev-toast-life var(--life) linear;
316
- }
317
- [data-slot="toaster"][data-stack]:hover
318
- [data-slot="toast"][data-autodismiss="true"],
319
- [data-slot="toaster"][data-stack]:focus-within
320
- [data-slot="toast"][data-autodismiss="true"] {
321
- animation-play-state: running, paused;
322
- }
323
-
324
- /* reduced motion: opacity-only enter + near-instant transform (still fires
325
- transitionend, so exit removal works), no height animation */
326
- @keyframes ev-toast-fade-in {
327
- from {
328
- opacity: 0;
329
- }
330
- }
331
- @media (prefers-reduced-motion: reduce) {
332
- [data-slot="toaster"][data-stack] [data-slot="toast"] {
333
- transition:
334
- transform 1ms linear,
335
- opacity 200ms;
336
- animation: ev-toast-fade-in 200ms;
337
- }
338
- /* keep the auto-dismiss life animation under reduced motion */
339
- [data-slot="toaster"][data-stack]
340
- [data-slot="toast"][data-autodismiss="true"][data-state="open"] {
341
- animation:
342
- ev-toast-fade-in 200ms,
343
- ev-toast-life var(--life) linear;
204
+ @media (min-width: 1024px) {
205
+ :root {
206
+ --page-px: 3rem;
344
207
  }
345
208
  }