@axiapps/axi-design 1.8.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/README.md CHANGED
@@ -83,6 +83,8 @@ not "what does the system look like". Everything else is
83
83
  | `--axi-bars-gap` | gap between columns in `.axi-bars` | `6px` | `<div class="axi-bars" style="--axi-bars-gap: 2px">` |
84
84
  | `--axi-plot-h` | height of a `.axi-plot` or `.axi-bars` | `180px` | `<div class="axi-plot" style="--axi-plot-h: 240px">` |
85
85
  | `--axi-plot-rows` | how many horizontal rules a `.axi-plot` draws | `4` | `<div class="axi-plot" style="--axi-plot-rows: 6">` |
86
+ | `--axi-tick-w` / `--axi-tick-h` | size of one `.axi-ticks__tick` | `5px` / `15px` | `<div class="axi-ticks" style="--axi-tick-w: 7px">` |
87
+ | `--axi-ticks-gap` | gap between marks in `.axi-ticks` | `3px` | `<div class="axi-ticks" style="--axi-ticks-gap: 2px">` |
86
88
 
87
89
  `--axi-page-pad: 0` is the one to know about: it is how a measure nested
88
90
  inside another measure avoids paying the gutter twice.
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
- .axi-select {
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
- .axi-select:hover {
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
@@ -1028,6 +1136,42 @@ a { color: inherit; }
1028
1136
  background: var(--axi-series, var(--axi-accent));
1029
1137
  }
1030
1138
 
1139
+ /* ---------- ticks ---------- */
1140
+ /* A run of yes/no along a timeline: attended or missed, passed or failed,
1141
+ shipped or skipped. This is rule 9 read the other way round - a bar has a
1142
+ length because a quantity has one, and "no" has no length at all. Drawn as
1143
+ a bar it has to be faked with a stub, and a stub reads as "a little bit",
1144
+ which is the same lie as a faded fill. So every event here is one mark of
1145
+ the same size, and what changes between them is only which ink it is drawn
1146
+ in: rule 10's two series, the accent for the fact and the neutral ramp for
1147
+ its absence.
1148
+
1149
+ Neither mark is outlined, which is the one place this departs from the
1150
+ filled/empty pair the language reaches for first. At the size a run of
1151
+ twenty sits in a table row the outline IS the mark - three pixels of edge
1152
+ around one pixel of middle - so the pair would differ in nothing the eye
1153
+ can resolve. A strip drawn large enough to outline is a bar chart, and
1154
+ .axi-bars already is one.
1155
+
1156
+ Marks are a fixed width and never flex: a run of fourteen and a run of
1157
+ three share a column in a table, and the short one has to read as a short
1158
+ run rather than as fourteen fatter events. */
1159
+ .axi-ticks {
1160
+ display: inline-flex;
1161
+ align-items: stretch;
1162
+ gap: var(--axi-ticks-gap, 3px);
1163
+ height: var(--axi-tick-h, 15px);
1164
+ }
1165
+ .axi-ticks__tick {
1166
+ flex: none;
1167
+ width: var(--axi-tick-w, 5px);
1168
+ background: var(--axi-rule);
1169
+ border-radius: var(--axi-radius-sm);
1170
+ }
1171
+ .axi-ticks__tick--on {
1172
+ background: var(--axi-series, var(--axi-accent));
1173
+ }
1174
+
1031
1175
  /* ---------- plot ---------- */
1032
1176
  /* A frame for a line or an area, with its horizontal rules drawn in. The
1033
1177
  rules are hard stops in a repeating gradient, which is the select caret's
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
@@ -247,6 +258,12 @@ is the one chart type that cannot be built without the thing rule 2 forbids, so
247
258
  a distribution is drawn as bars, or as a table sorted by the value, or not at
248
259
  all.
249
260
 
261
+ The other corollary is that a thing with no quantity gets no length. A run of
262
+ yes/no — attended or missed, passed or failed — is a sequence of facts, and a
263
+ fact has no magnitude to draw: rendering "no" as a short bar says "a little
264
+ bit" as loudly as a faded fill says "30%". That series is a row of marks of
265
+ one size, differing only in ink, which is `.axi-ticks`.
266
+
250
267
  ## 10. A chart's ink is the accent
251
268
 
252
269
  One series is the accent. A second, for comparison, is the neutral ramp —