@hanzo/design 0.4.1 → 0.4.3
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/components/core/Button.jsx +35 -9
- package/components/core/Card.jsx +20 -5
- package/components/forms/Input.jsx +13 -3
- package/components/navigation/Tabs.jsx +18 -7
- package/components/overlays/Dialog.jsx +2 -2
- package/components/overlays/DropdownMenu.jsx +9 -5
- package/dist/tokens.gen.d.ts +40 -22
- package/dist/tokens.gen.d.ts.map +1 -1
- package/dist/tokens.gen.js +40 -22
- package/guidelines/brand-elevation.card.html +3 -3
- package/guidelines/brand-imagery.card.html +3 -3
- package/guidelines/brand-motion.card.html +3 -3
- package/guidelines/brand-radii.card.html +6 -6
- package/guidelines/color-borders.card.html +8 -7
- package/guidelines/type-eyebrow.card.html +1 -1
- package/guidelines/type-mono.card.html +1 -1
- package/package.json +10 -8
- package/scripts/check-tokens.mjs +62 -18
- package/scripts/lint.mjs +26 -10
- package/src/tokens.gen.ts +40 -22
- package/styles.css +245 -60
- package/tailwind.css +245 -60
- package/tokens/base.css +56 -3
- package/tokens/colors.css +98 -52
- package/tokens/elevation.css +76 -5
- package/tokens/motion.css +15 -0
package/tailwind.css
CHANGED
|
@@ -102,59 +102,82 @@
|
|
|
102
102
|
--popover:#0f0f0f;
|
|
103
103
|
--popover-foreground:#fafafa;
|
|
104
104
|
--primary:#fafafa;
|
|
105
|
-
|
|
105
|
+
/* A 5% step, not the 16% this used to be. A filled button's fill is already
|
|
106
|
+
the top of the ink ramp, so the only directions available on hover are
|
|
107
|
+
"dimmer" and "brighter-around-the-edges" — and a 16% drop reads as the
|
|
108
|
+
button going DISABLED under the cursor, which is the one thing a hover must
|
|
109
|
+
never look like. The step is now small enough to register as pressure while
|
|
110
|
+
--bloom does the actual work of lighting it up. */
|
|
111
|
+
--primary-hover:#e5e5e5;
|
|
106
112
|
--primary-foreground:#0a0a0a;
|
|
107
113
|
--secondary:#262626;
|
|
114
|
+
/* One rung up, so a secondary button HAS a hover. It previously resolved to
|
|
115
|
+
--surface-3, which is --secondary — the same value it starts at. */
|
|
116
|
+
--secondary-hover:#333333;
|
|
108
117
|
--secondary-foreground:#fafafa;
|
|
109
118
|
--muted:#171717;
|
|
110
119
|
--muted-foreground:#a3a3a3;
|
|
111
120
|
--accent:#262626;
|
|
112
121
|
--accent-foreground:#fafafa;
|
|
113
|
-
|
|
122
|
+
/* Destructive is the ONE place the monochrome rule already yields, and it was
|
|
123
|
+
yielding in only half the system: a field that fails validation draws
|
|
124
|
+
--state-error (#ef4444, which DESIGN.md §2.4 lists as permitted precisely
|
|
125
|
+
for "destructive / blocking error"), while the button that performs the
|
|
126
|
+
destruction drew #666666 — a mid-grey that is indistinguishable from a
|
|
127
|
+
disabled control. "Delete everything" and "you cannot click this" must not
|
|
128
|
+
be the same colour. So the two converge on the one token that already
|
|
129
|
+
names this meaning, and the ramp stays monochrome everywhere else. */
|
|
130
|
+
--destructive:var(--state-error);
|
|
131
|
+
--destructive-hover:#dc2626; /* red-600 — deepens under the cursor */
|
|
114
132
|
--destructive-foreground:#fafafa;
|
|
115
133
|
|
|
116
134
|
/* ——— boundaries ———
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
--border
|
|
123
|
-
--border-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
--border-focus is therefore one NEUTRAL rung above --border-control, not an
|
|
145
|
-
alpha step: a focused field has to read brighter than a resting one, and
|
|
146
|
-
the resting one already sits at the WCAG floor. */
|
|
135
|
+
ONE ladder, cut from alpha, graded by duty. Every rung is a rung of the
|
|
136
|
+
opacity ladder above, so every boundary composites correctly on the page,
|
|
137
|
+
on a card, and inside a popover — and every rung is the reference's own
|
|
138
|
+
measured value, not a rounded ramp:
|
|
139
|
+
|
|
140
|
+
--border .10 the hairline. Cards, panels, dividers, list rows.
|
|
141
|
+
--border-strong .16 the same hairline, one step up. Hover, emphasis.
|
|
142
|
+
--border-control .15 a control's resting edge — input, select, textarea.
|
|
143
|
+
--border-focus .22 that control, focused.
|
|
144
|
+
--border-selected .30 the thing that is currently CHOSEN — the active
|
|
145
|
+
tab, the current tile, the featured plan.
|
|
146
|
+
|
|
147
|
+
The previous release cut the last three from the NEUTRAL ladder instead,
|
|
148
|
+
because a boundary that clears WCAG 1.4.11's 3:1 on a near-black ground IS
|
|
149
|
+
a mid-grey and no amount of tuning changes that. It is true, and it is also
|
|
150
|
+
what a wireframe looks like: a 1px #737373 box around every field, four
|
|
151
|
+
times heavier than the hairline beside it, so a form read as a debug
|
|
152
|
+
overlay rather than a surface. Aesthetics decided it — the reference draws
|
|
153
|
+
an input at rgb(255 255 255 / .15) and a focused one at .22 (sidebar.css
|
|
154
|
+
--input-border / .composer-box:focus-within), and that is what ships here.
|
|
155
|
+
|
|
156
|
+
What replaces the ratio is the FOCUS INDICATOR, which is the thing a
|
|
157
|
+
keyboard user actually navigates by and the one boundary still pinned at
|
|
158
|
+
3:1 (--ring, below). A resting edge is an affordance; a focus ring is a
|
|
159
|
+
position. Only the second one is load-bearing when you cannot see well, and
|
|
160
|
+
spending the contrast budget there rather than on all four edges of every
|
|
161
|
+
idle field is what lets the system be quiet AND findable at once. */
|
|
147
162
|
--border:var(--white-10);
|
|
148
163
|
--border-strong:var(--white-16);
|
|
149
|
-
--border-control:var(--
|
|
150
|
-
--border-focus:var(--
|
|
151
|
-
--
|
|
164
|
+
--border-control:var(--white-15); /* sidebar.css --input-border */
|
|
165
|
+
--border-focus:var(--white-22); /* .composer-box:focus-within */
|
|
166
|
+
--border-selected:var(--white-30); /* the current tab / tile / plan */
|
|
167
|
+
--input:var(--border-control); /* shadcn `border-input` — a control */
|
|
152
168
|
/* A focus indicator is a NON-TEXT CONTRAST target: WCAG 2.4.11/1.4.11 require
|
|
153
|
-
3:1 against every surface it can land on
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
169
|
+
3:1 against every surface it can land on, and unlike a resting edge it is
|
|
170
|
+
worth the loudness — it is transient, it only appears on keyboard focus,
|
|
171
|
+
and it is the whole of how a keyboard user knows where they are.
|
|
172
|
+
--white-40 clears it on every canvas in both themes precisely BECAUSE it is
|
|
173
|
+
alpha: on a lifted surface the ring lifts with it (3.77:1 on #0a0a0a,
|
|
174
|
+
3.66:1 on #262626), which a fixed grey cannot do. */
|
|
175
|
+
--ring:var(--white-40);
|
|
176
|
+
/* The soft halo that sits outside the ring — the reference's
|
|
177
|
+
`box-shadow: 0 0 0 2px rgb(255 255 255 / .06)` on a focused composer. It
|
|
178
|
+
carries no ratio; it is what makes focus read as designed rather than as a
|
|
179
|
+
browser default. Spent through --ring-focus (tokens/elevation.css). */
|
|
180
|
+
--ring-halo:var(--white-10);
|
|
158
181
|
--brand:#e4e4e7;
|
|
159
182
|
--brand-foreground:#09090b;
|
|
160
183
|
--brand-muted:#a3a3a3;
|
|
@@ -169,12 +192,27 @@
|
|
|
169
192
|
--glass:var(--white-05);
|
|
170
193
|
--glass-strong:var(--white-08);
|
|
171
194
|
|
|
172
|
-
/* ——— surface recipes
|
|
195
|
+
/* ——— surface recipes ———
|
|
196
|
+
A card has to be a SURFACE, which means you have to be able to see that it
|
|
197
|
+
is not the page. These were cut from neutral-900 (#171717) at .4/.5/.8, and
|
|
198
|
+
at .5 that composites to #101010 on a #0a0a0a ground — six levels above the
|
|
199
|
+
page, which is not a surface, it is a very slightly different black. Three
|
|
200
|
+
cards side by side then read as one flat field with hairlines ruled across
|
|
201
|
+
it, and no amount of shadow fixes it, because you cannot cast a shadow onto
|
|
202
|
+
black.
|
|
203
|
+
|
|
204
|
+
Cut from neutral-800 (#262626) instead, they land on 14 / 18 / 1f — the
|
|
205
|
+
reference's own panel (#171717 on a #0f0f0f container) with room either
|
|
206
|
+
side of it. They stay ALPHA so a card nested in a popover still lifts off
|
|
207
|
+
its parent rather than punching a hole in it; that is the whole reason
|
|
208
|
+
these are recipes and not just --surface-* rungs. */
|
|
173
209
|
--surface-page:var(--background);
|
|
174
|
-
--surface-card:rgb(
|
|
175
|
-
--surface-card-emphasis:rgb(
|
|
176
|
-
--surface-card-quiet:rgb(
|
|
177
|
-
|
|
210
|
+
--surface-card:rgb(38 38 38 / .5); /* -> #181818 on the page */
|
|
211
|
+
--surface-card-emphasis:rgb(38 38 38 / .75);/* -> #1f1f1f — featured */
|
|
212
|
+
--surface-card-quiet:rgb(38 38 38 / .35); /* -> #141414 — story cards */
|
|
213
|
+
/* A floating panel that is the SAME colour as the page is not floating. This
|
|
214
|
+
was rgb(10 10 10 / .95) — the page, at 95% of the page. */
|
|
215
|
+
--surface-overlay:rgb(23 23 23 / .92); /* dropdown / popover panels */
|
|
178
216
|
--surface-header:rgb(0 0 0 / .7); /* fixed nav, with backdrop blur */
|
|
179
217
|
--surface-scrim:rgb(0 0 0 / .8); /* the dialog / sheet backdrop */
|
|
180
218
|
|
|
@@ -226,22 +264,30 @@
|
|
|
226
264
|
--primary-hover:#262626;
|
|
227
265
|
--primary-foreground:#fafafa;
|
|
228
266
|
--secondary:#ededed;
|
|
267
|
+
--secondary-hover:#e0e0e0;
|
|
229
268
|
--secondary-foreground:#0a0a0a;
|
|
230
269
|
--muted:#f5f5f5;
|
|
231
270
|
--muted-foreground:#525252;
|
|
232
271
|
--accent:#ededed;
|
|
233
272
|
--accent-foreground:#0a0a0a;
|
|
234
|
-
--destructive
|
|
273
|
+
--destructive:var(--state-error);
|
|
274
|
+
--destructive-hover:#dc2626;
|
|
235
275
|
--destructive-foreground:#ffffff;
|
|
236
276
|
--border:rgb(0 0 0 / .10);
|
|
237
277
|
--border-strong:rgb(0 0 0 / .16);
|
|
238
|
-
--border-control:
|
|
239
|
-
|
|
278
|
+
--border-control:rgb(0 0 0 / .15);
|
|
279
|
+
/* Focus and selection are pushed further here than the .22/.30 they sit at in
|
|
280
|
+
dark. Black-on-white at a given alpha reads FAINTER than white-on-black at
|
|
281
|
+
the same alpha — .22 black on #ffffff measures 1.69:1 where .22 white on
|
|
282
|
+
#0a0a0a measures 1.97:1 — so mirroring the number would quietly make the
|
|
283
|
+
light theme's focus state the weakest state in the system. */
|
|
284
|
+
--border-focus:rgb(0 0 0 / .32);
|
|
285
|
+
--border-selected:rgb(0 0 0 / .42);
|
|
240
286
|
--input:var(--border-control);
|
|
241
|
-
/* Same
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
--ring:
|
|
287
|
+
/* Same duty, same 3:1 floor, opposite direction: .50 black is 3.98:1 on
|
|
288
|
+
#ffffff and 3.67:1 on #f5f5f5. */
|
|
289
|
+
--ring:rgb(0 0 0 / .5);
|
|
290
|
+
--ring-halo:rgb(0 0 0 / .07);
|
|
245
291
|
--black:#0a0a0a;
|
|
246
292
|
--white:#ffffff;
|
|
247
293
|
--selection:rgb(0 0 0 / .16);
|
|
@@ -498,14 +544,49 @@
|
|
|
498
544
|
}
|
|
499
545
|
|
|
500
546
|
/* ── tokens/elevation.css ─────────────────────────────────────── */
|
|
501
|
-
/*
|
|
502
|
-
|
|
503
|
-
|
|
547
|
+
/* Elevation on a near-black ground is NOT a drop shadow. A dark blur on a dark
|
|
548
|
+
page is nothing — you cannot cast a shadow onto black — which is why a system
|
|
549
|
+
that ships only `box-shadow` reads flat no matter how heavy the alphas get.
|
|
550
|
+
|
|
551
|
+
What actually lifts a surface here is LIGHT, and it arrives in three parts:
|
|
552
|
+
|
|
553
|
+
1. a hairline EDGE (tokens/colors.css --border)
|
|
554
|
+
2. an inset top HIGHLIGHT (--edge-highlight, below)
|
|
555
|
+
3. a wide, very dark DROP (--shadow-*)
|
|
556
|
+
|
|
557
|
+
(2) is the one that was missing and the one that does most of the work: a 1px
|
|
558
|
+
inset white line along the top edge reads as the surface catching the light
|
|
559
|
+
from above, and it is the whole difference between a panel that sits ON the
|
|
560
|
+
page and a rectangle that is merely a different colour than the page. The
|
|
561
|
+
reference spends it on its login card (`inset 0 1px 0 rgb(255 255 255 / .05)`)
|
|
562
|
+
and it is the single cheapest premium signal available on dark UI.
|
|
563
|
+
|
|
564
|
+
They are separate tokens rather than one baked shadow because a component
|
|
565
|
+
composes the ones it needs — `box-shadow: var(--edge-highlight), var(--shadow-lg)`
|
|
566
|
+
— and a card that wants the highlight without the drop should not have to
|
|
567
|
+
accept both. */
|
|
504
568
|
:root{
|
|
505
569
|
--shadow-none:none;
|
|
506
|
-
|
|
570
|
+
/* The floating surface: dropdowns, dialogs, sheets, the composer. Wide, soft,
|
|
571
|
+
and pushed well down. It was Tailwind's `shadow-2xl` at .25 alpha, which is
|
|
572
|
+
invisible on #0a0a0a — so the surfaces that float highest had the weakest
|
|
573
|
+
drop in the system while the t-shirt ramp below was correctly tuned. */
|
|
574
|
+
--shadow-floating:0 24px 60px -16px rgb(0 0 0 / .75);
|
|
507
575
|
--shadow-inset-hairline:inset 0 0 0 1px var(--white-10);
|
|
508
|
-
|
|
576
|
+
|
|
577
|
+
/* ——— the light ——— */
|
|
578
|
+
/* The top edge, catching light. Add it to any raised surface. */
|
|
579
|
+
--edge-highlight:inset 0 1px 0 0 rgb(255 255 255 / .10);
|
|
580
|
+
/* The bloom around a LIT action on hover — a filled primary button, the send
|
|
581
|
+
key. Monochrome brand: this is where "alive" comes from instead of a hue,
|
|
582
|
+
and it is the whole hover signal on a fill that is already at the top of
|
|
583
|
+
the ink ramp. It was .10, which is invisible: a white glow has to compete
|
|
584
|
+
with the white object casting it, so the alpha that reads around a WHITE
|
|
585
|
+
button is far higher than the one that reads around a dark one. */
|
|
586
|
+
--bloom:0 0 24px -2px rgb(255 255 255 / .28);
|
|
587
|
+
/* The focus halo: soft, outside the ring, carrying no ratio of its own. This
|
|
588
|
+
was `0 0 0 2px var(--ring)` — a second hard ring around the hard ring. */
|
|
589
|
+
--ring-focus:0 0 0 3px var(--ring-halo);
|
|
509
590
|
|
|
510
591
|
/* The t-shirt ramp. Named by size rather than by role, because that is how
|
|
511
592
|
every component library already asks for a shadow (and how @hanzo/brand
|
|
@@ -523,6 +604,11 @@
|
|
|
523
604
|
--glow-hero-blur:120px;
|
|
524
605
|
/* Card top-corner sheen used on the story cards. */
|
|
525
606
|
--sheen-card:radial-gradient(120% 120% at 80% 0%,rgb(255 255 255 / .08) 0%,transparent 55%); /* @kind color */
|
|
607
|
+
/* The panel's top hairline, brightest at the centre and dissolving before it
|
|
608
|
+
reaches either corner — so an edge never terminates in a hard stop. One of
|
|
609
|
+
the two details that make the reference's panels look machined rather than
|
|
610
|
+
drawn (sidebar.css .panel::before). Paint it on a 1px-tall ::before. */
|
|
611
|
+
--sheen-edge:linear-gradient(90deg,transparent,rgb(255 255 255 / .14) 50%,transparent); /* @kind color */
|
|
526
612
|
/* Chrome text: the canonical headline gradient. Never a saturated rainbow. */
|
|
527
613
|
--gradient-chrome:linear-gradient(to right,#ffffff,var(--white-80),var(--white-60));
|
|
528
614
|
--gradient-chrome-2:linear-gradient(to right,#ffffff,var(--neutral-500));
|
|
@@ -530,6 +616,37 @@
|
|
|
530
616
|
--gradient-protect:linear-gradient(to bottom,var(--white-10),transparent);
|
|
531
617
|
}
|
|
532
618
|
|
|
619
|
+
/* Light theme — the same three parts, redistributed.
|
|
620
|
+
On white the order of work inverts: the DROP does almost all of it and the
|
|
621
|
+
inset highlight does none, because a white line on a white card is nothing.
|
|
622
|
+
Every token below is one whose dark value is white — i.e. one that silently
|
|
623
|
+
stops existing in `.light` unless it is restated here. check-tokens.mjs
|
|
624
|
+
scans this file for exactly that omission.
|
|
625
|
+
|
|
626
|
+
--edge-highlight is `inset 0 0 0 0 transparent` rather than `none` on
|
|
627
|
+
purpose: components compose these — `box-shadow: var(--edge-highlight),
|
|
628
|
+
var(--shadow-lg)` — and `none` is not a legal layer in a comma list, so the
|
|
629
|
+
obvious spelling would invalidate the whole declaration and take the drop
|
|
630
|
+
shadow down with it. A zero-size transparent shadow composes to nothing and
|
|
631
|
+
costs nothing. */
|
|
632
|
+
.light{
|
|
633
|
+
--shadow-floating:0 24px 60px -16px rgb(0 0 0 / .18);
|
|
634
|
+
--shadow-inset-hairline:inset 0 0 0 1px rgb(0 0 0 / .10);
|
|
635
|
+
--edge-highlight:inset 0 0 0 0 transparent;
|
|
636
|
+
--bloom:0 6px 20px -4px rgb(0 0 0 / .18);
|
|
637
|
+
--shadow-sm:0 1px 2px 0 rgb(0 0 0 / .06);
|
|
638
|
+
--shadow:0 1px 3px 0 rgb(0 0 0 / .08), 0 1px 2px -1px rgb(0 0 0 / .06);
|
|
639
|
+
--shadow-md:0 4px 6px -1px rgb(0 0 0 / .08), 0 2px 4px -2px rgb(0 0 0 / .06);
|
|
640
|
+
--shadow-lg:0 10px 15px -3px rgb(0 0 0 / .09), 0 4px 6px -4px rgb(0 0 0 / .07);
|
|
641
|
+
--shadow-xl:0 20px 25px -5px rgb(0 0 0 / .10), 0 8px 10px -6px rgb(0 0 0 / .08);
|
|
642
|
+
--glow-hero:radial-gradient(circle,rgb(0 0 0 / .06) 0%,transparent 68%);
|
|
643
|
+
--sheen-card:radial-gradient(120% 120% at 80% 0%,rgb(0 0 0 / .04) 0%,transparent 55%);
|
|
644
|
+
--sheen-edge:linear-gradient(90deg,transparent,rgb(0 0 0 / .08) 50%,transparent);
|
|
645
|
+
--gradient-chrome:linear-gradient(to right,#0a0a0a,rgb(10 10 10 / .8),rgb(10 10 10 / .6));
|
|
646
|
+
--gradient-chrome-2:linear-gradient(to right,#0a0a0a,var(--neutral-500));
|
|
647
|
+
--gradient-protect:linear-gradient(to bottom,rgb(0 0 0 / .10),transparent);
|
|
648
|
+
}
|
|
649
|
+
|
|
533
650
|
/* ── tokens/motion.css ─────────────────────────────────────── */
|
|
534
651
|
/* Motion is restrained: fade + small rise, CSS-only hovers, one breathing glow.
|
|
535
652
|
No springs, no bounce, no parallax, no autoplay carousels. */
|
|
@@ -539,8 +656,17 @@
|
|
|
539
656
|
--duration-slow:400ms; /* @kind other */ /* hero element entry */
|
|
540
657
|
--duration-slower:500ms; /* @kind other */ /* section entry */
|
|
541
658
|
--duration-glow:9s; /* @kind other */ /* ambient radial breathe */
|
|
659
|
+
--duration-press:90ms; /* @kind other */ /* the press-down on :active */
|
|
542
660
|
--ease-out:cubic-bezier(0,0,0.2,1); /* @kind other */
|
|
543
661
|
--ease-in-out:cubic-bezier(0.4,0,0.2,1); /* @kind other */
|
|
662
|
+
/* The entrance curve. Nearly all of the distance is covered in the first
|
|
663
|
+
third and it settles without overshooting, so a panel arrives DECIDED
|
|
664
|
+
rather than drifting in — the difference between 300ms that feels
|
|
665
|
+
immediate and 300ms that feels slow. Use it for anything that appears
|
|
666
|
+
(menus, dialogs, sheets, toasts); --ease-out stays the curve for a state
|
|
667
|
+
change on something already on screen. It is not a spring: this system has
|
|
668
|
+
no bounce anywhere. */
|
|
669
|
+
--ease-emphasis:cubic-bezier(0.16,1,0.3,1); /* @kind other */
|
|
544
670
|
--stagger:60ms; /* @kind other */ /* per-element delay in a group */
|
|
545
671
|
--entry-rise:16px; /* hero y-offset */
|
|
546
672
|
--entry-rise-lg:24px; /* card y-offset */
|
|
@@ -549,6 +675,12 @@
|
|
|
549
675
|
@keyframes hanzo-fade-up{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}
|
|
550
676
|
@keyframes hanzo-fade-down{from{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:translateY(0)}}
|
|
551
677
|
@keyframes hanzo-slide-up-fade{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}
|
|
678
|
+
/* The entrance for anything that appears OVER the page — dialog, sheet, command
|
|
679
|
+
palette. It arrives from very slightly small and very slightly low, which is
|
|
680
|
+
what reads as "came forward" rather than "was toggled on". 3% and 8px: any
|
|
681
|
+
more and it is an animation you notice, which is the failure mode. Pair it
|
|
682
|
+
with --ease-emphasis. */
|
|
683
|
+
@keyframes hanzo-zoom-in{from{opacity:0;transform:scale(.97) translateY(8px)}to{opacity:1;transform:none}}
|
|
552
684
|
@keyframes hanzo-glow{0%,100%{transform:scale(1);opacity:.45}50%{transform:scale(1.08);opacity:.65}}
|
|
553
685
|
@keyframes hanzo-pulse-dot{0%,100%{opacity:1}50%{opacity:.35}}
|
|
554
686
|
|
|
@@ -689,7 +821,19 @@
|
|
|
689
821
|
background-color var(--duration-fast) var(--ease-out),
|
|
690
822
|
border-color var(--duration-fast) var(--ease-out),
|
|
691
823
|
box-shadow var(--duration-fast) var(--ease-out),
|
|
692
|
-
opacity var(--duration-fast) var(--ease-out)
|
|
824
|
+
opacity var(--duration-fast) var(--ease-out),
|
|
825
|
+
transform var(--duration-press) var(--ease-out);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/* ——— the press ———
|
|
829
|
+
A button that moves under the finger is the cheapest "this is a real
|
|
830
|
+
control" signal there is, and its absence is most of why a correct-looking
|
|
831
|
+
button still feels like a picture of a button. 2% over 90ms — small enough
|
|
832
|
+
that it reads as pressure rather than as animation, and it is the
|
|
833
|
+
reference's own value (.auth-signin-btn:active). Inputs are excluded: a
|
|
834
|
+
text field does not depress, and scaling one mid-caret is a glitch. */
|
|
835
|
+
:where(button,[role=button],a[role=button],summary):active:not(:disabled){
|
|
836
|
+
transform:scale(.98);
|
|
693
837
|
}
|
|
694
838
|
|
|
695
839
|
/* ——— controls ———
|
|
@@ -702,11 +846,29 @@
|
|
|
702
846
|
:where(input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=color]),select,textarea){
|
|
703
847
|
background:var(--surface-2);
|
|
704
848
|
border:1px solid var(--border-control);
|
|
705
|
-
|
|
849
|
+
/* --radius-md (8px), not --radius-sm (6px). On a 36px control the two are
|
|
850
|
+
one step apart and it is the step between "boxy" and "considered";
|
|
851
|
+
--radius-sm keeps its job on genuinely small parts — badges, chips, menu
|
|
852
|
+
rows — where 8px would look bubbly. The reference draws its composer and
|
|
853
|
+
its buttons at 8-10px and nothing at 6px except 22px chips. */
|
|
854
|
+
border-radius:var(--radius-md);
|
|
706
855
|
padding:0 var(--space-3);
|
|
707
856
|
}
|
|
708
857
|
:where(textarea){padding:var(--space-2) var(--space-3);resize:vertical}
|
|
709
|
-
|
|
858
|
+
/* ——— focus, on a control ———
|
|
859
|
+
Not the generic ring. A field already HAS an edge, so focus brightens that
|
|
860
|
+
edge (.15 -> .22) and adds a soft halo just outside it — which is what the
|
|
861
|
+
reference does (.composer-box:focus-within) and what separates a focused
|
|
862
|
+
field from a browser default. The generic `outline` is suppressed here
|
|
863
|
+
precisely because it would draw a second, harder box around this one. */
|
|
864
|
+
:where(input,select,textarea):focus-visible{
|
|
865
|
+
outline:none;
|
|
866
|
+
border-color:var(--border-focus);
|
|
867
|
+
box-shadow:var(--ring-focus);
|
|
868
|
+
}
|
|
869
|
+
/* A hovered field lifts its surface a rung. Nudging its EDGE from .15 to .16
|
|
870
|
+
is a state nobody can see. */
|
|
871
|
+
:where(input,select,textarea):hover:not(:focus-visible):not(:disabled){background:var(--surface-3)}
|
|
710
872
|
:where(input,textarea)::placeholder{color:var(--text-disabled)}
|
|
711
873
|
:where(input,select,textarea,button):disabled{opacity:.5;cursor:not-allowed}
|
|
712
874
|
:where(button,[role=button],summary,label,select){cursor:pointer}
|
|
@@ -749,6 +911,29 @@
|
|
|
749
911
|
/* --white-20 is white-on-white in the light theme, so selection reads through
|
|
750
912
|
--selection, which BOTH themes define. */
|
|
751
913
|
::selection{background:var(--selection);color:var(--text-primary)}
|
|
914
|
+
|
|
915
|
+
/* ——— the scrollbar ———
|
|
916
|
+
The single largest piece of UNSTYLED chrome left on a dark surface: a
|
|
917
|
+
default 15px light-grey scrollbar down the side of a #0a0a0a panel is the
|
|
918
|
+
one element on the page that did not get the memo, and it is visible on
|
|
919
|
+
every scrolling surface in the product. 6px, no track, thumb on the same
|
|
920
|
+
hairline ladder as everything else. Both syntaxes ship because they are
|
|
921
|
+
disjoint — `scrollbar-*` is Firefox and the `::-webkit-*` pseudos are
|
|
922
|
+
Chromium/Safari; neither engine reads the other. */
|
|
923
|
+
The thumb is a --white-* rung, so it does not invert; the `.light`
|
|
924
|
+
restatements sit here rather than in colors.css because a scrollbar is an
|
|
925
|
+
element default and this file is where element defaults live. Everything
|
|
926
|
+
stays INSIDE the layer — an unlayered rule here would outrank an app's own
|
|
927
|
+
utilities, which is the defect check 1c exists to catch. */
|
|
928
|
+
:where(html){scrollbar-color:var(--white-15) transparent;scrollbar-width:thin}
|
|
929
|
+
.light{scrollbar-color:rgb(0 0 0 / .18) transparent}
|
|
930
|
+
:where(*)::-webkit-scrollbar{width:6px;height:6px}
|
|
931
|
+
:where(*)::-webkit-scrollbar-track{background:transparent}
|
|
932
|
+
:where(*)::-webkit-scrollbar-thumb{background:var(--white-15);border-radius:var(--radius-full)}
|
|
933
|
+
:where(*)::-webkit-scrollbar-thumb:hover{background:var(--white-30)}
|
|
934
|
+
:where(*)::-webkit-scrollbar-corner{background:transparent}
|
|
935
|
+
.light :where(*)::-webkit-scrollbar-thumb{background:rgb(0 0 0 / .18)}
|
|
936
|
+
.light :where(*)::-webkit-scrollbar-thumb:hover{background:rgb(0 0 0 / .32)}
|
|
752
937
|
}
|
|
753
938
|
|
|
754
939
|
|
package/tokens/base.css
CHANGED
|
@@ -63,7 +63,19 @@
|
|
|
63
63
|
background-color var(--duration-fast) var(--ease-out),
|
|
64
64
|
border-color var(--duration-fast) var(--ease-out),
|
|
65
65
|
box-shadow var(--duration-fast) var(--ease-out),
|
|
66
|
-
opacity var(--duration-fast) var(--ease-out)
|
|
66
|
+
opacity var(--duration-fast) var(--ease-out),
|
|
67
|
+
transform var(--duration-press) var(--ease-out);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* ——— the press ———
|
|
71
|
+
A button that moves under the finger is the cheapest "this is a real
|
|
72
|
+
control" signal there is, and its absence is most of why a correct-looking
|
|
73
|
+
button still feels like a picture of a button. 2% over 90ms — small enough
|
|
74
|
+
that it reads as pressure rather than as animation, and it is the
|
|
75
|
+
reference's own value (.auth-signin-btn:active). Inputs are excluded: a
|
|
76
|
+
text field does not depress, and scaling one mid-caret is a glitch. */
|
|
77
|
+
:where(button,[role=button],a[role=button],summary):active:not(:disabled){
|
|
78
|
+
transform:scale(.98);
|
|
67
79
|
}
|
|
68
80
|
|
|
69
81
|
/* ——— controls ———
|
|
@@ -76,11 +88,29 @@
|
|
|
76
88
|
:where(input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=color]),select,textarea){
|
|
77
89
|
background:var(--surface-2);
|
|
78
90
|
border:1px solid var(--border-control);
|
|
79
|
-
|
|
91
|
+
/* --radius-md (8px), not --radius-sm (6px). On a 36px control the two are
|
|
92
|
+
one step apart and it is the step between "boxy" and "considered";
|
|
93
|
+
--radius-sm keeps its job on genuinely small parts — badges, chips, menu
|
|
94
|
+
rows — where 8px would look bubbly. The reference draws its composer and
|
|
95
|
+
its buttons at 8-10px and nothing at 6px except 22px chips. */
|
|
96
|
+
border-radius:var(--radius-md);
|
|
80
97
|
padding:0 var(--space-3);
|
|
81
98
|
}
|
|
82
99
|
:where(textarea){padding:var(--space-2) var(--space-3);resize:vertical}
|
|
83
|
-
|
|
100
|
+
/* ——— focus, on a control ———
|
|
101
|
+
Not the generic ring. A field already HAS an edge, so focus brightens that
|
|
102
|
+
edge (.15 -> .22) and adds a soft halo just outside it — which is what the
|
|
103
|
+
reference does (.composer-box:focus-within) and what separates a focused
|
|
104
|
+
field from a browser default. The generic `outline` is suppressed here
|
|
105
|
+
precisely because it would draw a second, harder box around this one. */
|
|
106
|
+
:where(input,select,textarea):focus-visible{
|
|
107
|
+
outline:none;
|
|
108
|
+
border-color:var(--border-focus);
|
|
109
|
+
box-shadow:var(--ring-focus);
|
|
110
|
+
}
|
|
111
|
+
/* A hovered field lifts its surface a rung. Nudging its EDGE from .15 to .16
|
|
112
|
+
is a state nobody can see. */
|
|
113
|
+
:where(input,select,textarea):hover:not(:focus-visible):not(:disabled){background:var(--surface-3)}
|
|
84
114
|
:where(input,textarea)::placeholder{color:var(--text-disabled)}
|
|
85
115
|
:where(input,select,textarea,button):disabled{opacity:.5;cursor:not-allowed}
|
|
86
116
|
:where(button,[role=button],summary,label,select){cursor:pointer}
|
|
@@ -123,4 +153,27 @@
|
|
|
123
153
|
/* --white-20 is white-on-white in the light theme, so selection reads through
|
|
124
154
|
--selection, which BOTH themes define. */
|
|
125
155
|
::selection{background:var(--selection);color:var(--text-primary)}
|
|
156
|
+
|
|
157
|
+
/* ——— the scrollbar ———
|
|
158
|
+
The single largest piece of UNSTYLED chrome left on a dark surface: a
|
|
159
|
+
default 15px light-grey scrollbar down the side of a #0a0a0a panel is the
|
|
160
|
+
one element on the page that did not get the memo, and it is visible on
|
|
161
|
+
every scrolling surface in the product. 6px, no track, thumb on the same
|
|
162
|
+
hairline ladder as everything else. Both syntaxes ship because they are
|
|
163
|
+
disjoint — `scrollbar-*` is Firefox and the `::-webkit-*` pseudos are
|
|
164
|
+
Chromium/Safari; neither engine reads the other. */
|
|
165
|
+
The thumb is a --white-* rung, so it does not invert; the `.light`
|
|
166
|
+
restatements sit here rather than in colors.css because a scrollbar is an
|
|
167
|
+
element default and this file is where element defaults live. Everything
|
|
168
|
+
stays INSIDE the layer — an unlayered rule here would outrank an app's own
|
|
169
|
+
utilities, which is the defect check 1c exists to catch. */
|
|
170
|
+
:where(html){scrollbar-color:var(--white-15) transparent;scrollbar-width:thin}
|
|
171
|
+
.light{scrollbar-color:rgb(0 0 0 / .18) transparent}
|
|
172
|
+
:where(*)::-webkit-scrollbar{width:6px;height:6px}
|
|
173
|
+
:where(*)::-webkit-scrollbar-track{background:transparent}
|
|
174
|
+
:where(*)::-webkit-scrollbar-thumb{background:var(--white-15);border-radius:var(--radius-full)}
|
|
175
|
+
:where(*)::-webkit-scrollbar-thumb:hover{background:var(--white-30)}
|
|
176
|
+
:where(*)::-webkit-scrollbar-corner{background:transparent}
|
|
177
|
+
.light :where(*)::-webkit-scrollbar-thumb{background:rgb(0 0 0 / .18)}
|
|
178
|
+
.light :where(*)::-webkit-scrollbar-thumb:hover{background:rgb(0 0 0 / .32)}
|
|
126
179
|
}
|