@axiapps/axi-design 1.42.0 → 1.44.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 +6 -0
- package/accents.json +2 -1
- package/dist/accents.css +1 -0
- package/dist/axi.css +542 -46
- package/dist/themes/flat.css +1 -0
- package/docs/RULES.md +214 -5
- package/package.json +1 -1
- package/src/data.css +204 -2
- package/src/layout.css +68 -0
- package/src/primitives.css +101 -8
- package/src/shells.css +107 -29
- package/src/utilities.css +62 -7
package/dist/themes/flat.css
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
--axi-text: #e8eaed;
|
|
17
17
|
--axi-text-dim: #8b929e;
|
|
18
18
|
--axi-text-faint: #5d6577;
|
|
19
|
+
--axi-scrim: rgba(4, 5, 8, .72);
|
|
19
20
|
--axi-shadow-panel: 0 2px 8px rgba(0, 0, 0, .4), 0 1px 3px rgba(0, 0, 0, .3), inset 0 1px 0 rgba(255, 255, 255, .05);
|
|
20
21
|
--axi-shadow-control: 0 1px 4px rgba(0, 0, 0, .3), inset 0 1px 0 rgba(255, 255, 255, .045);
|
|
21
22
|
--axi-shadow-panel-hover: 0 6px 18px rgba(0, 0, 0, .5), 0 2px 5px rgba(0, 0, 0, .35), inset 0 1px 0 rgba(255, 255, 255, .07);
|
package/docs/RULES.md
CHANGED
|
@@ -222,6 +222,31 @@ Every lift is turned off under `@media (prefers-reduced-motion: reduce)`, in
|
|
|
222
222
|
diamond still rotates, because a rotation that never changes is geometry and
|
|
223
223
|
not motion.
|
|
224
224
|
|
|
225
|
+
### A hover reveals no control
|
|
226
|
+
|
|
227
|
+
A control that exists only while the cursor is over it exists for nobody on a
|
|
228
|
+
touch screen, for nobody using a keyboard, and for nobody who has not already
|
|
229
|
+
found it. Three sites in one consumer had this shape: an open-link glyph at
|
|
230
|
+
`opacity: 0` until its card was hovered, and two help tips faded in the same
|
|
231
|
+
way. The glyph was the only way off the card. On a phone it was not there.
|
|
232
|
+
|
|
233
|
+
So: **a hover changes an element's depth, and may brighten its ink, and never
|
|
234
|
+
its presence.** What is on screen is on screen. If a control is worth having it
|
|
235
|
+
is drawn at rest, and if it is not worth drawing at rest it is not worth having.
|
|
236
|
+
|
|
237
|
+
The one thing a hover may bring in is an annotation of something already
|
|
238
|
+
present — a `.axi-tooltip` naming what a glyph does — and only because the
|
|
239
|
+
same tip answers focus. A tip that answers hover alone is a control-shaped
|
|
240
|
+
hole for everyone the first paragraph names.
|
|
241
|
+
|
|
242
|
+
The other half of this is a **dimmed** thing, which is what a consumer reaches
|
|
243
|
+
for when one series in a legend is isolated and the rest should recede.
|
|
244
|
+
`opacity: .3` on the rest is rule 2's faded ink, and it is what left those
|
|
245
|
+
keys unreadable. Receding is a step down the neutral ramp — `--axi-text-faint`
|
|
246
|
+
— which stays legible at every step, and a hover brings a receded key back to
|
|
247
|
+
plain so it can be found again. Nothing in the language fades, and this is one
|
|
248
|
+
more place that holds.
|
|
249
|
+
|
|
225
250
|
## 5. Filled means status, outlined means annotation
|
|
226
251
|
|
|
227
252
|
A filled chip asserts a value about the thing. An outlined chip in the cool ink
|
|
@@ -323,6 +348,21 @@ a colour that is a fill's contrast pair — `.axi-btn--primary`'s accent ink —
|
|
|
323
348
|
too, because an ink there would put a status colour on the accent block and cost
|
|
324
349
|
the label its legibility, which is rule 5's reason for the chip.
|
|
325
350
|
|
|
351
|
+
Applying this everywhere it was owed had one consequence worth naming, because
|
|
352
|
+
it is the rule arriving rather than a regression: hovering the **current**
|
|
353
|
+
breadcrumb used to turn it accent, and now it does not move. `[aria-current]`
|
|
354
|
+
outweighs a wrapped hover, which is the rail's stated refusal — the current item
|
|
355
|
+
does not brighten further under the cursor — reaching the one component that had
|
|
356
|
+
been disagreeing with it by accident of specificity.
|
|
357
|
+
|
|
358
|
+
Two things about *checking* this, both learned by getting them wrong. Weigh one
|
|
359
|
+
compound, never a selector list: a rule that lists two wrapped hovers beside a
|
|
360
|
+
state weighs as the state if you measure the list, and reports the two correct
|
|
361
|
+
hovers as offenders. And compare a hover only against a resting rule that
|
|
362
|
+
matches the **same element**: an `<a>` inside `.axi-prose` takes the container's
|
|
363
|
+
colour by inheritance, which no specificity can lose to, so measuring the link's
|
|
364
|
+
hover against the container's rule asks a question neither rule is answering.
|
|
365
|
+
|
|
326
366
|
## 7. The diamond is the family motif
|
|
327
367
|
|
|
328
368
|
A 45°-rotated outlined square. Bullet, status dot, language marker, and scaled
|
|
@@ -404,6 +444,51 @@ fact has no magnitude to draw: rendering "no" as a short bar says "a little
|
|
|
404
444
|
bit" as loudly as a faded fill says "30%". That series is a row of marks of
|
|
405
445
|
one size, differing only in ink, which is `.axi-ticks`.
|
|
406
446
|
|
|
447
|
+
### The matrix is the bound on "not at all"
|
|
448
|
+
|
|
449
|
+
The first corollary says this language does not draw a heatmap, and as written
|
|
450
|
+
that is too wide. It is true of a *distribution* — one series along one
|
|
451
|
+
categorical axis — because there a bar is always available, and reaching for
|
|
452
|
+
tint over length is choosing the illegible encoding when the legible one was
|
|
453
|
+
free.
|
|
454
|
+
|
|
455
|
+
A matrix is not that shape. Two categorical axes, both of them orderings the
|
|
456
|
+
reader navigates by — forty players down the side, sixty five-second buckets
|
|
457
|
+
across — and a quantity at each intersection. The plane is spent. There is no
|
|
458
|
+
third dimension left to give a length to, a bar per cell is 2400 bars four
|
|
459
|
+
pixels wide, and neither axis can be re-sorted by the value because both are
|
|
460
|
+
already sorted by something the reader needs: down the side by subgroup, across
|
|
461
|
+
by time. "Or not at all" would mean the shape is undrawable, and it is the only
|
|
462
|
+
shape that answers *who was doing this, and when*.
|
|
463
|
+
|
|
464
|
+
So a matrix may encode its quantity as intensity, under one condition — and the
|
|
465
|
+
condition is rule 9's own argument rather than an exemption from it. Rule 9 does
|
|
466
|
+
not object to intensity. It objects to intensity being the ONLY copy of the
|
|
467
|
+
number, and the illegible one. **A matrix cell prints its value.** The digit is
|
|
468
|
+
the legible copy, the band is what lets the eye find the shape without reading
|
|
469
|
+
two thousand numbers one at a time, and a reader who wants a figure reads the
|
|
470
|
+
figure. A matrix cell with no number in it is a heatmap, and for a heatmap the
|
|
471
|
+
corollary stands exactly as written.
|
|
472
|
+
|
|
473
|
+
Two further bounds, both of them rules already here rather than new ones.
|
|
474
|
+
|
|
475
|
+
The steps are **discrete and opaque**. A continuous alpha ramp of the accent
|
|
476
|
+
over the field is a faded accent, which is rule 2 — and it fails on its own
|
|
477
|
+
terms as well, because the cells a reader scans for are the low ones and those
|
|
478
|
+
are the ones a ramp makes hardest to see. Four steps, each a `color-mix()` of
|
|
479
|
+
the accent into `--axi-surface-paint`, so every band is a computed opaque
|
|
480
|
+
colour. The flat companion and not `--axi-surface`, because a surface token is
|
|
481
|
+
allowed to hold a gradient and `color-mix()` takes colours only; get that wrong
|
|
482
|
+
and the bands do not fade, they vanish.
|
|
483
|
+
|
|
484
|
+
And the band is the cell's **fill**, which settles what a row state may do to
|
|
485
|
+
it. Hover and selection raise a row by filling its cells, and a cell whose fill
|
|
486
|
+
is the data has no room for that — so on any table, a cell carrying a value in
|
|
487
|
+
its fill keeps it, and the row state is drawn by the leading edge and by every
|
|
488
|
+
cell that has nothing to say. That is not a concession to the matrix: it is the
|
|
489
|
+
edge-not-fill answer rule 8's selection already gives, arriving a second time
|
|
490
|
+
for the same reason.
|
|
491
|
+
|
|
407
492
|
## 10. A chart's ink is the accent
|
|
408
493
|
|
|
409
494
|
One series is the accent. A second, for comparison, is the neutral ramp —
|
|
@@ -501,6 +586,48 @@ that must be rejected, one per prohibition. The rule above is therefore a check
|
|
|
501
586
|
rather than a promise — the same treatment rule 3's weights get in
|
|
502
587
|
`tests/tokens.test.mjs`.
|
|
503
588
|
|
|
589
|
+
## 13. A state is an attribute, and the appearance follows it
|
|
590
|
+
|
|
591
|
+
Every state this language draws is keyed off an attribute the element already
|
|
592
|
+
carries, never off a class invented to describe the look. `[aria-current]` on a
|
|
593
|
+
rail item, a tab, a table row and a picked panel. `[aria-pressed="true"]` on a
|
|
594
|
+
pill. `[aria-selected="true"]` on a listbox option. `[aria-disabled="true"]`
|
|
595
|
+
beside `:disabled`, in 26 places. `[aria-sort]` on the sorted column,
|
|
596
|
+
`[aria-expanded]` on the thing that opens. One spelling per semantics, and the
|
|
597
|
+
semantics decides which — not the appearance, which is why a rail item and a
|
|
598
|
+
picked panel share `[aria-current]` while looking nothing alike.
|
|
599
|
+
|
|
600
|
+
The reason is not tidiness. **A state that exists only as an appearance is not a
|
|
601
|
+
state.** A consumer that marks the chosen card with a class has drawn a mark
|
|
602
|
+
sighted users can see and told everyone else nothing, and no amount of styling
|
|
603
|
+
fixes it from our side — the information was never in the document. Keying the
|
|
604
|
+
style off the attribute makes the two inseparable: you cannot get the look
|
|
605
|
+
without emitting the state, and you cannot emit the state and fail to get the
|
|
606
|
+
look.
|
|
607
|
+
|
|
608
|
+
This is also what stops the language growing a second vocabulary. An invented
|
|
609
|
+
`--selected` modifier would be a synonym for `[aria-current]` that a screen
|
|
610
|
+
reader cannot read, and the two would drift the first time one of them got a
|
|
611
|
+
tweak. There is no `.axi-panel--selected` for the same reason there is no
|
|
612
|
+
`.axi-btn--off`.
|
|
613
|
+
|
|
614
|
+
Two consequences when adding a component:
|
|
615
|
+
|
|
616
|
+
- **Find the attribute before writing the rule.** If the state the component
|
|
617
|
+
needs already has an ARIA spelling, use it, even if the look is unlike every
|
|
618
|
+
other user of that attribute. If it genuinely has none, that is the moment to
|
|
619
|
+
ask whether the state is real.
|
|
620
|
+
- **A state the markup holds needs no attribute at all.** A `<label>` wrapping
|
|
621
|
+
its own radio is the correct markup for a picker; the input holds the state,
|
|
622
|
+
so the label has nothing to set, and copying it onto the label would be a
|
|
623
|
+
second source of truth that can disagree with the first. That case is matched
|
|
624
|
+
structurally — `:has(> input:checked)`, the language's only `:has()`, with the
|
|
625
|
+
child combinator load-bearing: a descendant match would fire on any checkbox
|
|
626
|
+
buried in the component's content.
|
|
627
|
+
|
|
628
|
+
`tests/tokens.test.mjs` holds this rule to the components that carry it, so it
|
|
629
|
+
is a check rather than a promise.
|
|
630
|
+
|
|
504
631
|
## Tokens
|
|
505
632
|
|
|
506
633
|
Three layers, in `src/tokens.css` — the only file permitted to contain a colour
|
|
@@ -548,6 +675,7 @@ instead of joining it.
|
|
|
548
675
|
| Table corner | 3 | where the two cross |
|
|
549
676
|
| Sticky chrome | 40 | `.axi-mast` |
|
|
550
677
|
| Popovers | 41 | `.axi-menu__pop`, `.axi-picker__pop` |
|
|
678
|
+
| Sheet scrim | 44 | `.axi-scrim--sheet` |
|
|
551
679
|
| Sheet | 45 | `.axi-sheet` |
|
|
552
680
|
| Scrim | 50 | `.axi-scrim` |
|
|
553
681
|
| Drawer | 51 | `.axi-drawer` |
|
|
@@ -572,6 +700,15 @@ A negative `z-index` inside a component's own `isolation` context — the sigil'
|
|
|
572
700
|
backing shape — is not a layer and is not listed. It is invisible outside the
|
|
573
701
|
component that owns it.
|
|
574
702
|
|
|
703
|
+
The scrim appears twice, and that is the table saying something rather than
|
|
704
|
+
repeating itself. A scrim's rung is not a property of the scrim; it is "directly
|
|
705
|
+
below the thing I dismiss", so a language with two dismissible surfaces at two
|
|
706
|
+
rungs has two scrims. Reading the single 50 as the scrim's own number is what
|
|
707
|
+
makes a sheet impossible to scrim: at 50 over the sheet's 45 the scrim covers
|
|
708
|
+
the sheet completely, every click lands on the dismiss handler, and the surface
|
|
709
|
+
opens dead. So when you add a dismissible surface, check whether it needs a
|
|
710
|
+
scrim rung directly beneath it, and add both rows together.
|
|
711
|
+
|
|
575
712
|
## Light mode
|
|
576
713
|
|
|
577
714
|
Not shipped. The system is *structured* for it: no component contains a colour
|
|
@@ -684,6 +821,44 @@ The line to hold is the one-for-one rule above, not a list of layers. A theme
|
|
|
684
821
|
that restates the block still paints every component; a theme that invents one
|
|
685
822
|
does not.
|
|
686
823
|
|
|
824
|
+
### A change to the language is not finished until every theme wears it
|
|
825
|
+
|
|
826
|
+
The one-for-one rule above is written as an obligation on a *theme* — here is
|
|
827
|
+
what a new theme owes the language. Read only that way it has a hole in it, and
|
|
828
|
+
the hole is every change that goes the other direction. A token added to
|
|
829
|
+
`tokens.css`, a component added to `src/`, a look retuned: each of those is a
|
|
830
|
+
change to the thing the themes are mirroring, and none of them is finished when
|
|
831
|
+
the main theme looks right. There are three themes — the language itself in
|
|
832
|
+
`src/tokens.css`, `flat`, and `glass` — and a change lands in all three or it
|
|
833
|
+
has not landed.
|
|
834
|
+
|
|
835
|
+
That is the symmetric half of the toll already stated above. **A new theme
|
|
836
|
+
capability costs a main-theme token first; a change to the main theme costs
|
|
837
|
+
every theme a look.** Neither direction is optional, and the second is the one
|
|
838
|
+
easy to skip, because the default theme is the one on screen while you work.
|
|
839
|
+
|
|
840
|
+
What "answered in every theme" means depends on the shape of the change:
|
|
841
|
+
|
|
842
|
+
- **A new token.** Every theme either restates it or can point at why it does
|
|
843
|
+
not need to. Two reasons count. The default is inert — `--axi-surface-filter`
|
|
844
|
+
and `--axi-ground-image` are `none`, so a theme that wants neither is already
|
|
845
|
+
correct. Or the token aliases one the theme did restate —
|
|
846
|
+
`--axi-surface-float: var(--axi-surface)`, so `flat` restating the surface
|
|
847
|
+
restates the float with it. A token holding a literal of its own is answered
|
|
848
|
+
by neither of those, and every theme has to say it. This is checked; see
|
|
849
|
+
below.
|
|
850
|
+
- **A new component.** It renders under all three, and you look at it under all
|
|
851
|
+
three. The gallery's theme switcher is there for exactly the reason the accent
|
|
852
|
+
switcher is: a component that hard-coded something looks fine until you
|
|
853
|
+
change the thing it hard-coded. A panel that reads as a panel on opaque slate
|
|
854
|
+
can vanish on a translucent one.
|
|
855
|
+
- **A retuned look.** The `-paint` companions are the case that made this a
|
|
856
|
+
section. Lifting them meant every surface a theme grades needs a flat
|
|
857
|
+
companion beside it, and both themes had to be edited in the same commit as
|
|
858
|
+
the tokens — edit one and the other hands a gradient straight to
|
|
859
|
+
`background-color`, which is not a subtle failure but it is an invisible one
|
|
860
|
+
from the theme you happened to be looking at.
|
|
861
|
+
|
|
687
862
|
**What is mechanically enforced.** `tests/themes.test.mjs` reads every
|
|
688
863
|
`dist/themes/*.css` and checks the mirror rather than trusting it: the file
|
|
689
864
|
contains exactly one rule, its selector is `[data-axi-theme="<id>"]` for the
|
|
@@ -693,6 +868,16 @@ non-empty value, and every property it declares is already declared in
|
|
|
693
868
|
takes a second selector. An invented token fails the last. The suite passes
|
|
694
869
|
vacuously while no theme exists, and binds the moment the first file lands.
|
|
695
870
|
|
|
871
|
+
The section above is checked from the other side by the same file: a token that
|
|
872
|
+
*any* theme restates must be restated by *every* theme, unless that theme
|
|
873
|
+
inherits an answer already — the main-theme default is inert, or the token
|
|
874
|
+
aliases another the theme did restate, and the check follows the alias chain
|
|
875
|
+
rather than taking the two reasons on trust. So a token one theme has an opinion
|
|
876
|
+
about cannot be a token another theme forgot. What no test can check is the
|
|
877
|
+
third bullet, the look you did not look at: a component can paint under all
|
|
878
|
+
three themes and still be wrong under two of them, and the only instrument for
|
|
879
|
+
that is the theme switcher in the gallery.
|
|
880
|
+
|
|
696
881
|
## Adding a component
|
|
697
882
|
|
|
698
883
|
1. Which rule justifies it? If none, write the rule first or stop.
|
|
@@ -702,7 +887,11 @@ vacuously while no theme exists, and binds the moment the first file lands.
|
|
|
702
887
|
[Themes](#themes).
|
|
703
888
|
4. Add it to the gallery, and check it with the accent switcher — if it does
|
|
704
889
|
not follow the accent, it hard-coded something.
|
|
705
|
-
5.
|
|
890
|
+
5. Then check it with the theme switcher, under all three — the language,
|
|
891
|
+
`flat` and `glass`. Translucent surfaces and a 16px corner break different
|
|
892
|
+
things than opaque ones do, and a component is not done until it reads right
|
|
893
|
+
under each. See [Themes](#themes).
|
|
894
|
+
6. `npm run build` and commit `dist/axi.css` with your source change.
|
|
706
895
|
|
|
707
896
|
### A style only reachable through a layer will be re-invented
|
|
708
897
|
|
|
@@ -733,10 +922,30 @@ allowed to come apart, they had already come apart. When you lift a
|
|
|
733
922
|
layer-scoped style out, look for the declarations the layer was getting for
|
|
734
923
|
free from its element. Those are the ones the new spelling silently loses.
|
|
735
924
|
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
925
|
+
The third instance was not behind a layer prefix at all, and it widens the
|
|
926
|
+
rule. `.axi-palette__list` hid its scrollbar with the argument written inline —
|
|
927
|
+
a bar down the side of a small panel reporting a fact the row count has already
|
|
928
|
+
given. That was the language's only word for the decision, and the decision was
|
|
929
|
+
about *strips*, not about palettes. The same consumer needed it for two rails
|
|
930
|
+
and a picker slot and could not reach it, so it wrote the rule again five times
|
|
931
|
+
in its own stylesheet — twice with a `*` descendant arm, because the element
|
|
932
|
+
that actually scrolls sits one level inside a component it does not control,
|
|
933
|
+
which is the sledgehammer a consumer reaches for when the language gives it no
|
|
934
|
+
name. The remedy is unchanged: one rule, every spelling in it, and a class the
|
|
935
|
+
consumer can spend (`.axi-scroll-quiet`, in `src/utilities.css`).
|
|
936
|
+
|
|
937
|
+
So the check is not only "is this style behind a layer prefix". It is **is this
|
|
938
|
+
style the only statement of a decision that is broader than the component
|
|
939
|
+
stating it**. A component's own inline reasoning is the tell: if the comment
|
|
940
|
+
argues about a category of thing — a strip, a recess, a reading — and the
|
|
941
|
+
selector names one member of that category, the rest of the category has
|
|
942
|
+
nowhere to look.
|
|
943
|
+
|
|
944
|
+
Three instances, so the check belongs at the top of the list when adding
|
|
945
|
+
anything: grep `src/` for the component's style living behind a layer prefix,
|
|
946
|
+
and read the comments on any single-selector rule you are about to copy. If
|
|
947
|
+
either applies, it has consumers you cannot see, and they have already drawn
|
|
948
|
+
their own.
|
|
740
949
|
|
|
741
950
|
### A refusal holds at every level, not just the one it was written for
|
|
742
951
|
|
package/package.json
CHANGED
package/src/data.css
CHANGED
|
@@ -139,7 +139,17 @@
|
|
|
139
139
|
resting cell rule (`.axi-table :where(td, tbody th)`, also one class) and
|
|
140
140
|
wins on order, while an ink class in utilities.css - one class, later file -
|
|
141
141
|
wins over both. */
|
|
142
|
-
|
|
142
|
+
/* A DATA FILL SURVIVES A ROW STATE, and the `:not()` sits inside `:where()`
|
|
143
|
+
so saying so costs no specificity. A matrix cell's band IS its value (rule 9,
|
|
144
|
+
"the matrix is the bound on not at all") - washing it on the way past the row
|
|
145
|
+
you want deletes the reading. The row state is not weakened by this: it is
|
|
146
|
+
drawn by the leading edge below, and by every cell that has nothing to say.
|
|
147
|
+
Which is the edge-not-fill answer the selection rule below reaches on its own
|
|
148
|
+
grounds, arriving here a second time.
|
|
149
|
+
|
|
150
|
+
Nothing changes for a table with no `data-heat` in it, which is every table
|
|
151
|
+
in this language but one. */
|
|
152
|
+
.axi-table :where(tbody tr:hover) :where(td:not([data-heat]), th:not([data-heat])) {
|
|
143
153
|
background: var(--axi-surface-raised);
|
|
144
154
|
background-attachment: fixed;
|
|
145
155
|
color: var(--axi-text);
|
|
@@ -174,7 +184,7 @@
|
|
|
174
184
|
.axi-table :where(tbody tr) > :where(td, th):first-child {
|
|
175
185
|
border-inline-start: var(--axi-border-control) solid transparent;
|
|
176
186
|
}
|
|
177
|
-
.axi-table tbody tr[aria-current] :is(td, th) {
|
|
187
|
+
.axi-table tbody tr[aria-current] :is(td, th):where(:not([data-heat])) {
|
|
178
188
|
background: var(--axi-surface-raised);
|
|
179
189
|
background-attachment: fixed;
|
|
180
190
|
color: var(--axi-text);
|
|
@@ -335,6 +345,115 @@
|
|
|
335
345
|
}
|
|
336
346
|
.axi-table th:first-child .axi-table__sort { justify-content: flex-start; }
|
|
337
347
|
|
|
348
|
+
/* ---------- matrix ---------- */
|
|
349
|
+
/* Two categorical axes and a quantity where they cross: forty players down the
|
|
350
|
+
side, sixty five-second buckets across, a count in each cell. Rule 9's
|
|
351
|
+
"the matrix is the bound on not at all" is the whole argument for this
|
|
352
|
+
existing at all, and the short form is: the plane is spent, so there is no
|
|
353
|
+
third dimension left to give the quantity a LENGTH, and both axes are
|
|
354
|
+
already sorted by something the reader needs so neither can be re-sorted by
|
|
355
|
+
the value. Intensity is what is left, and it is admissible here because the
|
|
356
|
+
cell also prints its number - the digit is the legible copy rule 9 demands,
|
|
357
|
+
and the band is only what lets the eye find the shape without reading two
|
|
358
|
+
thousand figures one at a time. A matrix cell with nothing written in it is
|
|
359
|
+
a heatmap and rule 9 refuses it.
|
|
360
|
+
|
|
361
|
+
This is a MODIFIER and not a component, and that is the finding rather than
|
|
362
|
+
a convenience. Everything structural a matrix field needs, this table
|
|
363
|
+
already had: --sticky for the ruler that stays, --pinned for the names that
|
|
364
|
+
stay, --dense for sixty columns' padding, --fixed plus a <colgroup> for
|
|
365
|
+
proportional cells, and .axi-table__scroll for the frame they move inside.
|
|
366
|
+
The consumer measured against here had hand-written all five, and had got
|
|
367
|
+
--sticky wrong in exactly the way that modifier's comment warns about: its
|
|
368
|
+
ruler was --axi-surface-raised, so under glass the rows slid through it.
|
|
369
|
+
The only thing genuinely missing was the quantity.
|
|
370
|
+
|
|
371
|
+
What the modifier changes is the CELL, which stops being a number in a
|
|
372
|
+
column and becomes a patch in a field: centred rather than right-aligned,
|
|
373
|
+
because a band is read by position and not by its digits lining up, and
|
|
374
|
+
unpadded because the padding is the field. The name column keeps the base
|
|
375
|
+
table's left alignment and its gutter - it is still a name. */
|
|
376
|
+
.axi-table--matrix { border-collapse: separate; border-spacing: 0; }
|
|
377
|
+
.axi-table--matrix thead th { padding: 0 0 6px; text-align: center; }
|
|
378
|
+
.axi-table--matrix :is(td, tbody th) {
|
|
379
|
+
padding: 0;
|
|
380
|
+
text-align: center;
|
|
381
|
+
height: var(--axi-matrix-cell, 24px);
|
|
382
|
+
}
|
|
383
|
+
/* `min-width` and not `width`: under the default auto layout this is the floor
|
|
384
|
+
a one-digit column cannot fall below, and under --fixed a <colgroup> takes
|
|
385
|
+
over entirely (fixed layout ignores both min and max - see --fixed). */
|
|
386
|
+
.axi-table--matrix :is(td, tbody th):not(:first-child) {
|
|
387
|
+
min-width: var(--axi-matrix-cell, 24px);
|
|
388
|
+
}
|
|
389
|
+
.axi-table--matrix :is(td, tbody th):first-child {
|
|
390
|
+
text-align: left;
|
|
391
|
+
padding: 0 10px 0 0;
|
|
392
|
+
}
|
|
393
|
+
/* The four bands. Each is a color-mix() of the accent into the surface's flat
|
|
394
|
+
companion, which makes every one of them a computed OPAQUE colour - the
|
|
395
|
+
accent at 18% alpha over the field would be rule 2's faded ink, and it also
|
|
396
|
+
fails on its own terms, because the cells a reader is scanning for are the
|
|
397
|
+
quiet ones and an alpha ramp is where those disappear.
|
|
398
|
+
|
|
399
|
+
--axi-surface-paint and not --axi-surface, and this is the trap: a surface
|
|
400
|
+
token is allowed to hold a gradient (rule 1's one relief, which is how a
|
|
401
|
+
glass theme exists), color-mix() takes colours and nothing else, and an
|
|
402
|
+
invalid color-mix() is dropped at computed-value time. Spell it --axi-surface
|
|
403
|
+
and the bands do not fade - they vanish, silently, in whichever theme paints
|
|
404
|
+
a gradient. That is measured, not hypothetical: it is why
|
|
405
|
+
--axi-surface-paint exists (see tokens.css).
|
|
406
|
+
|
|
407
|
+
Four steps and not a continuous ramp, because a band is also where the digit
|
|
408
|
+
has to change ink, and a step is the only thing a flip can happen ON. The
|
|
409
|
+
top two carry --axi-accent-ink for the same reason .axi-btn--primary does:
|
|
410
|
+
past roughly half strength the field is the accent and the text on it is the
|
|
411
|
+
accent's companion, not the text ramp. */
|
|
412
|
+
.axi-table--matrix :is(td, tbody th)[data-heat='1'] {
|
|
413
|
+
background-color: color-mix(in srgb, var(--axi-accent) 18%, var(--axi-surface-paint));
|
|
414
|
+
}
|
|
415
|
+
.axi-table--matrix :is(td, tbody th)[data-heat='2'] {
|
|
416
|
+
background-color: color-mix(in srgb, var(--axi-accent) 42%, var(--axi-surface-paint));
|
|
417
|
+
}
|
|
418
|
+
.axi-table--matrix :is(td, tbody th)[data-heat='3'] {
|
|
419
|
+
background-color: color-mix(in srgb, var(--axi-accent) 70%, var(--axi-surface-paint));
|
|
420
|
+
color: var(--axi-accent-ink);
|
|
421
|
+
}
|
|
422
|
+
.axi-table--matrix :is(td, tbody th)[data-heat='4'] {
|
|
423
|
+
background-color: color-mix(in srgb, var(--axi-accent) 100%, var(--axi-surface-paint));
|
|
424
|
+
color: var(--axi-accent-ink);
|
|
425
|
+
}
|
|
426
|
+
/* A matrix whose columns are a TIMELINE rather than a list of categories, which
|
|
427
|
+
changes two things and only two.
|
|
428
|
+
|
|
429
|
+
The column labels move left. A label over a category names the column and
|
|
430
|
+
belongs centred over it; a label on a ruler names a MOMENT, and centring it
|
|
431
|
+
puts the text half a cell to the right of the instant it is pointing at.
|
|
432
|
+
|
|
433
|
+
And the division gets a line. `[data-tick]` marks the columns that carry a
|
|
434
|
+
label - every thirtieth second, not every bucket, because at a five-second
|
|
435
|
+
resolution a five-minute fight is sixty columns and a timestamp over each is
|
|
436
|
+
unreadable at the width a cell allows. The line is the RULE, at the hairline
|
|
437
|
+
step: it is the same line that parts the rows, continued down the field,
|
|
438
|
+
which is the whole reason it is not drawn on every column. Sixty ruled
|
|
439
|
+
columns is a spreadsheet; the bands are meant to be the figure. */
|
|
440
|
+
.axi-table--ruler thead th { text-align: left; }
|
|
441
|
+
.axi-table--ruler :is(thead th, td, tbody th)[data-tick] {
|
|
442
|
+
border-left: var(--axi-border-hairline) solid var(--axi-rule);
|
|
443
|
+
}
|
|
444
|
+
/* A change of category down the rows - a subgroup, a team, a date. Heavier than
|
|
445
|
+
the hairline that parts two rows of one group, and heavier in INK rather than
|
|
446
|
+
in weight: the same line, drawn darker. Going up a form step instead would
|
|
447
|
+
put a control-weight line inside running content, which is rule 8's grid of
|
|
448
|
+
boxes, and the boundary would then compete with the head's own lid.
|
|
449
|
+
|
|
450
|
+
An attribute and not a class, per rule 13, and on the row because that is
|
|
451
|
+
what starts: the consumer compares one row's category with the previous
|
|
452
|
+
row's, which is a fact only it can know. */
|
|
453
|
+
.axi-table tbody tr[data-group-start] > :is(td, th) {
|
|
454
|
+
border-top: var(--axi-border-hairline) solid var(--axi-ink-line);
|
|
455
|
+
}
|
|
456
|
+
|
|
338
457
|
/* ---------- meter ---------- */
|
|
339
458
|
/* Rule 9: a proportion is a length. The track is the ground, the fill is the
|
|
340
459
|
value, and the fill is one ink at full strength - a tinted or faded bar is
|
|
@@ -384,6 +503,61 @@
|
|
|
384
503
|
font-variant-numeric: tabular-nums;
|
|
385
504
|
}
|
|
386
505
|
|
|
506
|
+
/* ---------- readout ---------- */
|
|
507
|
+
/* A short run of labelled readings: logs seen, uploaded, failed; the session's
|
|
508
|
+
start and its length; a setting and the switch that sets it. The shape of
|
|
509
|
+
every status card in a dashboard's side column, and the one the consumer
|
|
510
|
+
that had four of them built by hand each time.
|
|
511
|
+
|
|
512
|
+
Rule 8 settles how it is drawn. The eye runs down the value column - that is
|
|
513
|
+
the whole point of stacking the readings - so it is a table's interior:
|
|
514
|
+
rows parted by the rule at the hairline weight, no outline and no block on
|
|
515
|
+
any row, nothing raised inside the panel that is already the raised thing.
|
|
516
|
+
It is not a table element because it is two cells wide, has no head, and
|
|
517
|
+
the reading in the second cell is as often a control as a figure; a <dl> is
|
|
518
|
+
what the document structure calls for and the classes sit on it.
|
|
519
|
+
|
|
520
|
+
The key is the meter list's name and the value is the meter list's value,
|
|
521
|
+
restated here rather than aliased because the two lists are laid out
|
|
522
|
+
differently and rule-8's test - is there a column? - is the same for both.
|
|
523
|
+
|
|
524
|
+
Row padding is a knob at two fallbacks, like the eyebrow's gap: 7px at
|
|
525
|
+
panel density, and 3px inside a tile, where a hairline already holds the
|
|
526
|
+
rows apart and 3px is still a clear gap at this type size. */
|
|
527
|
+
.axi-readout {
|
|
528
|
+
display: flex;
|
|
529
|
+
flex-direction: column;
|
|
530
|
+
margin: 0;
|
|
531
|
+
}
|
|
532
|
+
.axi-readout__row {
|
|
533
|
+
display: flex;
|
|
534
|
+
align-items: center;
|
|
535
|
+
justify-content: space-between;
|
|
536
|
+
gap: 10px;
|
|
537
|
+
padding: var(--axi-readout-pad, 7px) 0;
|
|
538
|
+
}
|
|
539
|
+
.axi-readout__row + .axi-readout__row {
|
|
540
|
+
border-top: var(--axi-border-hairline) solid var(--axi-rule);
|
|
541
|
+
}
|
|
542
|
+
.axi-readout__k {
|
|
543
|
+
min-width: 0;
|
|
544
|
+
margin: 0;
|
|
545
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
546
|
+
font: var(--axi-t-small);
|
|
547
|
+
font-weight: 700;
|
|
548
|
+
color: var(--axi-text-dim);
|
|
549
|
+
}
|
|
550
|
+
.axi-readout__v {
|
|
551
|
+
flex: none;
|
|
552
|
+
margin: 0;
|
|
553
|
+
text-align: right;
|
|
554
|
+
font: var(--axi-t-micro);
|
|
555
|
+
letter-spacing: var(--axi-ls-micro);
|
|
556
|
+
color: var(--axi-text);
|
|
557
|
+
font-variant-numeric: tabular-nums;
|
|
558
|
+
}
|
|
559
|
+
.axi-panel--tile .axi-readout__row { padding-block: var(--axi-readout-pad, 3px); }
|
|
560
|
+
|
|
387
561
|
/* ---------- bars ---------- */
|
|
388
562
|
/* The same rule stood on end. The baseline is drawn at the control weight
|
|
389
563
|
because it is an axis - the one line in a chart that is structure rather
|
|
@@ -513,3 +687,31 @@
|
|
|
513
687
|
text-transform: uppercase;
|
|
514
688
|
color: var(--axi-text-dim);
|
|
515
689
|
}
|
|
690
|
+
/* A key you can press, to isolate the series it names. The state is the
|
|
691
|
+
attribute rule 13 asks for: `aria-pressed="true"` on the key that is
|
|
692
|
+
isolated, and nothing at all on the others - which of them recede is the
|
|
693
|
+
legend's to work out, from whether any key is pressed, rather than a second
|
|
694
|
+
attribute the consumer has to compute and keep in step.
|
|
695
|
+
|
|
696
|
+
Receding is a step down the neutral ramp and not an opacity, which is rule
|
|
697
|
+
4's "nothing fades" reaching the legend: the consumer this was written for
|
|
698
|
+
had the rest of the keys at `opacity: .3` and they were unreadable, which
|
|
699
|
+
is the one thing a legend must not be. A receded key brightens back to
|
|
700
|
+
plain under the cursor so it can be found again - the hover is wrapped so it
|
|
701
|
+
costs nothing against an ink, and the receded rule steps aside for it.
|
|
702
|
+
|
|
703
|
+
The button reset and the hover are :where()-wrapped for the reason the ink
|
|
704
|
+
layer gives: an inked key keeps its ink. The pressed and receded colours are
|
|
705
|
+
the state's own meaning and stay at full weight. */
|
|
706
|
+
.axi-legend__key:where(button) {
|
|
707
|
+
padding: 0;
|
|
708
|
+
background: none;
|
|
709
|
+
border: 0;
|
|
710
|
+
cursor: pointer;
|
|
711
|
+
transition: color .1s;
|
|
712
|
+
}
|
|
713
|
+
.axi-legend__key:where(button:hover) { color: var(--axi-text); }
|
|
714
|
+
.axi-legend__key[aria-pressed="true"] { color: var(--axi-text); }
|
|
715
|
+
.axi-legend:has([aria-pressed="true"]) .axi-legend__key:where(:not([aria-pressed="true"], :hover)) {
|
|
716
|
+
color: var(--axi-text-faint);
|
|
717
|
+
}
|
package/src/layout.css
CHANGED
|
@@ -37,6 +37,68 @@
|
|
|
37
37
|
/* A horizontal run that wraps rather than overflowing. */
|
|
38
38
|
.axi-row { display: flex; align-items: center; flex-wrap: wrap; gap: var(--axi-row-gap, 10px); }
|
|
39
39
|
|
|
40
|
+
/* ---------- split pane ---------- */
|
|
41
|
+
/* A picker on the left choosing what the surface on the right shows. Thirteen
|
|
42
|
+
sections of one consumer are this shape, and a fourteenth had hand-built it
|
|
43
|
+
inline because there was no name for it.
|
|
44
|
+
|
|
45
|
+
It is two objects on one plane and not two panels, which is the whole reason
|
|
46
|
+
it needs naming: a picker list beside a table is the first case --axi-well's
|
|
47
|
+
own comment names, and the thing the list picks is the raised counterpart to
|
|
48
|
+
that recess. Drawn as two panels it read flat - a hairline parting one field,
|
|
49
|
+
with the list's scrollbar carving a groove down the middle of it - because
|
|
50
|
+
nothing in it was an object.
|
|
51
|
+
|
|
52
|
+
The body carries the CONTROL step, not the panel's. A split pane lives
|
|
53
|
+
inside a panel that has already paid a 6px block, and rule 8's counterpart
|
|
54
|
+
settles what a second one nested in it reads as: two planes arguing. The
|
|
55
|
+
consumer that derived this shape by hand got the outline and left out the
|
|
56
|
+
block entirely, which is a rule 3 failure in the other direction - a
|
|
57
|
+
boundary drawn around content that is standing on the surface behind it.
|
|
58
|
+
Outlined and blocked, at the weight that says "the contents of a box".
|
|
59
|
+
|
|
60
|
+
The radius follows the form step and not the footprint. The body is
|
|
61
|
+
panel-sized in area and control-weight in form, and .axi-panel--tile already
|
|
62
|
+
settled that argument the same way.
|
|
63
|
+
|
|
64
|
+
`minmax(0, 1fr)` rather than `1fr`: a table in a `1fr` track cannot shrink
|
|
65
|
+
below its own content, and every cell in .axi-table is `nowrap`, so the
|
|
66
|
+
column blows out and takes the pane's width with it. `1fr` is
|
|
67
|
+
`minmax(auto, 1fr)` and `auto` is a content floor.
|
|
68
|
+
|
|
69
|
+
No breakpoint of its own. Under 640px the two stack, which is layout.css's
|
|
70
|
+
one breakpoint and deliberately still its only one - .axi-table--fixed
|
|
71
|
+
already makes a 360px pane work, so a pane narrow enough to need a second
|
|
72
|
+
breakpoint is one --fixed was written for. */
|
|
73
|
+
.axi-split {
|
|
74
|
+
display: grid;
|
|
75
|
+
grid-template-columns: var(--axi-split-nav-w, 280px) minmax(0, 1fr);
|
|
76
|
+
gap: var(--axi-split-gap, 10px);
|
|
77
|
+
/* A split pane is routinely the flex child that fills a section's height,
|
|
78
|
+
and a grid that cannot shrink below its content pushes the section past
|
|
79
|
+
its own cap - the same reason .axi-palette__list says it. */
|
|
80
|
+
min-height: 0;
|
|
81
|
+
}
|
|
82
|
+
/* The picker is a well, declared as one in the same rule as .axi-well in
|
|
83
|
+
src/primitives.css rather than restated here. This part only says what the
|
|
84
|
+
slot adds: it scrolls, and it may not out-grow the row it sits in. Its
|
|
85
|
+
scrollbar is hidden by the language's quiet-scroll rule in
|
|
86
|
+
src/utilities.css, which is where that decision lives for every strip. */
|
|
87
|
+
.axi-split__nav { overflow-y: auto; min-height: 0; }
|
|
88
|
+
.axi-split__body {
|
|
89
|
+
/* Paired with the track's minmax(0, 1fr): the track stops the COLUMN
|
|
90
|
+
growing, this stops the ITEM, whose min-width is `auto` by default and is
|
|
91
|
+
what an ellipsis inside it is actually fighting. */
|
|
92
|
+
min-width: 0;
|
|
93
|
+
min-height: 0;
|
|
94
|
+
overflow: hidden;
|
|
95
|
+
background: var(--axi-surface);
|
|
96
|
+
backdrop-filter: var(--axi-surface-filter);
|
|
97
|
+
border: var(--axi-border-control) solid var(--axi-ink-line);
|
|
98
|
+
border-radius: var(--axi-radius-sm);
|
|
99
|
+
box-shadow: var(--axi-shadow-control);
|
|
100
|
+
}
|
|
101
|
+
|
|
40
102
|
@media (max-width: 640px) {
|
|
41
103
|
/* The fallback must match the resting rule's (var(--axi-gutter), 18px) -
|
|
42
104
|
README.md documents --axi-page-pad's fallback as --axi-gutter, and a
|
|
@@ -44,4 +106,10 @@
|
|
|
44
106
|
mobile, which is the one viewport where the gutter matters most. */
|
|
45
107
|
.axi-page { padding-inline: var(--axi-page-pad, var(--axi-gutter)); }
|
|
46
108
|
.axi-grid { grid-template-columns: 1fr; }
|
|
109
|
+
/* The picker stops being a column beside the thing it picks and becomes the
|
|
110
|
+
row above it. It keeps a height cap - a picker that grows to twenty rows
|
|
111
|
+
pushes the result it picked off the screen, which is the one thing the
|
|
112
|
+
stacked form must not do. */
|
|
113
|
+
.axi-split { grid-template-columns: 1fr; }
|
|
114
|
+
.axi-split__nav { max-height: var(--axi-split-nav-h, 200px); }
|
|
47
115
|
}
|