@axiapps/axi-design 1.9.0 → 1.10.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/dist/axi.css +112 -4
- package/docs/RULES.md +11 -0
- package/package.json +1 -1
- package/src/base.css +2 -0
- package/src/primitives.css +110 -4
package/dist/axi.css
CHANGED
|
@@ -157,6 +157,8 @@ a { color: inherit; }
|
|
|
157
157
|
.axi-pill:hover,
|
|
158
158
|
.axi-pill[aria-pressed="true"]:hover,
|
|
159
159
|
.axi-select:hover,
|
|
160
|
+
.axi-picker__btn:hover,
|
|
161
|
+
.axi-picker__btn[aria-expanded="true"],
|
|
160
162
|
.axi-card:hover,
|
|
161
163
|
.axi-drawer__close:hover {
|
|
162
164
|
transform: none !important;
|
|
@@ -400,8 +402,11 @@ a { color: inherit; }
|
|
|
400
402
|
/* ---------- select ---------- */
|
|
401
403
|
/* The closed box is ours everywhere: strip the native control and draw the
|
|
402
404
|
caret, so a select sits alongside the other controls as just another
|
|
403
|
-
outlined chip instead of announcing the OS.
|
|
404
|
-
|
|
405
|
+
outlined chip instead of announcing the OS. .axi-picker__btn is the same
|
|
406
|
+
box worn by a button instead of a <select>; the two share every
|
|
407
|
+
declaration here so a dropdown reads the same whichever half opens it. */
|
|
408
|
+
.axi-select,
|
|
409
|
+
.axi-picker__btn {
|
|
405
410
|
appearance: none;
|
|
406
411
|
padding: 10px 30px 10px 9px;
|
|
407
412
|
border: var(--axi-border-control) solid var(--axi-ink-line);
|
|
@@ -422,7 +427,13 @@ a { color: inherit; }
|
|
|
422
427
|
background-repeat: no-repeat;
|
|
423
428
|
transition: transform .1s, box-shadow .1s;
|
|
424
429
|
}
|
|
425
|
-
.
|
|
430
|
+
/* The open trigger keeps the lift. Rule 4 gives the raise to a pointer, but a
|
|
431
|
+
disclosure that drops back flat the moment the pointer moves into the list
|
|
432
|
+
it opened severs the two: the lift is what says this box and that popover
|
|
433
|
+
are one control. */
|
|
434
|
+
.axi-select:hover,
|
|
435
|
+
.axi-picker__btn:hover,
|
|
436
|
+
.axi-picker__btn[aria-expanded='true'] {
|
|
426
437
|
color: var(--axi-text);
|
|
427
438
|
box-shadow: var(--axi-offset-control) var(--axi-offset-control) 0 var(--axi-ink-line);
|
|
428
439
|
transform: translate(-2px, -2px);
|
|
@@ -431,7 +442,9 @@ a { color: inherit; }
|
|
|
431
442
|
/* The popup stays OS chrome until a browser lets us style it. Where one does
|
|
432
443
|
(Chromium's base-select), the list is drawn with the same outline and offset
|
|
433
444
|
block as our own popovers, so both dropdown kinds read as one family; where
|
|
434
|
-
it does not, the closed box above is still ours and the list is native
|
|
445
|
+
it does not, the closed box above is still ours and the list is native -
|
|
446
|
+
and if that native list is not good enough, .axi-picker below draws the
|
|
447
|
+
whole thing. */
|
|
435
448
|
@supports (appearance: base-select) {
|
|
436
449
|
/* base-select draws its own ::picker-icon, so the hand-drawn caret above
|
|
437
450
|
would be a second arrow. */
|
|
@@ -466,6 +479,101 @@ a { color: inherit; }
|
|
|
466
479
|
.axi-select option::checkmark { content: "\2713"; color: var(--axi-accent); font-weight: 900; }
|
|
467
480
|
}
|
|
468
481
|
|
|
482
|
+
/* ---------- picker ---------- */
|
|
483
|
+
/* The select's other half. Above, the native popup is left as OS chrome
|
|
484
|
+
wherever `appearance: base-select` is missing - and that is most places
|
|
485
|
+
today, including every Electron built on a Chromium older than the
|
|
486
|
+
property. What lands there is a raised list the language cannot reach: no
|
|
487
|
+
ink outline, no offset block, its own selection colour instead of the
|
|
488
|
+
accent. That is rule 3 broken by a box we do not own, so the fix is to stop
|
|
489
|
+
asking the OS to draw it. .axi-picker is a button and a popover wearing the
|
|
490
|
+
closed box and the list styling from the @supports branch above, so the two
|
|
491
|
+
kinds are the same dropdown and a consumer picks by what the platform has.
|
|
492
|
+
|
|
493
|
+
Prefer the native <select> where it works: it comes with keyboard handling,
|
|
494
|
+
typeahead, and a popup that can escape the window. This is what you use
|
|
495
|
+
when it does not.
|
|
496
|
+
|
|
497
|
+
Markup contract - the listbox pattern, and the popover carries the panel
|
|
498
|
+
weight because it is a raised surface, not a control:
|
|
499
|
+
|
|
500
|
+
<div class="axi-picker">
|
|
501
|
+
<button class="axi-picker__btn" aria-haspopup="listbox"
|
|
502
|
+
aria-expanded="false" aria-controls="months">Jul 2026</button>
|
|
503
|
+
<div class="axi-picker__pop" id="months" role="listbox" hidden>
|
|
504
|
+
<button class="axi-picker__opt" role="option" aria-selected="true">
|
|
505
|
+
Jul 2026
|
|
506
|
+
</button>
|
|
507
|
+
</div>
|
|
508
|
+
</div>
|
|
509
|
+
|
|
510
|
+
Like the menu and the tooltip, the language ships no script: `hidden`,
|
|
511
|
+
`aria-expanded` and `aria-selected` are the whole state, and the consumer
|
|
512
|
+
toggles them. gallery.js is the reference wiring, arrow keys and Escape
|
|
513
|
+
included. */
|
|
514
|
+
.axi-picker { position: relative; display: inline-flex; }
|
|
515
|
+
.axi-picker__btn {
|
|
516
|
+
display: inline-flex; align-items: center;
|
|
517
|
+
width: 100%;
|
|
518
|
+
text-align: left;
|
|
519
|
+
}
|
|
520
|
+
.axi-picker__pop {
|
|
521
|
+
/* Above .axi-mast's z-index: 40, for the same reason .axi-menu__pop is. */
|
|
522
|
+
position: absolute; top: calc(100% + 9px); left: 0; z-index: 41;
|
|
523
|
+
/* Never narrower than the box it came out of, and no wider than its
|
|
524
|
+
longest row needs. A list that shrinks to the text is a list that has
|
|
525
|
+
moved, and the eye has to find the column again. */
|
|
526
|
+
min-width: 100%;
|
|
527
|
+
max-height: 340px; overflow-y: auto;
|
|
528
|
+
padding: 6px;
|
|
529
|
+
background: var(--axi-surface-raised);
|
|
530
|
+
border: var(--axi-border-panel) solid var(--axi-ink-line);
|
|
531
|
+
border-radius: var(--axi-radius);
|
|
532
|
+
/* The block falls outside the popover's own box. An ancestor that clips -
|
|
533
|
+
a scrolling pane, a panel with overflow: hidden - eats it, and the only
|
|
534
|
+
shadow on the screen is the one that goes missing. */
|
|
535
|
+
box-shadow: var(--axi-offset-panel) var(--axi-offset-panel) 0 var(--axi-ink-line);
|
|
536
|
+
}
|
|
537
|
+
.axi-picker__pop[hidden] { display: none; }
|
|
538
|
+
/* A popover cannot always live beside its trigger. Inside a pane that scrolls
|
|
539
|
+
or a panel that clips, `position: absolute` puts the list where the
|
|
540
|
+
overflow can eat it - and the offset block falls outside the popover's box,
|
|
541
|
+
so the block is the first thing to go. The way out is the tooltip's
|
|
542
|
+
contract: append the popover to <body> and set left/top from script, having
|
|
543
|
+
measured the trigger. The box is unchanged; only who positions it moves. */
|
|
544
|
+
.axi-picker__pop--fixed { position: fixed; }
|
|
545
|
+
.axi-picker__opt {
|
|
546
|
+
display: flex; align-items: center; gap: 9px;
|
|
547
|
+
width: 100%;
|
|
548
|
+
padding: 8px 9px;
|
|
549
|
+
border: 0;
|
|
550
|
+
border-radius: var(--axi-radius-sm);
|
|
551
|
+
background: transparent;
|
|
552
|
+
color: var(--axi-text-dim);
|
|
553
|
+
font: var(--axi-t-label);
|
|
554
|
+
font-size: 12.5px;
|
|
555
|
+
letter-spacing: var(--axi-ls-label);
|
|
556
|
+
text-transform: uppercase;
|
|
557
|
+
text-align: left;
|
|
558
|
+
cursor: pointer;
|
|
559
|
+
}
|
|
560
|
+
/* The tick is in every row, inked only in the chosen one. Give it to the
|
|
561
|
+
selected row alone and every label shifts by its width as the choice moves,
|
|
562
|
+
which turns picking an option into the list twitching. */
|
|
563
|
+
.axi-picker__opt::before {
|
|
564
|
+
content: "\2713";
|
|
565
|
+
color: transparent;
|
|
566
|
+
font-weight: 900;
|
|
567
|
+
}
|
|
568
|
+
.axi-picker__opt:hover, .axi-picker__opt:focus {
|
|
569
|
+
background: var(--axi-ground); color: var(--axi-text);
|
|
570
|
+
}
|
|
571
|
+
/* The page-wide focus ring sits 2px outside its element; inside a popover
|
|
572
|
+
this tight that is 2px into the neighbouring row, so pull it back in. */
|
|
573
|
+
.axi-picker__opt:focus-visible { outline-offset: -3px; }
|
|
574
|
+
.axi-picker__opt[aria-selected='true'] { color: var(--axi-text); }
|
|
575
|
+
.axi-picker__opt[aria-selected='true']::before { color: var(--axi-accent); }
|
|
576
|
+
|
|
469
577
|
/* --- layout.css --- */
|
|
470
578
|
/* axi design language - layout.
|
|
471
579
|
Three measures, one grid, two spacing helpers. Deliberately small: a
|
package/docs/RULES.md
CHANGED
|
@@ -111,6 +111,17 @@ read `--axi-radius`, everything control-sized reads `--axi-radius-sm`, and
|
|
|
111
111
|
`tests/tokens.test.mjs` fails on a literal radius in a component file the same
|
|
112
112
|
way it fails on a literal border weight.
|
|
113
113
|
|
|
114
|
+
**A box the OS draws is a box that breaks this.** A native `<select>` popup is
|
|
115
|
+
a raised list the language cannot reach: no ink outline, no offset block, and
|
|
116
|
+
its own selection colour where the accent belongs. `.axi-select` styles the
|
|
117
|
+
closed box and hands the list to `appearance: base-select` where the browser
|
|
118
|
+
has it — but most do not yet, and an Electron app is pinned to whatever
|
|
119
|
+
Chromium its version shipped. `.axi-picker` is the way out: the same closed
|
|
120
|
+
box on a button, and the list drawn as a popover that takes the panel weight
|
|
121
|
+
like any other raised surface. Reach for the native select first, because it
|
|
122
|
+
brings keyboard handling and a popup that can leave the window; reach for the
|
|
123
|
+
picker when the popup it opens is not one this rule can touch.
|
|
124
|
+
|
|
114
125
|
## 4. Hover lifts
|
|
115
126
|
|
|
116
127
|
The lift is per form step, not one universal number: a control has no resting
|
package/package.json
CHANGED
package/src/base.css
CHANGED
package/src/primitives.css
CHANGED
|
@@ -234,8 +234,11 @@
|
|
|
234
234
|
/* ---------- select ---------- */
|
|
235
235
|
/* The closed box is ours everywhere: strip the native control and draw the
|
|
236
236
|
caret, so a select sits alongside the other controls as just another
|
|
237
|
-
outlined chip instead of announcing the OS.
|
|
238
|
-
|
|
237
|
+
outlined chip instead of announcing the OS. .axi-picker__btn is the same
|
|
238
|
+
box worn by a button instead of a <select>; the two share every
|
|
239
|
+
declaration here so a dropdown reads the same whichever half opens it. */
|
|
240
|
+
.axi-select,
|
|
241
|
+
.axi-picker__btn {
|
|
239
242
|
appearance: none;
|
|
240
243
|
padding: 10px 30px 10px 9px;
|
|
241
244
|
border: var(--axi-border-control) solid var(--axi-ink-line);
|
|
@@ -256,7 +259,13 @@
|
|
|
256
259
|
background-repeat: no-repeat;
|
|
257
260
|
transition: transform .1s, box-shadow .1s;
|
|
258
261
|
}
|
|
259
|
-
.
|
|
262
|
+
/* The open trigger keeps the lift. Rule 4 gives the raise to a pointer, but a
|
|
263
|
+
disclosure that drops back flat the moment the pointer moves into the list
|
|
264
|
+
it opened severs the two: the lift is what says this box and that popover
|
|
265
|
+
are one control. */
|
|
266
|
+
.axi-select:hover,
|
|
267
|
+
.axi-picker__btn:hover,
|
|
268
|
+
.axi-picker__btn[aria-expanded='true'] {
|
|
260
269
|
color: var(--axi-text);
|
|
261
270
|
box-shadow: var(--axi-offset-control) var(--axi-offset-control) 0 var(--axi-ink-line);
|
|
262
271
|
transform: translate(-2px, -2px);
|
|
@@ -265,7 +274,9 @@
|
|
|
265
274
|
/* The popup stays OS chrome until a browser lets us style it. Where one does
|
|
266
275
|
(Chromium's base-select), the list is drawn with the same outline and offset
|
|
267
276
|
block as our own popovers, so both dropdown kinds read as one family; where
|
|
268
|
-
it does not, the closed box above is still ours and the list is native
|
|
277
|
+
it does not, the closed box above is still ours and the list is native -
|
|
278
|
+
and if that native list is not good enough, .axi-picker below draws the
|
|
279
|
+
whole thing. */
|
|
269
280
|
@supports (appearance: base-select) {
|
|
270
281
|
/* base-select draws its own ::picker-icon, so the hand-drawn caret above
|
|
271
282
|
would be a second arrow. */
|
|
@@ -299,3 +310,98 @@
|
|
|
299
310
|
.axi-select option:checked { color: var(--axi-text); }
|
|
300
311
|
.axi-select option::checkmark { content: "\2713"; color: var(--axi-accent); font-weight: 900; }
|
|
301
312
|
}
|
|
313
|
+
|
|
314
|
+
/* ---------- picker ---------- */
|
|
315
|
+
/* The select's other half. Above, the native popup is left as OS chrome
|
|
316
|
+
wherever `appearance: base-select` is missing - and that is most places
|
|
317
|
+
today, including every Electron built on a Chromium older than the
|
|
318
|
+
property. What lands there is a raised list the language cannot reach: no
|
|
319
|
+
ink outline, no offset block, its own selection colour instead of the
|
|
320
|
+
accent. That is rule 3 broken by a box we do not own, so the fix is to stop
|
|
321
|
+
asking the OS to draw it. .axi-picker is a button and a popover wearing the
|
|
322
|
+
closed box and the list styling from the @supports branch above, so the two
|
|
323
|
+
kinds are the same dropdown and a consumer picks by what the platform has.
|
|
324
|
+
|
|
325
|
+
Prefer the native <select> where it works: it comes with keyboard handling,
|
|
326
|
+
typeahead, and a popup that can escape the window. This is what you use
|
|
327
|
+
when it does not.
|
|
328
|
+
|
|
329
|
+
Markup contract - the listbox pattern, and the popover carries the panel
|
|
330
|
+
weight because it is a raised surface, not a control:
|
|
331
|
+
|
|
332
|
+
<div class="axi-picker">
|
|
333
|
+
<button class="axi-picker__btn" aria-haspopup="listbox"
|
|
334
|
+
aria-expanded="false" aria-controls="months">Jul 2026</button>
|
|
335
|
+
<div class="axi-picker__pop" id="months" role="listbox" hidden>
|
|
336
|
+
<button class="axi-picker__opt" role="option" aria-selected="true">
|
|
337
|
+
Jul 2026
|
|
338
|
+
</button>
|
|
339
|
+
</div>
|
|
340
|
+
</div>
|
|
341
|
+
|
|
342
|
+
Like the menu and the tooltip, the language ships no script: `hidden`,
|
|
343
|
+
`aria-expanded` and `aria-selected` are the whole state, and the consumer
|
|
344
|
+
toggles them. gallery.js is the reference wiring, arrow keys and Escape
|
|
345
|
+
included. */
|
|
346
|
+
.axi-picker { position: relative; display: inline-flex; }
|
|
347
|
+
.axi-picker__btn {
|
|
348
|
+
display: inline-flex; align-items: center;
|
|
349
|
+
width: 100%;
|
|
350
|
+
text-align: left;
|
|
351
|
+
}
|
|
352
|
+
.axi-picker__pop {
|
|
353
|
+
/* Above .axi-mast's z-index: 40, for the same reason .axi-menu__pop is. */
|
|
354
|
+
position: absolute; top: calc(100% + 9px); left: 0; z-index: 41;
|
|
355
|
+
/* Never narrower than the box it came out of, and no wider than its
|
|
356
|
+
longest row needs. A list that shrinks to the text is a list that has
|
|
357
|
+
moved, and the eye has to find the column again. */
|
|
358
|
+
min-width: 100%;
|
|
359
|
+
max-height: 340px; overflow-y: auto;
|
|
360
|
+
padding: 6px;
|
|
361
|
+
background: var(--axi-surface-raised);
|
|
362
|
+
border: var(--axi-border-panel) solid var(--axi-ink-line);
|
|
363
|
+
border-radius: var(--axi-radius);
|
|
364
|
+
/* The block falls outside the popover's own box. An ancestor that clips -
|
|
365
|
+
a scrolling pane, a panel with overflow: hidden - eats it, and the only
|
|
366
|
+
shadow on the screen is the one that goes missing. */
|
|
367
|
+
box-shadow: var(--axi-offset-panel) var(--axi-offset-panel) 0 var(--axi-ink-line);
|
|
368
|
+
}
|
|
369
|
+
.axi-picker__pop[hidden] { display: none; }
|
|
370
|
+
/* A popover cannot always live beside its trigger. Inside a pane that scrolls
|
|
371
|
+
or a panel that clips, `position: absolute` puts the list where the
|
|
372
|
+
overflow can eat it - and the offset block falls outside the popover's box,
|
|
373
|
+
so the block is the first thing to go. The way out is the tooltip's
|
|
374
|
+
contract: append the popover to <body> and set left/top from script, having
|
|
375
|
+
measured the trigger. The box is unchanged; only who positions it moves. */
|
|
376
|
+
.axi-picker__pop--fixed { position: fixed; }
|
|
377
|
+
.axi-picker__opt {
|
|
378
|
+
display: flex; align-items: center; gap: 9px;
|
|
379
|
+
width: 100%;
|
|
380
|
+
padding: 8px 9px;
|
|
381
|
+
border: 0;
|
|
382
|
+
border-radius: var(--axi-radius-sm);
|
|
383
|
+
background: transparent;
|
|
384
|
+
color: var(--axi-text-dim);
|
|
385
|
+
font: var(--axi-t-label);
|
|
386
|
+
font-size: 12.5px;
|
|
387
|
+
letter-spacing: var(--axi-ls-label);
|
|
388
|
+
text-transform: uppercase;
|
|
389
|
+
text-align: left;
|
|
390
|
+
cursor: pointer;
|
|
391
|
+
}
|
|
392
|
+
/* The tick is in every row, inked only in the chosen one. Give it to the
|
|
393
|
+
selected row alone and every label shifts by its width as the choice moves,
|
|
394
|
+
which turns picking an option into the list twitching. */
|
|
395
|
+
.axi-picker__opt::before {
|
|
396
|
+
content: "\2713";
|
|
397
|
+
color: transparent;
|
|
398
|
+
font-weight: 900;
|
|
399
|
+
}
|
|
400
|
+
.axi-picker__opt:hover, .axi-picker__opt:focus {
|
|
401
|
+
background: var(--axi-ground); color: var(--axi-text);
|
|
402
|
+
}
|
|
403
|
+
/* The page-wide focus ring sits 2px outside its element; inside a popover
|
|
404
|
+
this tight that is 2px into the neighbouring row, so pull it back in. */
|
|
405
|
+
.axi-picker__opt:focus-visible { outline-offset: -3px; }
|
|
406
|
+
.axi-picker__opt[aria-selected='true'] { color: var(--axi-text); }
|
|
407
|
+
.axi-picker__opt[aria-selected='true']::before { color: var(--axi-accent); }
|