@evinvest/uikit 0.8.1 → 0.9.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/package.json +3 -2
- package/styles/tokens.css +345 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evinvest/uikit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Dep-light React UI kit: shadcn-semantics components with zero @radix-ui / cva, mirroring the ev_lib::uikit Rust feature. Ships the shared design tokens.",
|
|
5
5
|
"license": "BlueOak-1.0.0",
|
|
6
6
|
"publishConfig": {
|
|
@@ -35,7 +35,8 @@
|
|
|
35
35
|
"build": "tsup",
|
|
36
36
|
"test": "vitest run",
|
|
37
37
|
"typecheck": "tsc --noEmit",
|
|
38
|
-
"prepare": "mkdir -p styles && cp ../../tokens.css styles/tokens.css && tsup"
|
|
38
|
+
"prepare": "mkdir -p styles && cp ../../tokens.css styles/tokens.css && tsup",
|
|
39
|
+
"prepublishOnly": "node scripts/verify-pack.mjs"
|
|
39
40
|
},
|
|
40
41
|
"peerDependencies": {
|
|
41
42
|
"react": "^18 || ^19",
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Shared design tokens — the theme contract for the `ev_lib::uikit` (Rust) and
|
|
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.
|
|
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)
|
|
13
|
+
*
|
|
14
|
+
* Surface ladder — dark UI reads as unfinished when every plane is the same
|
|
15
|
+
* 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
|
|
17
|
+
* vs background) → popover #1d2c49 (1.13:1 vs card) → muted #26375c. Elevation
|
|
18
|
+
* rides the same ladder via --shadow-card / -elevated / -overlay.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
@theme inline {
|
|
22
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
23
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
24
|
+
--radius-lg: var(--radius);
|
|
25
|
+
--radius-xl: calc(var(--radius) + 4px);
|
|
26
|
+
--radius-2xl: calc(var(--radius) + 8px);
|
|
27
|
+
|
|
28
|
+
/* Depth has to be a ladder, not a single drop shadow: on a dark theme the
|
|
29
|
+
* value steps alone are too close to separate a resting card from a menu
|
|
30
|
+
* from a modal, so each tier gets its own spread + opacity. */
|
|
31
|
+
--shadow-card: 0 1px 2px 0 rgb(0 0 0 / 0.30);
|
|
32
|
+
--shadow-elevated:
|
|
33
|
+
0 4px 12px -2px rgb(0 0 0 / 0.40),
|
|
34
|
+
0 2px 4px -2px rgb(0 0 0 / 0.30);
|
|
35
|
+
--shadow-overlay: 0 16px 48px -12px rgb(0 0 0 / 0.60);
|
|
36
|
+
|
|
37
|
+
/* 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. */
|
|
42
|
+
--font-sans: var(--font-inter, "Inter"), ui-sans-serif, system-ui, sans-serif;
|
|
43
|
+
--font-serif: var(--font-playfair, "Playfair Display"), ui-serif, Georgia, serif;
|
|
44
|
+
--font-mono: var(--font-inter, "Inter"), ui-monospace, monospace;
|
|
45
|
+
|
|
46
|
+
/* shadcn semantic utilities */
|
|
47
|
+
--color-background: var(--background);
|
|
48
|
+
--color-foreground: var(--foreground);
|
|
49
|
+
--color-card: var(--card);
|
|
50
|
+
--color-card-foreground: var(--card-foreground);
|
|
51
|
+
--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
|
+
--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);
|
|
63
|
+
--color-border: var(--border);
|
|
64
|
+
--color-input: var(--input);
|
|
65
|
+
--color-ring: var(--ring);
|
|
66
|
+
|
|
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 ──────────────────────────────────── */
|
|
114
|
+
--background: #070d18;
|
|
115
|
+
--foreground: white;
|
|
116
|
+
--card: #16233c;
|
|
117
|
+
--card-foreground: white;
|
|
118
|
+
--popover: #1d2c49;
|
|
119
|
+
--popover-foreground: white;
|
|
120
|
+
/* teal, not the brand navy: navy filled a Button/Switch at ~1.1:1 against
|
|
121
|
+
* 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. */
|
|
123
|
+
--primary: #2a9d8f;
|
|
124
|
+
--primary-foreground: #070d18;
|
|
125
|
+
--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;
|
|
139
|
+
--radius: 0.375rem; /* 6px on mobile; bumped to 8px ≥ sm (see @media below) */
|
|
140
|
+
|
|
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
|
+
--sidebar-width: 14rem;
|
|
153
|
+
|
|
154
|
+
/* ── page container — responsive gutter + max width (uikit <Container>) ─ */
|
|
155
|
+
--page-max: 90rem; /* 1440px */
|
|
156
|
+
--page-px: 1rem; /* 16px — mobile gutter, scales up below */
|
|
157
|
+
|
|
158
|
+
/* ── shell offset — viewport space reserved by whatever shell hosts the app.
|
|
159
|
+
Standalone apps get 0; a hosting shell overrides on :root. Size
|
|
160
|
+
viewport-bound surfaces with calc(100dvh - var(--ev-shell-offset, 0px)). */
|
|
161
|
+
--ev-shell-offset: 0px;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/* Mobile-first scale-ups: roomier gutters + slightly larger radii on bigger
|
|
165
|
+
viewports. Lives here (not per-component) so every consumer scales alike. */
|
|
166
|
+
@media (min-width: 640px) {
|
|
167
|
+
:root {
|
|
168
|
+
--radius: 0.5rem;
|
|
169
|
+
--page-px: 1.5rem;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
@media (min-width: 1024px) {
|
|
174
|
+
: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;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
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;
|
|
344
|
+
}
|
|
345
|
+
}
|