@axiapps/axi-design 1.43.0 → 1.45.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/dist/axi.css +710 -46
- package/dist/themes/flat.css +1 -0
- package/docs/RULES.md +173 -4
- package/package.json +1 -1
- package/src/base.css +40 -0
- package/src/data.css +333 -2
- package/src/layout.css +68 -0
- package/src/primitives.css +123 -8
- package/src/shells.css +70 -29
- package/src/tokens.css +14 -0
- 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 —
|
|
@@ -421,6 +506,28 @@ their meanings inside a chart, so nothing else may borrow them for a category.
|
|
|
421
506
|
The status inks still mean status inside a plot: a line drawn in `--axi-danger`
|
|
422
507
|
is asserting that the quantity is bad, not that it is the third series.
|
|
423
508
|
|
|
509
|
+
### A chart's furniture recedes
|
|
510
|
+
|
|
511
|
+
Rule 10 is about the series. Everything drawn *around* a series - gridlines,
|
|
512
|
+
the axis, tick labels, the hover band, a brush - is furniture, and furniture is
|
|
513
|
+
drawn in the neutral ramp so the series are the only colour in the plot. The
|
|
514
|
+
grid sits a step below the rule, at `--axi-grid`, because a rule divides two
|
|
515
|
+
things and a grid is only what a value is read against; the axis is a rule at
|
|
516
|
+
the hairline; tick labels are the faint ink with tabular figures. A point, a
|
|
517
|
+
bar or a slice keeps its own fill and takes the hairline ink edge every small
|
|
518
|
+
object takes under rule 3. Nothing in a chart is dashed: a dash is decoration,
|
|
519
|
+
and a threshold is one of the few lines in a chart meant to be read.
|
|
520
|
+
|
|
521
|
+
One accent is spent inside a plot, and only on the one thing in it you can
|
|
522
|
+
grab - a brush's travellers. The hover band is already under the pointer and
|
|
523
|
+
needs no colour to be found.
|
|
524
|
+
|
|
525
|
+
An app that draws its charts with a library gets the same furniture by
|
|
526
|
+
wrapping the chart in `.axi-chart`, which binds these decisions to the
|
|
527
|
+
library's own element classes. The binding is written without `!important`:
|
|
528
|
+
a consumer that hands the library an inline style has drawn a second design,
|
|
529
|
+
and the remedy is to stop handing it, not for the language to shout over it.
|
|
530
|
+
|
|
424
531
|
## 11. An indicator of work animates a composited property
|
|
425
532
|
|
|
426
533
|
Spinners, progress strips and pulses almost always report on something
|
|
@@ -501,6 +608,48 @@ that must be rejected, one per prohibition. The rule above is therefore a check
|
|
|
501
608
|
rather than a promise — the same treatment rule 3's weights get in
|
|
502
609
|
`tests/tokens.test.mjs`.
|
|
503
610
|
|
|
611
|
+
## 13. A state is an attribute, and the appearance follows it
|
|
612
|
+
|
|
613
|
+
Every state this language draws is keyed off an attribute the element already
|
|
614
|
+
carries, never off a class invented to describe the look. `[aria-current]` on a
|
|
615
|
+
rail item, a tab, a table row and a picked panel. `[aria-pressed="true"]` on a
|
|
616
|
+
pill. `[aria-selected="true"]` on a listbox option. `[aria-disabled="true"]`
|
|
617
|
+
beside `:disabled`, in 26 places. `[aria-sort]` on the sorted column,
|
|
618
|
+
`[aria-expanded]` on the thing that opens. One spelling per semantics, and the
|
|
619
|
+
semantics decides which — not the appearance, which is why a rail item and a
|
|
620
|
+
picked panel share `[aria-current]` while looking nothing alike.
|
|
621
|
+
|
|
622
|
+
The reason is not tidiness. **A state that exists only as an appearance is not a
|
|
623
|
+
state.** A consumer that marks the chosen card with a class has drawn a mark
|
|
624
|
+
sighted users can see and told everyone else nothing, and no amount of styling
|
|
625
|
+
fixes it from our side — the information was never in the document. Keying the
|
|
626
|
+
style off the attribute makes the two inseparable: you cannot get the look
|
|
627
|
+
without emitting the state, and you cannot emit the state and fail to get the
|
|
628
|
+
look.
|
|
629
|
+
|
|
630
|
+
This is also what stops the language growing a second vocabulary. An invented
|
|
631
|
+
`--selected` modifier would be a synonym for `[aria-current]` that a screen
|
|
632
|
+
reader cannot read, and the two would drift the first time one of them got a
|
|
633
|
+
tweak. There is no `.axi-panel--selected` for the same reason there is no
|
|
634
|
+
`.axi-btn--off`.
|
|
635
|
+
|
|
636
|
+
Two consequences when adding a component:
|
|
637
|
+
|
|
638
|
+
- **Find the attribute before writing the rule.** If the state the component
|
|
639
|
+
needs already has an ARIA spelling, use it, even if the look is unlike every
|
|
640
|
+
other user of that attribute. If it genuinely has none, that is the moment to
|
|
641
|
+
ask whether the state is real.
|
|
642
|
+
- **A state the markup holds needs no attribute at all.** A `<label>` wrapping
|
|
643
|
+
its own radio is the correct markup for a picker; the input holds the state,
|
|
644
|
+
so the label has nothing to set, and copying it onto the label would be a
|
|
645
|
+
second source of truth that can disagree with the first. That case is matched
|
|
646
|
+
structurally — `:has(> input:checked)`, the language's only `:has()`, with the
|
|
647
|
+
child combinator load-bearing: a descendant match would fire on any checkbox
|
|
648
|
+
buried in the component's content.
|
|
649
|
+
|
|
650
|
+
`tests/tokens.test.mjs` holds this rule to the components that carry it, so it
|
|
651
|
+
is a check rather than a promise.
|
|
652
|
+
|
|
504
653
|
## Tokens
|
|
505
654
|
|
|
506
655
|
Three layers, in `src/tokens.css` — the only file permitted to contain a colour
|
|
@@ -795,10 +944,30 @@ allowed to come apart, they had already come apart. When you lift a
|
|
|
795
944
|
layer-scoped style out, look for the declarations the layer was getting for
|
|
796
945
|
free from its element. Those are the ones the new spelling silently loses.
|
|
797
946
|
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
947
|
+
The third instance was not behind a layer prefix at all, and it widens the
|
|
948
|
+
rule. `.axi-palette__list` hid its scrollbar with the argument written inline —
|
|
949
|
+
a bar down the side of a small panel reporting a fact the row count has already
|
|
950
|
+
given. That was the language's only word for the decision, and the decision was
|
|
951
|
+
about *strips*, not about palettes. The same consumer needed it for two rails
|
|
952
|
+
and a picker slot and could not reach it, so it wrote the rule again five times
|
|
953
|
+
in its own stylesheet — twice with a `*` descendant arm, because the element
|
|
954
|
+
that actually scrolls sits one level inside a component it does not control,
|
|
955
|
+
which is the sledgehammer a consumer reaches for when the language gives it no
|
|
956
|
+
name. The remedy is unchanged: one rule, every spelling in it, and a class the
|
|
957
|
+
consumer can spend (`.axi-scroll-quiet`, in `src/utilities.css`).
|
|
958
|
+
|
|
959
|
+
So the check is not only "is this style behind a layer prefix". It is **is this
|
|
960
|
+
style the only statement of a decision that is broader than the component
|
|
961
|
+
stating it**. A component's own inline reasoning is the tell: if the comment
|
|
962
|
+
argues about a category of thing — a strip, a recess, a reading — and the
|
|
963
|
+
selector names one member of that category, the rest of the category has
|
|
964
|
+
nowhere to look.
|
|
965
|
+
|
|
966
|
+
Three instances, so the check belongs at the top of the list when adding
|
|
967
|
+
anything: grep `src/` for the component's style living behind a layer prefix,
|
|
968
|
+
and read the comments on any single-selector rule you are about to copy. If
|
|
969
|
+
either applies, it has consumers you cannot see, and they have already drawn
|
|
970
|
+
their own.
|
|
802
971
|
|
|
803
972
|
### A refusal holds at every level, not just the one it was written for
|
|
804
973
|
|
package/package.json
CHANGED
package/src/base.css
CHANGED
|
@@ -72,3 +72,43 @@ a { color: inherit; }
|
|
|
72
72
|
transform: none !important;
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
|
+
|
|
76
|
+
/* The scrollbar is chrome, and chrome is drawn in the language's own ink. A
|
|
77
|
+
browser's default bar is a light capsule on a light channel, which is two
|
|
78
|
+
things this language does not have: a colour at partial opacity over the
|
|
79
|
+
ground (rule 2) and the one radius the system refuses (see --axi-radius).
|
|
80
|
+
The consumer that first themed it had written both anyway - a 999px thumb
|
|
81
|
+
in white at 35% - in its own stylesheet, because the language said nothing
|
|
82
|
+
here, and a decision the language leaves unsaid is one every consumer makes
|
|
83
|
+
differently.
|
|
84
|
+
|
|
85
|
+
So: the thumb is the rule ink, a rectangle, in a transparent channel. The
|
|
86
|
+
channel is transparent rather than the ground for the reason the well's fill
|
|
87
|
+
gives: an opaque page colour inside a translucent pane is a black patch, and
|
|
88
|
+
a bar runs down the inside of panes and wells as often as down the page.
|
|
89
|
+
|
|
90
|
+
Two spellings, one per engine, and neither is a fallback for the other.
|
|
91
|
+
`::-webkit-scrollbar` is the only spelling that controls the SHAPE - the
|
|
92
|
+
standard `scrollbar-color` pair recolours whatever the engine draws, and
|
|
93
|
+
what Chromium draws is a capsule with arrow buttons, the shape this language
|
|
94
|
+
refuses. Worse, Chromium (121+) switches the pseudo-elements off entirely the
|
|
95
|
+
moment either standard property is set, so writing both does not combine
|
|
96
|
+
them: it hands Chromium the capsule and throws the rectangle away. (The
|
|
97
|
+
consumer this replaces had written both, and had been tuning a webkit block
|
|
98
|
+
Chromium stopped reading, for years.) So the standard pair is scoped to the
|
|
99
|
+
engines that have no pseudo-element to disable, which today is Firefox, and
|
|
100
|
+
the pseudo-elements carry the design everywhere else. scrollbar-color
|
|
101
|
+
inherits, so the root says it once; scrollbar-width does not, so it is set
|
|
102
|
+
on everything.
|
|
103
|
+
|
|
104
|
+
The quiet-scroll rule in utilities.css still wins where it applies: those
|
|
105
|
+
selectors carry a class and these carry none, so a strip that has asked for
|
|
106
|
+
no bar gets none, and everything else gets this one. */
|
|
107
|
+
@supports not selector(::-webkit-scrollbar) {
|
|
108
|
+
html { scrollbar-color: var(--axi-rule) transparent; }
|
|
109
|
+
* { scrollbar-width: thin; }
|
|
110
|
+
}
|
|
111
|
+
::-webkit-scrollbar { width: 10px; height: 10px; }
|
|
112
|
+
::-webkit-scrollbar-track,
|
|
113
|
+
::-webkit-scrollbar-corner { background: transparent; }
|
|
114
|
+
::-webkit-scrollbar-thumb { background: var(--axi-rule); border-radius: 0; }
|
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,160 @@
|
|
|
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
|
+
}
|
|
718
|
+
|
|
719
|
+
/* ---------- library charts ---------- */
|
|
720
|
+
/* The language draws its own bars, plot and ticks above. An app that needs a
|
|
721
|
+
stacked area, a brush or a pie reaches for a charting library instead, and
|
|
722
|
+
the library arrives with its own furniture: a dashed grey grid, a #666 axis,
|
|
723
|
+
a slate tooltip with a half-rem corner. Every series it draws is the
|
|
724
|
+
consumer's data and keeps its own colour (rule 10). Everything drawn AROUND
|
|
725
|
+
the series is furniture, and furniture is the language's.
|
|
726
|
+
|
|
727
|
+
This is the binding for recharts, which is the library the suite uses. It
|
|
728
|
+
is scoped to `.axi-chart` so a chart opts in by wrapping, and it reaches the
|
|
729
|
+
library's elements by the class names recharts puts on them. recharts spreads
|
|
730
|
+
stroke and fill onto its SVG as presentation attributes, which a stylesheet
|
|
731
|
+
outranks, so nothing here needs to be passed into the library - and nothing
|
|
732
|
+
here is written with !important. A consumer that hands the library an inline
|
|
733
|
+
`contentStyle` has written a second design for the tooltip, and the answer
|
|
734
|
+
is to stop writing it, not for the language to shout over it: the tooltip is
|
|
735
|
+
`.axi-tooltip--flow` as custom content, drawn once, like every other tooltip.
|
|
736
|
+
|
|
737
|
+
The grammar is the one every component here already follows. Furniture is
|
|
738
|
+
drawn in the neutral ramp and recedes: the grid at --axi-grid, a step below
|
|
739
|
+
the rule (see tokens.css); the axis as a rule at the hairline weight, the
|
|
740
|
+
same line the table draws between rows; tick labels in the faint ink with
|
|
741
|
+
tabular figures, so a y-axis whose digits change width holds still. Every
|
|
742
|
+
small object standing on a series - a point, a bar, a slice - takes the ink
|
|
743
|
+
edge rule 3 gives every other small object, at the hairline because it is a
|
|
744
|
+
mark and not a raised surface, and keeps its fill, because the fill is which
|
|
745
|
+
series it belongs to. The hover band is the raised step's -paint companion
|
|
746
|
+
(a `fill` cannot be a gradient) rather than a white wash, which is rule 2.
|
|
747
|
+
|
|
748
|
+
The brush is the one place an accent is spent. Its two travellers are the
|
|
749
|
+
only thing in any chart you can grab, and they are small: a target you have
|
|
750
|
+
to find before you can use it is what the accent is for, unlike the hover
|
|
751
|
+
band, which is already under the pointer. The slide between them is the
|
|
752
|
+
surface as one paint, the trough behind them a well. One accent in the plot,
|
|
753
|
+
on the one control.
|
|
754
|
+
|
|
755
|
+
Solid, never dashed. A dash is decoration, and fifteen charts of dashes is a
|
|
756
|
+
texture rather than a scale; the reference line - a threshold, a claim - is
|
|
757
|
+
solid too, because it is one of the few lines in a chart meant to be read. */
|
|
758
|
+
.axi-chart {
|
|
759
|
+
/* Every figure in a chart is a label of a number, so the whole scope reads
|
|
760
|
+
tabular. Nothing else is declared on the scope: a chart's size belongs to
|
|
761
|
+
the library's container and the consumer's layout. */
|
|
762
|
+
font-variant-numeric: tabular-nums;
|
|
763
|
+
}
|
|
764
|
+
.axi-chart .recharts-cartesian-grid line {
|
|
765
|
+
stroke: var(--axi-grid);
|
|
766
|
+
stroke-dasharray: none;
|
|
767
|
+
stroke-width: 1;
|
|
768
|
+
}
|
|
769
|
+
/* Stripes are a second background inside a chart that already has one. */
|
|
770
|
+
.axi-chart .recharts-cartesian-grid-bg,
|
|
771
|
+
.axi-chart .recharts-cartesian-gridstripes-horizontal,
|
|
772
|
+
.axi-chart .recharts-cartesian-gridstripes-vertical { display: none; }
|
|
773
|
+
|
|
774
|
+
.axi-chart .recharts-cartesian-axis-line {
|
|
775
|
+
stroke: var(--axi-rule);
|
|
776
|
+
stroke-width: var(--axi-border-hairline);
|
|
777
|
+
}
|
|
778
|
+
.axi-chart .recharts-cartesian-axis-tick-line { stroke: var(--axi-rule); }
|
|
779
|
+
.axi-chart .recharts-cartesian-axis-tick-value { fill: var(--axi-text-faint); }
|
|
780
|
+
.axi-chart .recharts-label,
|
|
781
|
+
.axi-chart .recharts-legend-item-text { fill: var(--axi-text-dim); color: var(--axi-text-dim); }
|
|
782
|
+
/* The legend is a key, so it reads as labels rather than as chips - the same
|
|
783
|
+
type .axi-legend__key sets. */
|
|
784
|
+
.axi-chart .recharts-legend-wrapper {
|
|
785
|
+
font: var(--axi-t-micro);
|
|
786
|
+
letter-spacing: var(--axi-ls-micro);
|
|
787
|
+
text-transform: uppercase;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/* The hover band. A fill change rather than a wash: the next surface up, as
|
|
791
|
+
one paint, edged with the rule at the hairline like every line here. */
|
|
792
|
+
.axi-chart .recharts-tooltip-cursor {
|
|
793
|
+
fill: var(--axi-surface-raised-paint);
|
|
794
|
+
fill-opacity: 1;
|
|
795
|
+
stroke: var(--axi-rule);
|
|
796
|
+
stroke-width: var(--axi-border-hairline);
|
|
797
|
+
}
|
|
798
|
+
/* A line chart's cursor is a crosshair, not a band, so it stays a line. */
|
|
799
|
+
.axi-chart .recharts-curve.recharts-tooltip-cursor {
|
|
800
|
+
fill: none;
|
|
801
|
+
stroke: var(--axi-rule);
|
|
802
|
+
stroke-dasharray: none;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/* Small objects standing on a series keep their fill and take the ink edge. */
|
|
806
|
+
.axi-chart .recharts-line-dot,
|
|
807
|
+
.axi-chart .recharts-scatter-symbol > path,
|
|
808
|
+
.axi-chart .recharts-pie-sector > path,
|
|
809
|
+
.axi-chart .recharts-bar-rectangle > path {
|
|
810
|
+
stroke: var(--axi-ink-line);
|
|
811
|
+
stroke-width: var(--axi-border-hairline);
|
|
812
|
+
}
|
|
813
|
+
.axi-chart .recharts-active-dot > * {
|
|
814
|
+
stroke: var(--axi-ink-line);
|
|
815
|
+
stroke-width: var(--axi-border-control);
|
|
816
|
+
}
|
|
817
|
+
.axi-chart .recharts-bar-background-rectangle { fill: var(--axi-well-fill); }
|
|
818
|
+
|
|
819
|
+
.axi-chart .recharts-reference-line-line {
|
|
820
|
+
stroke: var(--axi-text-faint);
|
|
821
|
+
stroke-width: var(--axi-border-hairline);
|
|
822
|
+
stroke-dasharray: none;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/* The brush: a well, a slide, two travellers, and the preview curve dropped to
|
|
826
|
+
furniture weight because it is context rather than data you read off. */
|
|
827
|
+
.axi-chart .recharts-brush > rect:first-of-type {
|
|
828
|
+
fill: var(--axi-well-fill);
|
|
829
|
+
stroke: var(--axi-well-line);
|
|
830
|
+
stroke-width: var(--axi-border-control);
|
|
831
|
+
}
|
|
832
|
+
.axi-chart .recharts-brush-slide {
|
|
833
|
+
fill: var(--axi-surface-paint);
|
|
834
|
+
fill-opacity: 1;
|
|
835
|
+
}
|
|
836
|
+
.axi-chart .recharts-brush-traveller > rect {
|
|
837
|
+
fill: var(--axi-accent);
|
|
838
|
+
stroke: var(--axi-ink-line);
|
|
839
|
+
stroke-width: var(--axi-border-hairline);
|
|
840
|
+
}
|
|
841
|
+
.axi-chart .recharts-brush-traveller > line { stroke: var(--axi-accent-ink); }
|
|
842
|
+
.axi-chart .recharts-brush-texts { fill: var(--axi-text-faint); }
|
|
843
|
+
.axi-chart .recharts-brush .recharts-curve {
|
|
844
|
+
stroke: var(--axi-text-faint);
|
|
845
|
+
fill: none;
|
|
846
|
+
}
|