@axiapps/axi-design 1.43.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/dist/axi.css +505 -46
- package/dist/themes/flat.css +1 -0
- package/docs/RULES.md +151 -4
- 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 +70 -29
- package/src/utilities.css +62 -7
package/dist/axi.css
CHANGED
|
@@ -425,13 +425,69 @@ a.axi-panel--tile:hover,
|
|
|
425
425
|
button.axi-panel--tile:hover,
|
|
426
426
|
.axi-panel--tile[role='button']:hover { box-shadow: var(--axi-shadow-control-hover); }
|
|
427
427
|
|
|
428
|
+
/* The one of these you picked. A grid of panels, tiles or cards where one is
|
|
429
|
+
chosen - a theme picker, a fight picker, a leader you have opened - and the
|
|
430
|
+
surface has to say which.
|
|
431
|
+
|
|
432
|
+
Not the accent fill a rail item takes. A rail item is a word, and a word can
|
|
433
|
+
be printed on the accent; a panel carries content, and an accent fill drowns
|
|
434
|
+
it. The answer is the third weight .axi-rail__nav--quiet already found for
|
|
435
|
+
the same reason: the surface rises and the accent arrives on the edge. A
|
|
436
|
+
whole edge here rather than the leading one, because a grid is read in two
|
|
437
|
+
directions and has no leading side to light.
|
|
438
|
+
|
|
439
|
+
Keyed off what the element already says, the way .axi-panel--tile keys its
|
|
440
|
+
press off being an <a> or a <button> rather than off a second class. The
|
|
441
|
+
language has one spelling per selection semantics, and this rule takes all
|
|
442
|
+
of them rather than inventing a fourth: `aria-current` for a place you
|
|
443
|
+
navigated to (a rail item, a tab, a table row), `aria-pressed` for a toggle
|
|
444
|
+
(a pill), `aria-selected` for an option in a listbox (a picker option). A
|
|
445
|
+
consumer that marks a card by appending a border utility has told assistive
|
|
446
|
+
tech nothing at all, and that is the defect this replaces rather than the
|
|
447
|
+
missing fill - ten sites in one consumer, six different bases, three
|
|
448
|
+
invented extra marks, and not one of them announced.
|
|
449
|
+
|
|
450
|
+
The fourth arm is a <label> wrapping its own radio, which is the correct
|
|
451
|
+
markup for a picker made of panels: the input carries the state, so the
|
|
452
|
+
label has no attribute to set and copying the state onto one would be a
|
|
453
|
+
second source of truth that can disagree. This is the language's only
|
|
454
|
+
:has(), and the child combinator is the whole point - `> input:checked`
|
|
455
|
+
means this panel IS the control, where a descendant match would light a
|
|
456
|
+
panel up for any checkbox buried in its content.
|
|
457
|
+
|
|
458
|
+
Both spellings on one rule, per "a style only reachable through a layer will
|
|
459
|
+
be re-invented": .axi-card is a shell and .axi-panel a primitive, and a
|
|
460
|
+
consumer marking one of them must not have to find out which file its
|
|
461
|
+
selected state was written in. The fill is the `background` shorthand and
|
|
462
|
+
not the -paint companion, because --axi-surface-raised is a surface token
|
|
463
|
+
and a theme is allowed to make a surface a gradient. */
|
|
464
|
+
:is(.axi-card, .axi-panel):is(
|
|
465
|
+
[aria-current],
|
|
466
|
+
[aria-pressed='true'],
|
|
467
|
+
[aria-selected='true'],
|
|
468
|
+
:has(> input:checked)
|
|
469
|
+
) {
|
|
470
|
+
background: var(--axi-surface-raised);
|
|
471
|
+
border-color: var(--axi-accent);
|
|
472
|
+
}
|
|
473
|
+
|
|
428
474
|
/* ---------- well ---------- */
|
|
429
475
|
/* The panel's inverse: a field sunk into the surface around it rather than
|
|
430
476
|
raised off it. No block, because a recess casts nothing, and the rule rather
|
|
431
477
|
than the ink for its edge - see --axi-well-fill in src/tokens.css for why
|
|
432
478
|
both of those follow from the same fact. Reach for this wherever a second
|
|
433
479
|
panel inside a panel was the instinct: a picker list beside a table, a column
|
|
434
|
-
of cards standing on the page, a trough a bar fills.
|
|
480
|
+
of cards standing on the page, a trough a bar fills.
|
|
481
|
+
|
|
482
|
+
.axi-split__nav is the first of those three named as a part rather than left
|
|
483
|
+
to the consumer to remember, and it is in THIS rule rather than restating the
|
|
484
|
+
fill next to the split pane. Two rules that agree today are two rules that
|
|
485
|
+
disagree after the next edit - see "A style only reachable through a layer
|
|
486
|
+
will be re-invented" in docs/RULES.md, which is the same argument arriving
|
|
487
|
+
from the other direction. The split pane's slot cannot be a well by
|
|
488
|
+
convention: a consumer who forgets the second class gets a picker with no
|
|
489
|
+
recess, and the language has no way to notice. */
|
|
490
|
+
.axi-split__nav,
|
|
435
491
|
.axi-well {
|
|
436
492
|
background: var(--axi-well-fill);
|
|
437
493
|
border: var(--axi-border-control) solid var(--axi-well-line);
|
|
@@ -530,6 +586,43 @@ button.axi-panel--tile:hover,
|
|
|
530
586
|
icon-only button has no label to pad around, so the 20px sides meant for
|
|
531
587
|
one produced a wide rectangle around a single glyph - visible in this
|
|
532
588
|
language's own gallery, in the example titled "Alone in a button". */
|
|
589
|
+
/* Icon over label rather than beside it. Not a style choice - it is the only
|
|
590
|
+
thing that makes a bar of equal actions fit a phone. Measured on the
|
|
591
|
+
consumer that needed it: four icon+label actions side by side want 387px of
|
|
592
|
+
the 337px available at 393px wide, and every label is one unbreakable word,
|
|
593
|
+
so min-content equals max-content and flex-shrink has nothing to give. The
|
|
594
|
+
last action simply runs off the screen. Stacked, the same four come to
|
|
595
|
+
~291px.
|
|
596
|
+
|
|
597
|
+
min-width: 0 because that failure has a second half. A flex item's default
|
|
598
|
+
min-width is its content, so a button in a row refuses to shrink below its
|
|
599
|
+
own label even when the row is told to divide the space - which is the same
|
|
600
|
+
trap .axi-tabs__tab documents for its label and .axi-split__body for its
|
|
601
|
+
track.
|
|
602
|
+
|
|
603
|
+
The label is addressed as an element because a stacked button has two
|
|
604
|
+
children and only one of them is words; naming a part would make every
|
|
605
|
+
consumer add a class to say what the span already says. :where() so it
|
|
606
|
+
spends no specificity - the ink layer and any modifier still out-rank it,
|
|
607
|
+
which is the contract src/utilities.css is owed. */
|
|
608
|
+
.axi-btn--stack {
|
|
609
|
+
flex-direction: column;
|
|
610
|
+
gap: 5px;
|
|
611
|
+
min-width: 0;
|
|
612
|
+
}
|
|
613
|
+
.axi-btn--stack > :where(span) {
|
|
614
|
+
/* Both, and the pair is the point: max-width caps it against the button,
|
|
615
|
+
min-width: 0 is what lets it shrink at all. A flex item's default
|
|
616
|
+
min-width is its content, so max-width alone leaves the ellipsis
|
|
617
|
+
unreachable - the label overflows the cap instead of truncating inside
|
|
618
|
+
it. tests/tokens.test.mjs pins the pair for every truncating rule in the
|
|
619
|
+
language, and it caught this one. */
|
|
620
|
+
max-width: 100%;
|
|
621
|
+
min-width: 0;
|
|
622
|
+
overflow: hidden;
|
|
623
|
+
text-overflow: ellipsis;
|
|
624
|
+
white-space: nowrap;
|
|
625
|
+
}
|
|
533
626
|
.axi-btn--sm { --axi-btn-pad: 8px 13px; --axi-btn-size: 12px; }
|
|
534
627
|
.axi-btn--xs { --axi-btn-pad: 5px 9px; --axi-btn-size: 11px; }
|
|
535
628
|
.axi-btn--icon { --axi-btn-pad: 7px; }
|
|
@@ -828,7 +921,7 @@ button.axi-panel--tile:hover,
|
|
|
828
921
|
says `axi-link axi-ink-dim` must stay dim under the cursor, and the plain
|
|
829
922
|
`:hover` form is what made the danger button read white under one. */
|
|
830
923
|
.axi-link,
|
|
831
|
-
.axi-prose a {
|
|
924
|
+
.axi-prose :where(a) {
|
|
832
925
|
padding: 0;
|
|
833
926
|
background: none;
|
|
834
927
|
border: 0;
|
|
@@ -841,7 +934,7 @@ button.axi-panel--tile:hover,
|
|
|
841
934
|
cursor: pointer;
|
|
842
935
|
}
|
|
843
936
|
.axi-link:where(:hover),
|
|
844
|
-
.axi-prose
|
|
937
|
+
.axi-prose :where(a:hover) { color: var(--axi-text); }
|
|
845
938
|
|
|
846
939
|
/* ---------- quiet action ---------- */
|
|
847
940
|
/* A control that is only its label. Not a button - it draws no box, takes no
|
|
@@ -967,8 +1060,8 @@ button.axi-panel--tile:hover,
|
|
|
967
1060
|
disclosure that drops back flat the moment the pointer moves into the list
|
|
968
1061
|
it opened severs the two: the lift is what says this box and that popover
|
|
969
1062
|
are one control. */
|
|
970
|
-
.axi-select:hover,
|
|
971
|
-
.axi-picker__btn:hover,
|
|
1063
|
+
.axi-select:where(:hover),
|
|
1064
|
+
.axi-picker__btn:where(:hover),
|
|
972
1065
|
.axi-picker__btn[aria-expanded='true'] {
|
|
973
1066
|
color: var(--axi-text);
|
|
974
1067
|
box-shadow: var(--axi-shadow-control);
|
|
@@ -995,7 +1088,7 @@ button.axi-panel--tile:hover,
|
|
|
995
1088
|
backdrop-filter: var(--axi-surface-filter);
|
|
996
1089
|
box-shadow: var(--axi-shadow-panel);
|
|
997
1090
|
}
|
|
998
|
-
.axi-select option {
|
|
1091
|
+
.axi-select :where(option) {
|
|
999
1092
|
display: flex; align-items: center; gap: 9px;
|
|
1000
1093
|
padding: 8px 9px;
|
|
1001
1094
|
border-radius: var(--axi-radius-sm);
|
|
@@ -1010,7 +1103,7 @@ button.axi-panel--tile:hover,
|
|
|
1010
1103
|
level's fill - and this popover is filled with --axi-surface-raised, which
|
|
1011
1104
|
a theme may take translucent, so an opaque row here is the black patch
|
|
1012
1105
|
--axi-well-fill exists to prevent. Same for the picker's rows below. */
|
|
1013
|
-
.axi-select option:hover,
|
|
1106
|
+
.axi-select :where(option:hover, option:focus) {
|
|
1014
1107
|
background: var(--axi-well-fill); color: var(--axi-text);
|
|
1015
1108
|
}
|
|
1016
1109
|
/* The page-wide focus ring sits 2px outside its element; inside a picker
|
|
@@ -1109,7 +1202,7 @@ button.axi-panel--tile:hover,
|
|
|
1109
1202
|
color: transparent;
|
|
1110
1203
|
font-weight: 900;
|
|
1111
1204
|
}
|
|
1112
|
-
.axi-picker__opt:hover,
|
|
1205
|
+
.axi-picker__opt:where(:hover, :focus) {
|
|
1113
1206
|
background: var(--axi-well-fill); color: var(--axi-text);
|
|
1114
1207
|
}
|
|
1115
1208
|
/* The page-wide focus ring sits 2px outside its element; inside a popover
|
|
@@ -1296,6 +1389,68 @@ textarea.axi-input {
|
|
|
1296
1389
|
/* A horizontal run that wraps rather than overflowing. */
|
|
1297
1390
|
.axi-row { display: flex; align-items: center; flex-wrap: wrap; gap: var(--axi-row-gap, 10px); }
|
|
1298
1391
|
|
|
1392
|
+
/* ---------- split pane ---------- */
|
|
1393
|
+
/* A picker on the left choosing what the surface on the right shows. Thirteen
|
|
1394
|
+
sections of one consumer are this shape, and a fourteenth had hand-built it
|
|
1395
|
+
inline because there was no name for it.
|
|
1396
|
+
|
|
1397
|
+
It is two objects on one plane and not two panels, which is the whole reason
|
|
1398
|
+
it needs naming: a picker list beside a table is the first case --axi-well's
|
|
1399
|
+
own comment names, and the thing the list picks is the raised counterpart to
|
|
1400
|
+
that recess. Drawn as two panels it read flat - a hairline parting one field,
|
|
1401
|
+
with the list's scrollbar carving a groove down the middle of it - because
|
|
1402
|
+
nothing in it was an object.
|
|
1403
|
+
|
|
1404
|
+
The body carries the CONTROL step, not the panel's. A split pane lives
|
|
1405
|
+
inside a panel that has already paid a 6px block, and rule 8's counterpart
|
|
1406
|
+
settles what a second one nested in it reads as: two planes arguing. The
|
|
1407
|
+
consumer that derived this shape by hand got the outline and left out the
|
|
1408
|
+
block entirely, which is a rule 3 failure in the other direction - a
|
|
1409
|
+
boundary drawn around content that is standing on the surface behind it.
|
|
1410
|
+
Outlined and blocked, at the weight that says "the contents of a box".
|
|
1411
|
+
|
|
1412
|
+
The radius follows the form step and not the footprint. The body is
|
|
1413
|
+
panel-sized in area and control-weight in form, and .axi-panel--tile already
|
|
1414
|
+
settled that argument the same way.
|
|
1415
|
+
|
|
1416
|
+
`minmax(0, 1fr)` rather than `1fr`: a table in a `1fr` track cannot shrink
|
|
1417
|
+
below its own content, and every cell in .axi-table is `nowrap`, so the
|
|
1418
|
+
column blows out and takes the pane's width with it. `1fr` is
|
|
1419
|
+
`minmax(auto, 1fr)` and `auto` is a content floor.
|
|
1420
|
+
|
|
1421
|
+
No breakpoint of its own. Under 640px the two stack, which is layout.css's
|
|
1422
|
+
one breakpoint and deliberately still its only one - .axi-table--fixed
|
|
1423
|
+
already makes a 360px pane work, so a pane narrow enough to need a second
|
|
1424
|
+
breakpoint is one --fixed was written for. */
|
|
1425
|
+
.axi-split {
|
|
1426
|
+
display: grid;
|
|
1427
|
+
grid-template-columns: var(--axi-split-nav-w, 280px) minmax(0, 1fr);
|
|
1428
|
+
gap: var(--axi-split-gap, 10px);
|
|
1429
|
+
/* A split pane is routinely the flex child that fills a section's height,
|
|
1430
|
+
and a grid that cannot shrink below its content pushes the section past
|
|
1431
|
+
its own cap - the same reason .axi-palette__list says it. */
|
|
1432
|
+
min-height: 0;
|
|
1433
|
+
}
|
|
1434
|
+
/* The picker is a well, declared as one in the same rule as .axi-well in
|
|
1435
|
+
src/primitives.css rather than restated here. This part only says what the
|
|
1436
|
+
slot adds: it scrolls, and it may not out-grow the row it sits in. Its
|
|
1437
|
+
scrollbar is hidden by the language's quiet-scroll rule in
|
|
1438
|
+
src/utilities.css, which is where that decision lives for every strip. */
|
|
1439
|
+
.axi-split__nav { overflow-y: auto; min-height: 0; }
|
|
1440
|
+
.axi-split__body {
|
|
1441
|
+
/* Paired with the track's minmax(0, 1fr): the track stops the COLUMN
|
|
1442
|
+
growing, this stops the ITEM, whose min-width is `auto` by default and is
|
|
1443
|
+
what an ellipsis inside it is actually fighting. */
|
|
1444
|
+
min-width: 0;
|
|
1445
|
+
min-height: 0;
|
|
1446
|
+
overflow: hidden;
|
|
1447
|
+
background: var(--axi-surface);
|
|
1448
|
+
backdrop-filter: var(--axi-surface-filter);
|
|
1449
|
+
border: var(--axi-border-control) solid var(--axi-ink-line);
|
|
1450
|
+
border-radius: var(--axi-radius-sm);
|
|
1451
|
+
box-shadow: var(--axi-shadow-control);
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1299
1454
|
@media (max-width: 640px) {
|
|
1300
1455
|
/* The fallback must match the resting rule's (var(--axi-gutter), 18px) -
|
|
1301
1456
|
README.md documents --axi-page-pad's fallback as --axi-gutter, and a
|
|
@@ -1303,6 +1458,12 @@ textarea.axi-input {
|
|
|
1303
1458
|
mobile, which is the one viewport where the gutter matters most. */
|
|
1304
1459
|
.axi-page { padding-inline: var(--axi-page-pad, var(--axi-gutter)); }
|
|
1305
1460
|
.axi-grid { grid-template-columns: 1fr; }
|
|
1461
|
+
/* The picker stops being a column beside the thing it picks and becomes the
|
|
1462
|
+
row above it. It keeps a height cap - a picker that grows to twenty rows
|
|
1463
|
+
pushes the result it picked off the screen, which is the one thing the
|
|
1464
|
+
stacked form must not do. */
|
|
1465
|
+
.axi-split { grid-template-columns: 1fr; }
|
|
1466
|
+
.axi-split__nav { max-height: var(--axi-split-nav-h, 200px); }
|
|
1306
1467
|
}
|
|
1307
1468
|
|
|
1308
1469
|
/* --- shells.css --- */
|
|
@@ -1311,14 +1472,23 @@ textarea.axi-input {
|
|
|
1311
1472
|
redefining them, so a change to .axi-panel moves the notice, the toolbar,
|
|
1312
1473
|
the popover and the card together. */
|
|
1313
1474
|
|
|
1314
|
-
/* The small uppercase heading that labels a panel's contents.
|
|
1475
|
+
/* The small uppercase heading that labels a panel's contents.
|
|
1476
|
+
|
|
1477
|
+
The gap under it is a knob because the eyebrow heads more than one size of
|
|
1478
|
+
thing. 14px is the figure for a panel at its resting padding; a tile is one
|
|
1479
|
+
form step down and its padding is one density step down with it, so the
|
|
1480
|
+
eyebrow inside one reads the tighter figure in the rule below, the same way
|
|
1481
|
+
.axi-panel--tile reads --axi-panel-pad at a tighter fallback of its own. An
|
|
1482
|
+
eyebrow sitting in a row - a sheet's head, a toolbar - wants no gap at all,
|
|
1483
|
+
and says so with the knob rather than with an inline margin. */
|
|
1315
1484
|
.axi-eyebrow {
|
|
1316
|
-
margin: 0 0 14px;
|
|
1485
|
+
margin: 0 0 var(--axi-eyebrow-gap, 14px);
|
|
1317
1486
|
font: var(--axi-t-eyebrow);
|
|
1318
1487
|
letter-spacing: var(--axi-ls-eyebrow);
|
|
1319
1488
|
text-transform: uppercase;
|
|
1320
1489
|
color: var(--axi-text-faint);
|
|
1321
1490
|
}
|
|
1491
|
+
.axi-panel--tile .axi-eyebrow { margin-bottom: var(--axi-eyebrow-gap, 6px); }
|
|
1322
1492
|
|
|
1323
1493
|
/* ---------- masthead ---------- */
|
|
1324
1494
|
/* Opaque, because content scrolls under it. Which is also why it paints the
|
|
@@ -1403,9 +1573,35 @@ textarea.axi-input {
|
|
|
1403
1573
|
it shrinks an item - so one tab too many sent the last item in source order
|
|
1404
1574
|
to a second row instead of taking width off the widest one. The strip is the
|
|
1405
1575
|
item that can give: it scrolls, and the row stays a row. */
|
|
1406
|
-
.axi-mast .axi-tabs { margin-left: auto;
|
|
1407
|
-
|
|
1408
|
-
|
|
1576
|
+
.axi-mast .axi-tabs { margin-left: auto; }
|
|
1577
|
+
/* A strip with more tabs than room scrolls rather than losing them.
|
|
1578
|
+
|
|
1579
|
+
.axi-tabs is `display: flex` with no wrap, so a strip too wide for its
|
|
1580
|
+
container overflows - and outside a scroll container the tabs past the edge
|
|
1581
|
+
are simply unreachable. The language already believed this twice and could
|
|
1582
|
+
say it in neither place a consumer could use: once here, behind .axi-mast,
|
|
1583
|
+
and once at the 640px breakpoint below, where EVERY strip scrolls. A
|
|
1584
|
+
standalone strip above that breakpoint with one tab too many had nowhere to
|
|
1585
|
+
look, and the consumer that hit it wrote `overflow-x: auto` on its own
|
|
1586
|
+
wrapper - the same re-invention docs/RULES.md describes, arriving a fourth
|
|
1587
|
+
time.
|
|
1588
|
+
|
|
1589
|
+
A modifier rather than the base rule, and this is the part worth arguing.
|
|
1590
|
+
`overflow-x: auto` computes `overflow-y` from `visible` to `auto`, so a
|
|
1591
|
+
strip that scrolls is also a strip that clips vertically - a focus ring, a
|
|
1592
|
+
badge hanging off a tab, a popover anchored to one. Every strip paying that
|
|
1593
|
+
to fix the strips that are too long is the wrong trade; the strips that are
|
|
1594
|
+
too long say so.
|
|
1595
|
+
|
|
1596
|
+
The bar is NOT hidden here, and the quiet-scroll rule in src/utilities.css
|
|
1597
|
+
deliberately does not name this. That rule is about a bar drawn down the
|
|
1598
|
+
side of a narrow strip, where the object's own edge is already a vertical
|
|
1599
|
+
line and a count elsewhere already says how many rows there are. A tab strip
|
|
1600
|
+
that scrolls horizontally has no second signal: the bar is the only thing
|
|
1601
|
+
saying there are more tabs. Same reason .axi-table__scroll keeps its. */
|
|
1602
|
+
.axi-tabs--scroll,
|
|
1603
|
+
.axi-mast .axi-tabs { flex-shrink: 1; min-width: 0; overflow-x: auto; }
|
|
1604
|
+
.axi-tabs :where(a, button) {
|
|
1409
1605
|
padding: 8px 16px;
|
|
1410
1606
|
font: 800 13.5px/1 var(--axi-sans);
|
|
1411
1607
|
color: var(--axi-text-dim); text-decoration: none;
|
|
@@ -1414,8 +1610,7 @@ textarea.axi-input {
|
|
|
1414
1610
|
border: var(--axi-border-control) solid transparent;
|
|
1415
1611
|
border-radius: var(--axi-radius-sm);
|
|
1416
1612
|
}
|
|
1417
|
-
.axi-tabs a:hover,
|
|
1418
|
-
.axi-tabs button:hover { color: var(--axi-text); border-color: var(--axi-rule); }
|
|
1613
|
+
.axi-tabs :where(a:hover, button:hover) { color: var(--axi-text); border-color: var(--axi-rule); }
|
|
1419
1614
|
/* The current tab is filled and blocked - the same treatment a pressed pill
|
|
1420
1615
|
gets, because it is the same idea: this one is on. */
|
|
1421
1616
|
.axi-tabs a[aria-current="page"],
|
|
@@ -1447,13 +1642,13 @@ textarea.axi-input {
|
|
|
1447
1642
|
text-overflow: ellipsis;
|
|
1448
1643
|
white-space: nowrap;
|
|
1449
1644
|
}
|
|
1450
|
-
.axi-tabs .axi-tabs__close {
|
|
1645
|
+
.axi-tabs :where(.axi-tabs__close) {
|
|
1451
1646
|
padding: 0 6px;
|
|
1452
1647
|
font: var(--axi-t-micro);
|
|
1453
1648
|
color: var(--axi-text-faint);
|
|
1454
1649
|
border-color: transparent;
|
|
1455
1650
|
}
|
|
1456
|
-
.axi-tabs .axi-tabs__close:hover { color: var(--axi-text); border-color: transparent; }
|
|
1651
|
+
.axi-tabs :where(.axi-tabs__close:hover) { color: var(--axi-text); border-color: transparent; }
|
|
1457
1652
|
|
|
1458
1653
|
/* ---------- rail ---------- */
|
|
1459
1654
|
/* Vertical navigation, for a set too large for a tab strip to hold. Same
|
|
@@ -1527,7 +1722,7 @@ textarea.axi-input {
|
|
|
1527
1722
|
border-radius: var(--axi-radius-sm);
|
|
1528
1723
|
cursor: pointer;
|
|
1529
1724
|
}
|
|
1530
|
-
.axi-rail__item:hover { color: var(--axi-text); background: var(--axi-surface-raised); }
|
|
1725
|
+
.axi-rail__item:where(:hover) { color: var(--axi-text); background: var(--axi-surface-raised); }
|
|
1531
1726
|
/* The same treatment a pressed pill and a current tab get. Only ever one of
|
|
1532
1727
|
these on screen. Any aria-current value, because the right one depends on
|
|
1533
1728
|
what the rail navigates - `page` for a view, `location` for a place within
|
|
@@ -1590,7 +1785,7 @@ textarea.axi-input {
|
|
|
1590
1785
|
border-radius: var(--axi-radius-sm);
|
|
1591
1786
|
cursor: pointer;
|
|
1592
1787
|
}
|
|
1593
|
-
.axi-rail__subitem:hover { color: var(--axi-text-dim); background: var(--axi-surface-raised); }
|
|
1788
|
+
.axi-rail__subitem:where(:hover) { color: var(--axi-text-dim); background: var(--axi-surface-raised); }
|
|
1594
1789
|
.axi-rail__subitem[aria-current] { color: var(--axi-text); }
|
|
1595
1790
|
/* An annotation on an item, not a measurement of it, so rule 5 draws it
|
|
1596
1791
|
outlined rather than filled. */
|
|
@@ -1738,13 +1933,13 @@ textarea.axi-input {
|
|
|
1738
1933
|
box-shadow: var(--axi-shadow-panel);
|
|
1739
1934
|
}
|
|
1740
1935
|
.axi-menu__pop[hidden] { display: none; }
|
|
1741
|
-
.axi-menu__pop label {
|
|
1936
|
+
.axi-menu__pop :where(label) {
|
|
1742
1937
|
display: flex; gap: 9px; align-items: flex-start;
|
|
1743
1938
|
padding: 7px 8px; border-radius: var(--axi-radius-sm);
|
|
1744
1939
|
font-size: 13px; font-weight: 600; color: var(--axi-text-dim); cursor: pointer;
|
|
1745
1940
|
}
|
|
1746
1941
|
/* A hovered row sinks - the same fill the select's and the picker's rows use. */
|
|
1747
|
-
.axi-menu__pop label:hover { background: var(--axi-well-fill); color: var(--axi-text); }
|
|
1942
|
+
.axi-menu__pop :where(label:hover) { background: var(--axi-well-fill); color: var(--axi-text); }
|
|
1748
1943
|
.axi-menu__pop input { margin: 3px 0 0; }
|
|
1749
1944
|
|
|
1750
1945
|
/* ---------- command palette ---------- */
|
|
@@ -1805,14 +2000,16 @@ textarea.axi-input {
|
|
|
1805
2000
|
}
|
|
1806
2001
|
/* min-height: 0 because the panel is a flex column and a list that cannot
|
|
1807
2002
|
shrink below its content pushes the panel past its own max-height. The
|
|
1808
|
-
scrollbar is hidden
|
|
1809
|
-
|
|
1810
|
-
|
|
2003
|
+
scrollbar is hidden, but not here: that decision is the language's and not
|
|
2004
|
+
this component's, and it now lives once, in src/utilities.css under "the
|
|
2005
|
+
quiet scroll", with this selector in the same rule as every other strip that
|
|
2006
|
+
makes it. The argument that produced it was written here first - at twelve
|
|
2007
|
+
rows behind a fixed cap the bar is a channel carved down the side of a small
|
|
2008
|
+
panel to report a fact the row count in the bar has already given - and it is
|
|
2009
|
+
preserved in full at the rule. */
|
|
1811
2010
|
.axi-palette__list {
|
|
1812
2011
|
flex: 1; min-height: 0; overflow-y: auto; padding: 4px 0;
|
|
1813
|
-
scrollbar-width: none;
|
|
1814
2012
|
}
|
|
1815
|
-
.axi-palette__list::-webkit-scrollbar { width: 0; height: 0; }
|
|
1816
2013
|
/* One .axi-palette__section per group of rows, each opening with a
|
|
1817
2014
|
.axi-palette__group heading. The section is a named part rather than left to
|
|
1818
2015
|
the consumer's wrapper so the rule between groups can be stated as what it
|
|
@@ -1834,7 +2031,7 @@ textarea.axi-input {
|
|
|
1834
2031
|
color: var(--axi-text-dim);
|
|
1835
2032
|
font: var(--axi-t-small); text-align: left; cursor: pointer;
|
|
1836
2033
|
}
|
|
1837
|
-
.axi-palette__row:hover { background: var(--axi-surface-raised); color: var(--axi-text); }
|
|
2034
|
+
.axi-palette__row:where(:hover) { background: var(--axi-surface-raised); color: var(--axi-text); }
|
|
1838
2035
|
.axi-palette__row[data-active] { background: var(--axi-accent); color: var(--axi-accent-ink); }
|
|
1839
2036
|
/* The row's own ink is set above, but a row carries a glyph and a second line
|
|
1840
2037
|
of text that were coloured on their own; on the filled row they all become
|
|
@@ -1866,7 +2063,7 @@ textarea.axi-input {
|
|
|
1866
2063
|
cursor: pointer;
|
|
1867
2064
|
transition: transform .1s, box-shadow .1s, color .1s;
|
|
1868
2065
|
}
|
|
1869
|
-
.axi-palette__trigger:hover {
|
|
2066
|
+
.axi-palette__trigger:where(:hover) {
|
|
1870
2067
|
color: var(--axi-text);
|
|
1871
2068
|
box-shadow: var(--axi-shadow-control);
|
|
1872
2069
|
transform: translate(-2px, -2px);
|
|
@@ -2013,7 +2210,7 @@ textarea.axi-input {
|
|
|
2013
2210
|
color: var(--axi-text-faint);
|
|
2014
2211
|
}
|
|
2015
2212
|
.axi-card__go { margin-left: auto; color: var(--axi-accent); font-weight: 900; text-transform: uppercase; letter-spacing: .05em; }
|
|
2016
|
-
.axi-card:hover .axi-card__go { color: var(--axi-text); }
|
|
2213
|
+
.axi-card:where(:hover) :where(.axi-card__go) { color: var(--axi-text); }
|
|
2017
2214
|
|
|
2018
2215
|
/* ---------- drawer ---------- */
|
|
2019
2216
|
/* A scrim's layer is not a property of the scrim. It is "directly below the
|
|
@@ -2086,7 +2283,7 @@ textarea.axi-input {
|
|
|
2086
2283
|
font-size: 15px; font-weight: 900; cursor: pointer;
|
|
2087
2284
|
transition: transform .1s, box-shadow .1s;
|
|
2088
2285
|
}
|
|
2089
|
-
.axi-drawer__close:hover {
|
|
2286
|
+
.axi-drawer__close:where(:hover) {
|
|
2090
2287
|
background: var(--axi-accent); color: var(--axi-accent-ink);
|
|
2091
2288
|
box-shadow: var(--axi-shadow-control);
|
|
2092
2289
|
transform: translate(-2px, -2px);
|
|
@@ -2228,7 +2425,7 @@ textarea.axi-input {
|
|
|
2228
2425
|
-webkit-app-region: drag;
|
|
2229
2426
|
}
|
|
2230
2427
|
.axi-titlebar__btns { margin-left: auto; display: flex; height: 100%; -webkit-app-region: no-drag; }
|
|
2231
|
-
.axi-titlebar__btns button {
|
|
2428
|
+
.axi-titlebar__btns :where(button) {
|
|
2232
2429
|
width: 44px;
|
|
2233
2430
|
border: 0;
|
|
2234
2431
|
background: transparent;
|
|
@@ -2239,10 +2436,10 @@ textarea.axi-input {
|
|
|
2239
2436
|
/* No lift here. A window button is chrome, not content: it has nothing to
|
|
2240
2437
|
lift off, and 44px of movement in the corner of every screen is exactly
|
|
2241
2438
|
the scattered motion rule 4 rations. It answers on the neutral ramp. */
|
|
2242
|
-
.axi-titlebar__btns button:hover { background: var(--axi-surface); color: var(--axi-text); }
|
|
2439
|
+
.axi-titlebar__btns :where(button:hover) { background: var(--axi-surface); color: var(--axi-text); }
|
|
2243
2440
|
/* Close is the one destructive control in the chrome, and the one place a
|
|
2244
2441
|
status ink belongs in a titlebar. */
|
|
2245
|
-
.axi-titlebar__btns button:last-child:hover { background: var(--axi-danger); color: var(--axi-ink-on-fill); }
|
|
2442
|
+
.axi-titlebar__btns :where(button:last-child:hover) { background: var(--axi-danger); color: var(--axi-ink-on-fill); }
|
|
2246
2443
|
/* The scrolling region under the chrome. */
|
|
2247
2444
|
.axi-window__body { flex: 1; overflow-y: auto; }
|
|
2248
2445
|
|
|
@@ -2261,8 +2458,8 @@ textarea.axi-input {
|
|
|
2261
2458
|
flex-wrap: wrap;
|
|
2262
2459
|
font: var(--axi-t-small);
|
|
2263
2460
|
}
|
|
2264
|
-
.axi-crumbs a { color: var(--axi-text-dim); text-decoration: none; }
|
|
2265
|
-
.axi-crumbs a:hover { color: var(--axi-accent); }
|
|
2461
|
+
.axi-crumbs :where(a) { color: var(--axi-text-dim); text-decoration: none; }
|
|
2462
|
+
.axi-crumbs :where(a:hover) { color: var(--axi-accent); }
|
|
2266
2463
|
/* 7px, matching the prose bullet (src/prose.css) exactly, with the same
|
|
2267
2464
|
--axi-border-hairline outline: rule 7 calls the motif "a 45deg-rotated
|
|
2268
2465
|
outlined square", and a bare fill with no border does not match it. The
|
|
@@ -2296,7 +2493,7 @@ textarea.axi-input {
|
|
|
2296
2493
|
border: var(--axi-border-control) solid var(--axi-ink-line);
|
|
2297
2494
|
border-radius: var(--axi-radius-sm);
|
|
2298
2495
|
}
|
|
2299
|
-
.axi-pages__n:hover { color: var(--axi-text); }
|
|
2496
|
+
.axi-pages__n:where(:hover) { color: var(--axi-text); }
|
|
2300
2497
|
.axi-pages__n[aria-current="page"] {
|
|
2301
2498
|
background: var(--axi-accent);
|
|
2302
2499
|
color: var(--axi-accent-ink);
|
|
@@ -2309,7 +2506,12 @@ textarea.axi-input {
|
|
|
2309
2506
|
@media (max-width: 640px) {
|
|
2310
2507
|
.axi-mast__in { min-height: 0; padding: 10px 16px; gap: 12px; }
|
|
2311
2508
|
.axi-mast .axi-tabs { margin-left: 0; }
|
|
2312
|
-
|
|
2509
|
+
/* Every strip scrolls on a phone, whether or not it asked. This cannot join
|
|
2510
|
+
the shared rule above - a media-query rule and an unconditional one are
|
|
2511
|
+
different rules by construction - so the values are pinned against it by
|
|
2512
|
+
test instead: the arm may declare nothing the modifier does not, and
|
|
2513
|
+
nothing at a different value. */
|
|
2514
|
+
.axi-tabs { width: 100%; flex-shrink: 1; min-width: 0; overflow-x: auto; }
|
|
2313
2515
|
/* The offset blocks eat horizontal room a phone does not have, so the panel
|
|
2314
2516
|
step drops to the control step here rather than to an invented number.
|
|
2315
2517
|
The card's hover has to come down with it - left alone it kept deepening
|
|
@@ -2601,7 +2803,17 @@ textarea.axi-input {
|
|
|
2601
2803
|
resting cell rule (`.axi-table :where(td, tbody th)`, also one class) and
|
|
2602
2804
|
wins on order, while an ink class in utilities.css - one class, later file -
|
|
2603
2805
|
wins over both. */
|
|
2604
|
-
|
|
2806
|
+
/* A DATA FILL SURVIVES A ROW STATE, and the `:not()` sits inside `:where()`
|
|
2807
|
+
so saying so costs no specificity. A matrix cell's band IS its value (rule 9,
|
|
2808
|
+
"the matrix is the bound on not at all") - washing it on the way past the row
|
|
2809
|
+
you want deletes the reading. The row state is not weakened by this: it is
|
|
2810
|
+
drawn by the leading edge below, and by every cell that has nothing to say.
|
|
2811
|
+
Which is the edge-not-fill answer the selection rule below reaches on its own
|
|
2812
|
+
grounds, arriving here a second time.
|
|
2813
|
+
|
|
2814
|
+
Nothing changes for a table with no `data-heat` in it, which is every table
|
|
2815
|
+
in this language but one. */
|
|
2816
|
+
.axi-table :where(tbody tr:hover) :where(td:not([data-heat]), th:not([data-heat])) {
|
|
2605
2817
|
background: var(--axi-surface-raised);
|
|
2606
2818
|
background-attachment: fixed;
|
|
2607
2819
|
color: var(--axi-text);
|
|
@@ -2636,7 +2848,7 @@ textarea.axi-input {
|
|
|
2636
2848
|
.axi-table :where(tbody tr) > :where(td, th):first-child {
|
|
2637
2849
|
border-inline-start: var(--axi-border-control) solid transparent;
|
|
2638
2850
|
}
|
|
2639
|
-
.axi-table tbody tr[aria-current] :is(td, th) {
|
|
2851
|
+
.axi-table tbody tr[aria-current] :is(td, th):where(:not([data-heat])) {
|
|
2640
2852
|
background: var(--axi-surface-raised);
|
|
2641
2853
|
background-attachment: fixed;
|
|
2642
2854
|
color: var(--axi-text);
|
|
@@ -2797,6 +3009,115 @@ textarea.axi-input {
|
|
|
2797
3009
|
}
|
|
2798
3010
|
.axi-table th:first-child .axi-table__sort { justify-content: flex-start; }
|
|
2799
3011
|
|
|
3012
|
+
/* ---------- matrix ---------- */
|
|
3013
|
+
/* Two categorical axes and a quantity where they cross: forty players down the
|
|
3014
|
+
side, sixty five-second buckets across, a count in each cell. Rule 9's
|
|
3015
|
+
"the matrix is the bound on not at all" is the whole argument for this
|
|
3016
|
+
existing at all, and the short form is: the plane is spent, so there is no
|
|
3017
|
+
third dimension left to give the quantity a LENGTH, and both axes are
|
|
3018
|
+
already sorted by something the reader needs so neither can be re-sorted by
|
|
3019
|
+
the value. Intensity is what is left, and it is admissible here because the
|
|
3020
|
+
cell also prints its number - the digit is the legible copy rule 9 demands,
|
|
3021
|
+
and the band is only what lets the eye find the shape without reading two
|
|
3022
|
+
thousand figures one at a time. A matrix cell with nothing written in it is
|
|
3023
|
+
a heatmap and rule 9 refuses it.
|
|
3024
|
+
|
|
3025
|
+
This is a MODIFIER and not a component, and that is the finding rather than
|
|
3026
|
+
a convenience. Everything structural a matrix field needs, this table
|
|
3027
|
+
already had: --sticky for the ruler that stays, --pinned for the names that
|
|
3028
|
+
stay, --dense for sixty columns' padding, --fixed plus a <colgroup> for
|
|
3029
|
+
proportional cells, and .axi-table__scroll for the frame they move inside.
|
|
3030
|
+
The consumer measured against here had hand-written all five, and had got
|
|
3031
|
+
--sticky wrong in exactly the way that modifier's comment warns about: its
|
|
3032
|
+
ruler was --axi-surface-raised, so under glass the rows slid through it.
|
|
3033
|
+
The only thing genuinely missing was the quantity.
|
|
3034
|
+
|
|
3035
|
+
What the modifier changes is the CELL, which stops being a number in a
|
|
3036
|
+
column and becomes a patch in a field: centred rather than right-aligned,
|
|
3037
|
+
because a band is read by position and not by its digits lining up, and
|
|
3038
|
+
unpadded because the padding is the field. The name column keeps the base
|
|
3039
|
+
table's left alignment and its gutter - it is still a name. */
|
|
3040
|
+
.axi-table--matrix { border-collapse: separate; border-spacing: 0; }
|
|
3041
|
+
.axi-table--matrix thead th { padding: 0 0 6px; text-align: center; }
|
|
3042
|
+
.axi-table--matrix :is(td, tbody th) {
|
|
3043
|
+
padding: 0;
|
|
3044
|
+
text-align: center;
|
|
3045
|
+
height: var(--axi-matrix-cell, 24px);
|
|
3046
|
+
}
|
|
3047
|
+
/* `min-width` and not `width`: under the default auto layout this is the floor
|
|
3048
|
+
a one-digit column cannot fall below, and under --fixed a <colgroup> takes
|
|
3049
|
+
over entirely (fixed layout ignores both min and max - see --fixed). */
|
|
3050
|
+
.axi-table--matrix :is(td, tbody th):not(:first-child) {
|
|
3051
|
+
min-width: var(--axi-matrix-cell, 24px);
|
|
3052
|
+
}
|
|
3053
|
+
.axi-table--matrix :is(td, tbody th):first-child {
|
|
3054
|
+
text-align: left;
|
|
3055
|
+
padding: 0 10px 0 0;
|
|
3056
|
+
}
|
|
3057
|
+
/* The four bands. Each is a color-mix() of the accent into the surface's flat
|
|
3058
|
+
companion, which makes every one of them a computed OPAQUE colour - the
|
|
3059
|
+
accent at 18% alpha over the field would be rule 2's faded ink, and it also
|
|
3060
|
+
fails on its own terms, because the cells a reader is scanning for are the
|
|
3061
|
+
quiet ones and an alpha ramp is where those disappear.
|
|
3062
|
+
|
|
3063
|
+
--axi-surface-paint and not --axi-surface, and this is the trap: a surface
|
|
3064
|
+
token is allowed to hold a gradient (rule 1's one relief, which is how a
|
|
3065
|
+
glass theme exists), color-mix() takes colours and nothing else, and an
|
|
3066
|
+
invalid color-mix() is dropped at computed-value time. Spell it --axi-surface
|
|
3067
|
+
and the bands do not fade - they vanish, silently, in whichever theme paints
|
|
3068
|
+
a gradient. That is measured, not hypothetical: it is why
|
|
3069
|
+
--axi-surface-paint exists (see tokens.css).
|
|
3070
|
+
|
|
3071
|
+
Four steps and not a continuous ramp, because a band is also where the digit
|
|
3072
|
+
has to change ink, and a step is the only thing a flip can happen ON. The
|
|
3073
|
+
top two carry --axi-accent-ink for the same reason .axi-btn--primary does:
|
|
3074
|
+
past roughly half strength the field is the accent and the text on it is the
|
|
3075
|
+
accent's companion, not the text ramp. */
|
|
3076
|
+
.axi-table--matrix :is(td, tbody th)[data-heat='1'] {
|
|
3077
|
+
background-color: color-mix(in srgb, var(--axi-accent) 18%, var(--axi-surface-paint));
|
|
3078
|
+
}
|
|
3079
|
+
.axi-table--matrix :is(td, tbody th)[data-heat='2'] {
|
|
3080
|
+
background-color: color-mix(in srgb, var(--axi-accent) 42%, var(--axi-surface-paint));
|
|
3081
|
+
}
|
|
3082
|
+
.axi-table--matrix :is(td, tbody th)[data-heat='3'] {
|
|
3083
|
+
background-color: color-mix(in srgb, var(--axi-accent) 70%, var(--axi-surface-paint));
|
|
3084
|
+
color: var(--axi-accent-ink);
|
|
3085
|
+
}
|
|
3086
|
+
.axi-table--matrix :is(td, tbody th)[data-heat='4'] {
|
|
3087
|
+
background-color: color-mix(in srgb, var(--axi-accent) 100%, var(--axi-surface-paint));
|
|
3088
|
+
color: var(--axi-accent-ink);
|
|
3089
|
+
}
|
|
3090
|
+
/* A matrix whose columns are a TIMELINE rather than a list of categories, which
|
|
3091
|
+
changes two things and only two.
|
|
3092
|
+
|
|
3093
|
+
The column labels move left. A label over a category names the column and
|
|
3094
|
+
belongs centred over it; a label on a ruler names a MOMENT, and centring it
|
|
3095
|
+
puts the text half a cell to the right of the instant it is pointing at.
|
|
3096
|
+
|
|
3097
|
+
And the division gets a line. `[data-tick]` marks the columns that carry a
|
|
3098
|
+
label - every thirtieth second, not every bucket, because at a five-second
|
|
3099
|
+
resolution a five-minute fight is sixty columns and a timestamp over each is
|
|
3100
|
+
unreadable at the width a cell allows. The line is the RULE, at the hairline
|
|
3101
|
+
step: it is the same line that parts the rows, continued down the field,
|
|
3102
|
+
which is the whole reason it is not drawn on every column. Sixty ruled
|
|
3103
|
+
columns is a spreadsheet; the bands are meant to be the figure. */
|
|
3104
|
+
.axi-table--ruler thead th { text-align: left; }
|
|
3105
|
+
.axi-table--ruler :is(thead th, td, tbody th)[data-tick] {
|
|
3106
|
+
border-left: var(--axi-border-hairline) solid var(--axi-rule);
|
|
3107
|
+
}
|
|
3108
|
+
/* A change of category down the rows - a subgroup, a team, a date. Heavier than
|
|
3109
|
+
the hairline that parts two rows of one group, and heavier in INK rather than
|
|
3110
|
+
in weight: the same line, drawn darker. Going up a form step instead would
|
|
3111
|
+
put a control-weight line inside running content, which is rule 8's grid of
|
|
3112
|
+
boxes, and the boundary would then compete with the head's own lid.
|
|
3113
|
+
|
|
3114
|
+
An attribute and not a class, per rule 13, and on the row because that is
|
|
3115
|
+
what starts: the consumer compares one row's category with the previous
|
|
3116
|
+
row's, which is a fact only it can know. */
|
|
3117
|
+
.axi-table tbody tr[data-group-start] > :is(td, th) {
|
|
3118
|
+
border-top: var(--axi-border-hairline) solid var(--axi-ink-line);
|
|
3119
|
+
}
|
|
3120
|
+
|
|
2800
3121
|
/* ---------- meter ---------- */
|
|
2801
3122
|
/* Rule 9: a proportion is a length. The track is the ground, the fill is the
|
|
2802
3123
|
value, and the fill is one ink at full strength - a tinted or faded bar is
|
|
@@ -2846,6 +3167,61 @@ textarea.axi-input {
|
|
|
2846
3167
|
font-variant-numeric: tabular-nums;
|
|
2847
3168
|
}
|
|
2848
3169
|
|
|
3170
|
+
/* ---------- readout ---------- */
|
|
3171
|
+
/* A short run of labelled readings: logs seen, uploaded, failed; the session's
|
|
3172
|
+
start and its length; a setting and the switch that sets it. The shape of
|
|
3173
|
+
every status card in a dashboard's side column, and the one the consumer
|
|
3174
|
+
that had four of them built by hand each time.
|
|
3175
|
+
|
|
3176
|
+
Rule 8 settles how it is drawn. The eye runs down the value column - that is
|
|
3177
|
+
the whole point of stacking the readings - so it is a table's interior:
|
|
3178
|
+
rows parted by the rule at the hairline weight, no outline and no block on
|
|
3179
|
+
any row, nothing raised inside the panel that is already the raised thing.
|
|
3180
|
+
It is not a table element because it is two cells wide, has no head, and
|
|
3181
|
+
the reading in the second cell is as often a control as a figure; a <dl> is
|
|
3182
|
+
what the document structure calls for and the classes sit on it.
|
|
3183
|
+
|
|
3184
|
+
The key is the meter list's name and the value is the meter list's value,
|
|
3185
|
+
restated here rather than aliased because the two lists are laid out
|
|
3186
|
+
differently and rule-8's test - is there a column? - is the same for both.
|
|
3187
|
+
|
|
3188
|
+
Row padding is a knob at two fallbacks, like the eyebrow's gap: 7px at
|
|
3189
|
+
panel density, and 3px inside a tile, where a hairline already holds the
|
|
3190
|
+
rows apart and 3px is still a clear gap at this type size. */
|
|
3191
|
+
.axi-readout {
|
|
3192
|
+
display: flex;
|
|
3193
|
+
flex-direction: column;
|
|
3194
|
+
margin: 0;
|
|
3195
|
+
}
|
|
3196
|
+
.axi-readout__row {
|
|
3197
|
+
display: flex;
|
|
3198
|
+
align-items: center;
|
|
3199
|
+
justify-content: space-between;
|
|
3200
|
+
gap: 10px;
|
|
3201
|
+
padding: var(--axi-readout-pad, 7px) 0;
|
|
3202
|
+
}
|
|
3203
|
+
.axi-readout__row + .axi-readout__row {
|
|
3204
|
+
border-top: var(--axi-border-hairline) solid var(--axi-rule);
|
|
3205
|
+
}
|
|
3206
|
+
.axi-readout__k {
|
|
3207
|
+
min-width: 0;
|
|
3208
|
+
margin: 0;
|
|
3209
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
3210
|
+
font: var(--axi-t-small);
|
|
3211
|
+
font-weight: 700;
|
|
3212
|
+
color: var(--axi-text-dim);
|
|
3213
|
+
}
|
|
3214
|
+
.axi-readout__v {
|
|
3215
|
+
flex: none;
|
|
3216
|
+
margin: 0;
|
|
3217
|
+
text-align: right;
|
|
3218
|
+
font: var(--axi-t-micro);
|
|
3219
|
+
letter-spacing: var(--axi-ls-micro);
|
|
3220
|
+
color: var(--axi-text);
|
|
3221
|
+
font-variant-numeric: tabular-nums;
|
|
3222
|
+
}
|
|
3223
|
+
.axi-panel--tile .axi-readout__row { padding-block: var(--axi-readout-pad, 3px); }
|
|
3224
|
+
|
|
2849
3225
|
/* ---------- bars ---------- */
|
|
2850
3226
|
/* The same rule stood on end. The baseline is drawn at the control weight
|
|
2851
3227
|
because it is an axis - the one line in a chart that is structure rather
|
|
@@ -2975,6 +3351,34 @@ textarea.axi-input {
|
|
|
2975
3351
|
text-transform: uppercase;
|
|
2976
3352
|
color: var(--axi-text-dim);
|
|
2977
3353
|
}
|
|
3354
|
+
/* A key you can press, to isolate the series it names. The state is the
|
|
3355
|
+
attribute rule 13 asks for: `aria-pressed="true"` on the key that is
|
|
3356
|
+
isolated, and nothing at all on the others - which of them recede is the
|
|
3357
|
+
legend's to work out, from whether any key is pressed, rather than a second
|
|
3358
|
+
attribute the consumer has to compute and keep in step.
|
|
3359
|
+
|
|
3360
|
+
Receding is a step down the neutral ramp and not an opacity, which is rule
|
|
3361
|
+
4's "nothing fades" reaching the legend: the consumer this was written for
|
|
3362
|
+
had the rest of the keys at `opacity: .3` and they were unreadable, which
|
|
3363
|
+
is the one thing a legend must not be. A receded key brightens back to
|
|
3364
|
+
plain under the cursor so it can be found again - the hover is wrapped so it
|
|
3365
|
+
costs nothing against an ink, and the receded rule steps aside for it.
|
|
3366
|
+
|
|
3367
|
+
The button reset and the hover are :where()-wrapped for the reason the ink
|
|
3368
|
+
layer gives: an inked key keeps its ink. The pressed and receded colours are
|
|
3369
|
+
the state's own meaning and stay at full weight. */
|
|
3370
|
+
.axi-legend__key:where(button) {
|
|
3371
|
+
padding: 0;
|
|
3372
|
+
background: none;
|
|
3373
|
+
border: 0;
|
|
3374
|
+
cursor: pointer;
|
|
3375
|
+
transition: color .1s;
|
|
3376
|
+
}
|
|
3377
|
+
.axi-legend__key:where(button:hover) { color: var(--axi-text); }
|
|
3378
|
+
.axi-legend__key[aria-pressed="true"] { color: var(--axi-text); }
|
|
3379
|
+
.axi-legend:has([aria-pressed="true"]) .axi-legend__key:where(:not([aria-pressed="true"], :hover)) {
|
|
3380
|
+
color: var(--axi-text-faint);
|
|
3381
|
+
}
|
|
2978
3382
|
|
|
2979
3383
|
/* --- feedback.css --- */
|
|
2980
3384
|
/* axi design language - feedback.
|
|
@@ -3147,8 +3551,8 @@ textarea.axi-input {
|
|
|
3147
3551
|
|
|
3148
3552
|
/* --- utilities.css --- */
|
|
3149
3553
|
/* axi design language - utilities.
|
|
3150
|
-
Two families of single-purpose class and one
|
|
3151
|
-
the cascade
|
|
3554
|
+
Two families of single-purpose class, one state and one refusal, and the file
|
|
3555
|
+
is last in the cascade
|
|
3152
3556
|
because that is the whole point of it: a utility has to be able to overrule
|
|
3153
3557
|
the component it sits inside, and it is not allowed to buy that with
|
|
3154
3558
|
!important or with a padded selector.
|
|
@@ -3251,6 +3655,7 @@ textarea.axi-input {
|
|
|
3251
3655
|
.axi-chip--action:is(:disabled, [aria-disabled="true"]),
|
|
3252
3656
|
.axi-drawer__close:is(:disabled, [aria-disabled="true"]),
|
|
3253
3657
|
.axi-input:is(:disabled, [aria-disabled="true"]),
|
|
3658
|
+
.axi-legend__key:where(button):is(:disabled, [aria-disabled="true"]),
|
|
3254
3659
|
.axi-link:is(:disabled, [aria-disabled="true"]),
|
|
3255
3660
|
.axi-palette__row:is(:disabled, [aria-disabled="true"]),
|
|
3256
3661
|
.axi-palette__trigger:is(:disabled, [aria-disabled="true"]),
|
|
@@ -3263,11 +3668,65 @@ textarea.axi-input {
|
|
|
3263
3668
|
.axi-select:is(:disabled, [aria-disabled="true"]),
|
|
3264
3669
|
.axi-switch:is(:disabled, [aria-disabled="true"]),
|
|
3265
3670
|
.axi-table__sort:is(:disabled, [aria-disabled="true"]),
|
|
3266
|
-
.axi-menu__pop label:is(:disabled, [aria-disabled="true"]),
|
|
3267
|
-
.axi-prose a:is(:disabled, [aria-disabled="true"]),
|
|
3268
|
-
.axi-tabs a:is(:disabled, [aria-disabled="true"]),
|
|
3269
|
-
.axi-
|
|
3270
|
-
.axi-titlebar__btns button:is(:disabled, [aria-disabled="true"]) {
|
|
3671
|
+
.axi-menu__pop :where(label):is(:disabled, [aria-disabled="true"]),
|
|
3672
|
+
.axi-prose :where(a):is(:disabled, [aria-disabled="true"]),
|
|
3673
|
+
.axi-tabs :where(a, button):is(:disabled, [aria-disabled="true"]),
|
|
3674
|
+
.axi-titlebar__btns :where(button):is(:disabled, [aria-disabled="true"]) {
|
|
3271
3675
|
opacity: .5;
|
|
3272
3676
|
cursor: not-allowed;
|
|
3273
3677
|
}
|
|
3678
|
+
|
|
3679
|
+
/* ---------- the quiet scroll ---------- */
|
|
3680
|
+
/* A scrollbar down the side of a narrow strip is a channel, not information.
|
|
3681
|
+
|
|
3682
|
+
The language had already decided this and said it in exactly one place, on
|
|
3683
|
+
.axi-palette__list, with the argument written inline: at twelve rows behind
|
|
3684
|
+
a fixed cap the bar reports a fact the row count in the bar has already
|
|
3685
|
+
given. Said only there, it was the language's only word for the decision -
|
|
3686
|
+
and the consumer that needed the same thing for its own strips could not
|
|
3687
|
+
reach it, so it wrote the rule again. Five times, in its own stylesheet, for
|
|
3688
|
+
two rails and a picker slot, twice with a `*` descendant arm because the
|
|
3689
|
+
element that actually scrolls sits one level inside a component it does not
|
|
3690
|
+
control. That is the third instance of the pattern in docs/RULES.md ("A style
|
|
3691
|
+
only reachable through a layer will be re-invented"), and the remedy is the
|
|
3692
|
+
same one: give the decision a name a consumer can spend, and put every
|
|
3693
|
+
spelling of it in ONE rule.
|
|
3694
|
+
|
|
3695
|
+
So: one rule. The class is what a consumer says about its own scroller; the
|
|
3696
|
+
four component parts beside it are the strips the language ships. Two rules
|
|
3697
|
+
that agree today are two rules that disagree after the next edit.
|
|
3698
|
+
|
|
3699
|
+
This is a refusal rather than a utility - it says what NOT to draw - which is
|
|
3700
|
+
why it is here rather than beside each component even though nothing needs
|
|
3701
|
+
the last-in-cascade position to win. One fact about parts living in three
|
|
3702
|
+
different source files has one home, the same as the dead state above.
|
|
3703
|
+
|
|
3704
|
+
Why these four and not everything that scrolls. The strip is the test: a
|
|
3705
|
+
rail is 208px wide and a palette list sits in a small floating panel, so the
|
|
3706
|
+
bar costs a measurable share of the width AND draws a second vertical line
|
|
3707
|
+
beside the object's own edge. A table's horizontal bar is the opposite case
|
|
3708
|
+
and .axi-table__scroll keeps it on purpose (see src/data.css) - there the bar
|
|
3709
|
+
is the only thing telling you a column is off-screen. Nothing here reaches a
|
|
3710
|
+
descendant: `*` would take the table's bar away the moment someone put one
|
|
3711
|
+
in a rail.
|
|
3712
|
+
|
|
3713
|
+
`.axi-rail` and `.axi-rail__nav` both, because either can be the scroller.
|
|
3714
|
+
The rail declares `overflow-y: auto` and is the scroller by default, but a
|
|
3715
|
+
rail with something pinned below its list - a "back" action at the floor -
|
|
3716
|
+
scrolls the nav instead and leaves the rail still. Both are the same strip
|
|
3717
|
+
and the decision cannot depend on which one the consumer chose. */
|
|
3718
|
+
.axi-scroll-quiet,
|
|
3719
|
+
.axi-palette__list,
|
|
3720
|
+
.axi-rail,
|
|
3721
|
+
.axi-rail__nav,
|
|
3722
|
+
.axi-split__nav {
|
|
3723
|
+
scrollbar-width: none;
|
|
3724
|
+
}
|
|
3725
|
+
.axi-scroll-quiet::-webkit-scrollbar,
|
|
3726
|
+
.axi-palette__list::-webkit-scrollbar,
|
|
3727
|
+
.axi-rail::-webkit-scrollbar,
|
|
3728
|
+
.axi-rail__nav::-webkit-scrollbar,
|
|
3729
|
+
.axi-split__nav::-webkit-scrollbar {
|
|
3730
|
+
width: 0;
|
|
3731
|
+
height: 0;
|
|
3732
|
+
}
|