@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/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
|
}
|
package/src/primitives.css
CHANGED
|
@@ -75,13 +75,91 @@ a.axi-panel--tile:hover,
|
|
|
75
75
|
button.axi-panel--tile:hover,
|
|
76
76
|
.axi-panel--tile[role='button']:hover { box-shadow: var(--axi-shadow-control-hover); }
|
|
77
77
|
|
|
78
|
+
/* The status cap rule 5 describes, as a thing you can write. A verdict about
|
|
79
|
+
the reading inside a panel, a tile or a stat is said once, across its head,
|
|
80
|
+
in the status ink - directly over the value it judges - and the rest of the
|
|
81
|
+
outline stays the ink line every other edge in the language is drawn in. Not
|
|
82
|
+
down the left: a full-height stripe is the box's frame, and five tiles in a
|
|
83
|
+
row become five coloured frames saying nothing about any one number.
|
|
84
|
+
|
|
85
|
+
It recolours the top edge the element already has rather than adding a bar,
|
|
86
|
+
so it is drawn at the element's own weight by construction - the panel step
|
|
87
|
+
on a panel, the control step on a tile or a stat - which is rule 5's own
|
|
88
|
+
argument against a heavy cap on a light frame. The state is an attribute the
|
|
89
|
+
consumer declares about its data (rule 13), read by the three objects a
|
|
90
|
+
reading lives in; a card's strip is a different claim (which category) and
|
|
91
|
+
keeps its own word.
|
|
92
|
+
|
|
93
|
+
Four values and no fifth: the accent is not a verdict, and a cap in it would
|
|
94
|
+
say "picked" about a thing nobody picked. */
|
|
95
|
+
.axi-panel[data-status="ok"], .axi-stat[data-status="ok"] { border-top-color: var(--axi-ok); }
|
|
96
|
+
.axi-panel[data-status="warn"], .axi-stat[data-status="warn"] { border-top-color: var(--axi-warn); }
|
|
97
|
+
.axi-panel[data-status="danger"], .axi-stat[data-status="danger"] { border-top-color: var(--axi-danger); }
|
|
98
|
+
.axi-panel[data-status="meta"], .axi-stat[data-status="meta"] { border-top-color: var(--axi-meta); }
|
|
99
|
+
|
|
100
|
+
/* The one of these you picked. A grid of panels, tiles or cards where one is
|
|
101
|
+
chosen - a theme picker, a fight picker, a leader you have opened - and the
|
|
102
|
+
surface has to say which.
|
|
103
|
+
|
|
104
|
+
Not the accent fill a rail item takes. A rail item is a word, and a word can
|
|
105
|
+
be printed on the accent; a panel carries content, and an accent fill drowns
|
|
106
|
+
it. The answer is the third weight .axi-rail__nav--quiet already found for
|
|
107
|
+
the same reason: the surface rises and the accent arrives on the edge. A
|
|
108
|
+
whole edge here rather than the leading one, because a grid is read in two
|
|
109
|
+
directions and has no leading side to light.
|
|
110
|
+
|
|
111
|
+
Keyed off what the element already says, the way .axi-panel--tile keys its
|
|
112
|
+
press off being an <a> or a <button> rather than off a second class. The
|
|
113
|
+
language has one spelling per selection semantics, and this rule takes all
|
|
114
|
+
of them rather than inventing a fourth: `aria-current` for a place you
|
|
115
|
+
navigated to (a rail item, a tab, a table row), `aria-pressed` for a toggle
|
|
116
|
+
(a pill), `aria-selected` for an option in a listbox (a picker option). A
|
|
117
|
+
consumer that marks a card by appending a border utility has told assistive
|
|
118
|
+
tech nothing at all, and that is the defect this replaces rather than the
|
|
119
|
+
missing fill - ten sites in one consumer, six different bases, three
|
|
120
|
+
invented extra marks, and not one of them announced.
|
|
121
|
+
|
|
122
|
+
The fourth arm is a <label> wrapping its own radio, which is the correct
|
|
123
|
+
markup for a picker made of panels: the input carries the state, so the
|
|
124
|
+
label has no attribute to set and copying the state onto one would be a
|
|
125
|
+
second source of truth that can disagree. This is the language's only
|
|
126
|
+
:has(), and the child combinator is the whole point - `> input:checked`
|
|
127
|
+
means this panel IS the control, where a descendant match would light a
|
|
128
|
+
panel up for any checkbox buried in its content.
|
|
129
|
+
|
|
130
|
+
Both spellings on one rule, per "a style only reachable through a layer will
|
|
131
|
+
be re-invented": .axi-card is a shell and .axi-panel a primitive, and a
|
|
132
|
+
consumer marking one of them must not have to find out which file its
|
|
133
|
+
selected state was written in. The fill is the `background` shorthand and
|
|
134
|
+
not the -paint companion, because --axi-surface-raised is a surface token
|
|
135
|
+
and a theme is allowed to make a surface a gradient. */
|
|
136
|
+
:is(.axi-card, .axi-panel):is(
|
|
137
|
+
[aria-current],
|
|
138
|
+
[aria-pressed='true'],
|
|
139
|
+
[aria-selected='true'],
|
|
140
|
+
:has(> input:checked)
|
|
141
|
+
) {
|
|
142
|
+
background: var(--axi-surface-raised);
|
|
143
|
+
border-color: var(--axi-accent);
|
|
144
|
+
}
|
|
145
|
+
|
|
78
146
|
/* ---------- well ---------- */
|
|
79
147
|
/* The panel's inverse: a field sunk into the surface around it rather than
|
|
80
148
|
raised off it. No block, because a recess casts nothing, and the rule rather
|
|
81
149
|
than the ink for its edge - see --axi-well-fill in src/tokens.css for why
|
|
82
150
|
both of those follow from the same fact. Reach for this wherever a second
|
|
83
151
|
panel inside a panel was the instinct: a picker list beside a table, a column
|
|
84
|
-
of cards standing on the page, a trough a bar fills.
|
|
152
|
+
of cards standing on the page, a trough a bar fills.
|
|
153
|
+
|
|
154
|
+
.axi-split__nav is the first of those three named as a part rather than left
|
|
155
|
+
to the consumer to remember, and it is in THIS rule rather than restating the
|
|
156
|
+
fill next to the split pane. Two rules that agree today are two rules that
|
|
157
|
+
disagree after the next edit - see "A style only reachable through a layer
|
|
158
|
+
will be re-invented" in docs/RULES.md, which is the same argument arriving
|
|
159
|
+
from the other direction. The split pane's slot cannot be a well by
|
|
160
|
+
convention: a consumer who forgets the second class gets a picker with no
|
|
161
|
+
recess, and the language has no way to notice. */
|
|
162
|
+
.axi-split__nav,
|
|
85
163
|
.axi-well {
|
|
86
164
|
background: var(--axi-well-fill);
|
|
87
165
|
border: var(--axi-border-control) solid var(--axi-well-line);
|
|
@@ -180,6 +258,43 @@ button.axi-panel--tile:hover,
|
|
|
180
258
|
icon-only button has no label to pad around, so the 20px sides meant for
|
|
181
259
|
one produced a wide rectangle around a single glyph - visible in this
|
|
182
260
|
language's own gallery, in the example titled "Alone in a button". */
|
|
261
|
+
/* Icon over label rather than beside it. Not a style choice - it is the only
|
|
262
|
+
thing that makes a bar of equal actions fit a phone. Measured on the
|
|
263
|
+
consumer that needed it: four icon+label actions side by side want 387px of
|
|
264
|
+
the 337px available at 393px wide, and every label is one unbreakable word,
|
|
265
|
+
so min-content equals max-content and flex-shrink has nothing to give. The
|
|
266
|
+
last action simply runs off the screen. Stacked, the same four come to
|
|
267
|
+
~291px.
|
|
268
|
+
|
|
269
|
+
min-width: 0 because that failure has a second half. A flex item's default
|
|
270
|
+
min-width is its content, so a button in a row refuses to shrink below its
|
|
271
|
+
own label even when the row is told to divide the space - which is the same
|
|
272
|
+
trap .axi-tabs__tab documents for its label and .axi-split__body for its
|
|
273
|
+
track.
|
|
274
|
+
|
|
275
|
+
The label is addressed as an element because a stacked button has two
|
|
276
|
+
children and only one of them is words; naming a part would make every
|
|
277
|
+
consumer add a class to say what the span already says. :where() so it
|
|
278
|
+
spends no specificity - the ink layer and any modifier still out-rank it,
|
|
279
|
+
which is the contract src/utilities.css is owed. */
|
|
280
|
+
.axi-btn--stack {
|
|
281
|
+
flex-direction: column;
|
|
282
|
+
gap: 5px;
|
|
283
|
+
min-width: 0;
|
|
284
|
+
}
|
|
285
|
+
.axi-btn--stack > :where(span) {
|
|
286
|
+
/* Both, and the pair is the point: max-width caps it against the button,
|
|
287
|
+
min-width: 0 is what lets it shrink at all. A flex item's default
|
|
288
|
+
min-width is its content, so max-width alone leaves the ellipsis
|
|
289
|
+
unreachable - the label overflows the cap instead of truncating inside
|
|
290
|
+
it. tests/tokens.test.mjs pins the pair for every truncating rule in the
|
|
291
|
+
language, and it caught this one. */
|
|
292
|
+
max-width: 100%;
|
|
293
|
+
min-width: 0;
|
|
294
|
+
overflow: hidden;
|
|
295
|
+
text-overflow: ellipsis;
|
|
296
|
+
white-space: nowrap;
|
|
297
|
+
}
|
|
183
298
|
.axi-btn--sm { --axi-btn-pad: 8px 13px; --axi-btn-size: 12px; }
|
|
184
299
|
.axi-btn--xs { --axi-btn-pad: 5px 9px; --axi-btn-size: 11px; }
|
|
185
300
|
.axi-btn--icon { --axi-btn-pad: 7px; }
|
|
@@ -478,7 +593,7 @@ button.axi-panel--tile:hover,
|
|
|
478
593
|
says `axi-link axi-ink-dim` must stay dim under the cursor, and the plain
|
|
479
594
|
`:hover` form is what made the danger button read white under one. */
|
|
480
595
|
.axi-link,
|
|
481
|
-
.axi-prose a {
|
|
596
|
+
.axi-prose :where(a) {
|
|
482
597
|
padding: 0;
|
|
483
598
|
background: none;
|
|
484
599
|
border: 0;
|
|
@@ -491,7 +606,7 @@ button.axi-panel--tile:hover,
|
|
|
491
606
|
cursor: pointer;
|
|
492
607
|
}
|
|
493
608
|
.axi-link:where(:hover),
|
|
494
|
-
.axi-prose
|
|
609
|
+
.axi-prose :where(a:hover) { color: var(--axi-text); }
|
|
495
610
|
|
|
496
611
|
/* ---------- quiet action ---------- */
|
|
497
612
|
/* A control that is only its label. Not a button - it draws no box, takes no
|
|
@@ -617,8 +732,8 @@ button.axi-panel--tile:hover,
|
|
|
617
732
|
disclosure that drops back flat the moment the pointer moves into the list
|
|
618
733
|
it opened severs the two: the lift is what says this box and that popover
|
|
619
734
|
are one control. */
|
|
620
|
-
.axi-select:hover,
|
|
621
|
-
.axi-picker__btn:hover,
|
|
735
|
+
.axi-select:where(:hover),
|
|
736
|
+
.axi-picker__btn:where(:hover),
|
|
622
737
|
.axi-picker__btn[aria-expanded='true'] {
|
|
623
738
|
color: var(--axi-text);
|
|
624
739
|
box-shadow: var(--axi-shadow-control);
|
|
@@ -645,7 +760,7 @@ button.axi-panel--tile:hover,
|
|
|
645
760
|
backdrop-filter: var(--axi-surface-filter);
|
|
646
761
|
box-shadow: var(--axi-shadow-panel);
|
|
647
762
|
}
|
|
648
|
-
.axi-select option {
|
|
763
|
+
.axi-select :where(option) {
|
|
649
764
|
display: flex; align-items: center; gap: 9px;
|
|
650
765
|
padding: 8px 9px;
|
|
651
766
|
border-radius: var(--axi-radius-sm);
|
|
@@ -660,7 +775,7 @@ button.axi-panel--tile:hover,
|
|
|
660
775
|
level's fill - and this popover is filled with --axi-surface-raised, which
|
|
661
776
|
a theme may take translucent, so an opaque row here is the black patch
|
|
662
777
|
--axi-well-fill exists to prevent. Same for the picker's rows below. */
|
|
663
|
-
.axi-select option:hover,
|
|
778
|
+
.axi-select :where(option:hover, option:focus) {
|
|
664
779
|
background: var(--axi-well-fill); color: var(--axi-text);
|
|
665
780
|
}
|
|
666
781
|
/* The page-wide focus ring sits 2px outside its element; inside a picker
|
|
@@ -759,7 +874,7 @@ button.axi-panel--tile:hover,
|
|
|
759
874
|
color: transparent;
|
|
760
875
|
font-weight: 900;
|
|
761
876
|
}
|
|
762
|
-
.axi-picker__opt:hover,
|
|
877
|
+
.axi-picker__opt:where(:hover, :focus) {
|
|
763
878
|
background: var(--axi-well-fill); color: var(--axi-text);
|
|
764
879
|
}
|
|
765
880
|
/* The page-wide focus ring sits 2px outside its element; inside a popover
|
package/src/shells.css
CHANGED
|
@@ -3,14 +3,23 @@
|
|
|
3
3
|
redefining them, so a change to .axi-panel moves the notice, the toolbar,
|
|
4
4
|
the popover and the card together. */
|
|
5
5
|
|
|
6
|
-
/* The small uppercase heading that labels a panel's contents.
|
|
6
|
+
/* The small uppercase heading that labels a panel's contents.
|
|
7
|
+
|
|
8
|
+
The gap under it is a knob because the eyebrow heads more than one size of
|
|
9
|
+
thing. 14px is the figure for a panel at its resting padding; a tile is one
|
|
10
|
+
form step down and its padding is one density step down with it, so the
|
|
11
|
+
eyebrow inside one reads the tighter figure in the rule below, the same way
|
|
12
|
+
.axi-panel--tile reads --axi-panel-pad at a tighter fallback of its own. An
|
|
13
|
+
eyebrow sitting in a row - a sheet's head, a toolbar - wants no gap at all,
|
|
14
|
+
and says so with the knob rather than with an inline margin. */
|
|
7
15
|
.axi-eyebrow {
|
|
8
|
-
margin: 0 0 14px;
|
|
16
|
+
margin: 0 0 var(--axi-eyebrow-gap, 14px);
|
|
9
17
|
font: var(--axi-t-eyebrow);
|
|
10
18
|
letter-spacing: var(--axi-ls-eyebrow);
|
|
11
19
|
text-transform: uppercase;
|
|
12
20
|
color: var(--axi-text-faint);
|
|
13
21
|
}
|
|
22
|
+
.axi-panel--tile .axi-eyebrow { margin-bottom: var(--axi-eyebrow-gap, 6px); }
|
|
14
23
|
|
|
15
24
|
/* ---------- masthead ---------- */
|
|
16
25
|
/* Opaque, because content scrolls under it. Which is also why it paints the
|
|
@@ -95,9 +104,35 @@
|
|
|
95
104
|
it shrinks an item - so one tab too many sent the last item in source order
|
|
96
105
|
to a second row instead of taking width off the widest one. The strip is the
|
|
97
106
|
item that can give: it scrolls, and the row stays a row. */
|
|
98
|
-
.axi-mast .axi-tabs { margin-left: auto;
|
|
99
|
-
|
|
100
|
-
|
|
107
|
+
.axi-mast .axi-tabs { margin-left: auto; }
|
|
108
|
+
/* A strip with more tabs than room scrolls rather than losing them.
|
|
109
|
+
|
|
110
|
+
.axi-tabs is `display: flex` with no wrap, so a strip too wide for its
|
|
111
|
+
container overflows - and outside a scroll container the tabs past the edge
|
|
112
|
+
are simply unreachable. The language already believed this twice and could
|
|
113
|
+
say it in neither place a consumer could use: once here, behind .axi-mast,
|
|
114
|
+
and once at the 640px breakpoint below, where EVERY strip scrolls. A
|
|
115
|
+
standalone strip above that breakpoint with one tab too many had nowhere to
|
|
116
|
+
look, and the consumer that hit it wrote `overflow-x: auto` on its own
|
|
117
|
+
wrapper - the same re-invention docs/RULES.md describes, arriving a fourth
|
|
118
|
+
time.
|
|
119
|
+
|
|
120
|
+
A modifier rather than the base rule, and this is the part worth arguing.
|
|
121
|
+
`overflow-x: auto` computes `overflow-y` from `visible` to `auto`, so a
|
|
122
|
+
strip that scrolls is also a strip that clips vertically - a focus ring, a
|
|
123
|
+
badge hanging off a tab, a popover anchored to one. Every strip paying that
|
|
124
|
+
to fix the strips that are too long is the wrong trade; the strips that are
|
|
125
|
+
too long say so.
|
|
126
|
+
|
|
127
|
+
The bar is NOT hidden here, and the quiet-scroll rule in src/utilities.css
|
|
128
|
+
deliberately does not name this. That rule is about a bar drawn down the
|
|
129
|
+
side of a narrow strip, where the object's own edge is already a vertical
|
|
130
|
+
line and a count elsewhere already says how many rows there are. A tab strip
|
|
131
|
+
that scrolls horizontally has no second signal: the bar is the only thing
|
|
132
|
+
saying there are more tabs. Same reason .axi-table__scroll keeps its. */
|
|
133
|
+
.axi-tabs--scroll,
|
|
134
|
+
.axi-mast .axi-tabs { flex-shrink: 1; min-width: 0; overflow-x: auto; }
|
|
135
|
+
.axi-tabs :where(a, button) {
|
|
101
136
|
padding: 8px 16px;
|
|
102
137
|
font: 800 13.5px/1 var(--axi-sans);
|
|
103
138
|
color: var(--axi-text-dim); text-decoration: none;
|
|
@@ -106,8 +141,7 @@
|
|
|
106
141
|
border: var(--axi-border-control) solid transparent;
|
|
107
142
|
border-radius: var(--axi-radius-sm);
|
|
108
143
|
}
|
|
109
|
-
.axi-tabs a:hover,
|
|
110
|
-
.axi-tabs button:hover { color: var(--axi-text); border-color: var(--axi-rule); }
|
|
144
|
+
.axi-tabs :where(a:hover, button:hover) { color: var(--axi-text); border-color: var(--axi-rule); }
|
|
111
145
|
/* The current tab is filled and blocked - the same treatment a pressed pill
|
|
112
146
|
gets, because it is the same idea: this one is on. */
|
|
113
147
|
.axi-tabs a[aria-current="page"],
|
|
@@ -139,13 +173,13 @@
|
|
|
139
173
|
text-overflow: ellipsis;
|
|
140
174
|
white-space: nowrap;
|
|
141
175
|
}
|
|
142
|
-
.axi-tabs .axi-tabs__close {
|
|
176
|
+
.axi-tabs :where(.axi-tabs__close) {
|
|
143
177
|
padding: 0 6px;
|
|
144
178
|
font: var(--axi-t-micro);
|
|
145
179
|
color: var(--axi-text-faint);
|
|
146
180
|
border-color: transparent;
|
|
147
181
|
}
|
|
148
|
-
.axi-tabs .axi-tabs__close:hover { color: var(--axi-text); border-color: transparent; }
|
|
182
|
+
.axi-tabs :where(.axi-tabs__close:hover) { color: var(--axi-text); border-color: transparent; }
|
|
149
183
|
|
|
150
184
|
/* ---------- rail ---------- */
|
|
151
185
|
/* Vertical navigation, for a set too large for a tab strip to hold. Same
|
|
@@ -219,7 +253,7 @@
|
|
|
219
253
|
border-radius: var(--axi-radius-sm);
|
|
220
254
|
cursor: pointer;
|
|
221
255
|
}
|
|
222
|
-
.axi-rail__item:hover { color: var(--axi-text); background: var(--axi-surface-raised); }
|
|
256
|
+
.axi-rail__item:where(:hover) { color: var(--axi-text); background: var(--axi-surface-raised); }
|
|
223
257
|
/* The same treatment a pressed pill and a current tab get. Only ever one of
|
|
224
258
|
these on screen. Any aria-current value, because the right one depends on
|
|
225
259
|
what the rail navigates - `page` for a view, `location` for a place within
|
|
@@ -282,7 +316,7 @@
|
|
|
282
316
|
border-radius: var(--axi-radius-sm);
|
|
283
317
|
cursor: pointer;
|
|
284
318
|
}
|
|
285
|
-
.axi-rail__subitem:hover { color: var(--axi-text-dim); background: var(--axi-surface-raised); }
|
|
319
|
+
.axi-rail__subitem:where(:hover) { color: var(--axi-text-dim); background: var(--axi-surface-raised); }
|
|
286
320
|
.axi-rail__subitem[aria-current] { color: var(--axi-text); }
|
|
287
321
|
/* An annotation on an item, not a measurement of it, so rule 5 draws it
|
|
288
322
|
outlined rather than filled. */
|
|
@@ -430,13 +464,13 @@
|
|
|
430
464
|
box-shadow: var(--axi-shadow-panel);
|
|
431
465
|
}
|
|
432
466
|
.axi-menu__pop[hidden] { display: none; }
|
|
433
|
-
.axi-menu__pop label {
|
|
467
|
+
.axi-menu__pop :where(label) {
|
|
434
468
|
display: flex; gap: 9px; align-items: flex-start;
|
|
435
469
|
padding: 7px 8px; border-radius: var(--axi-radius-sm);
|
|
436
470
|
font-size: 13px; font-weight: 600; color: var(--axi-text-dim); cursor: pointer;
|
|
437
471
|
}
|
|
438
472
|
/* A hovered row sinks - the same fill the select's and the picker's rows use. */
|
|
439
|
-
.axi-menu__pop label:hover { background: var(--axi-well-fill); color: var(--axi-text); }
|
|
473
|
+
.axi-menu__pop :where(label:hover) { background: var(--axi-well-fill); color: var(--axi-text); }
|
|
440
474
|
.axi-menu__pop input { margin: 3px 0 0; }
|
|
441
475
|
|
|
442
476
|
/* ---------- command palette ---------- */
|
|
@@ -497,14 +531,16 @@
|
|
|
497
531
|
}
|
|
498
532
|
/* min-height: 0 because the panel is a flex column and a list that cannot
|
|
499
533
|
shrink below its content pushes the panel past its own max-height. The
|
|
500
|
-
scrollbar is hidden
|
|
501
|
-
|
|
502
|
-
|
|
534
|
+
scrollbar is hidden, but not here: that decision is the language's and not
|
|
535
|
+
this component's, and it now lives once, in src/utilities.css under "the
|
|
536
|
+
quiet scroll", with this selector in the same rule as every other strip that
|
|
537
|
+
makes it. The argument that produced it was written here first - at twelve
|
|
538
|
+
rows behind a fixed cap the bar is a channel carved down the side of a small
|
|
539
|
+
panel to report a fact the row count in the bar has already given - and it is
|
|
540
|
+
preserved in full at the rule. */
|
|
503
541
|
.axi-palette__list {
|
|
504
542
|
flex: 1; min-height: 0; overflow-y: auto; padding: 4px 0;
|
|
505
|
-
scrollbar-width: none;
|
|
506
543
|
}
|
|
507
|
-
.axi-palette__list::-webkit-scrollbar { width: 0; height: 0; }
|
|
508
544
|
/* One .axi-palette__section per group of rows, each opening with a
|
|
509
545
|
.axi-palette__group heading. The section is a named part rather than left to
|
|
510
546
|
the consumer's wrapper so the rule between groups can be stated as what it
|
|
@@ -526,7 +562,7 @@
|
|
|
526
562
|
color: var(--axi-text-dim);
|
|
527
563
|
font: var(--axi-t-small); text-align: left; cursor: pointer;
|
|
528
564
|
}
|
|
529
|
-
.axi-palette__row:hover { background: var(--axi-surface-raised); color: var(--axi-text); }
|
|
565
|
+
.axi-palette__row:where(:hover) { background: var(--axi-surface-raised); color: var(--axi-text); }
|
|
530
566
|
.axi-palette__row[data-active] { background: var(--axi-accent); color: var(--axi-accent-ink); }
|
|
531
567
|
/* The row's own ink is set above, but a row carries a glyph and a second line
|
|
532
568
|
of text that were coloured on their own; on the filled row they all become
|
|
@@ -558,7 +594,7 @@
|
|
|
558
594
|
cursor: pointer;
|
|
559
595
|
transition: transform .1s, box-shadow .1s, color .1s;
|
|
560
596
|
}
|
|
561
|
-
.axi-palette__trigger:hover {
|
|
597
|
+
.axi-palette__trigger:where(:hover) {
|
|
562
598
|
color: var(--axi-text);
|
|
563
599
|
box-shadow: var(--axi-shadow-control);
|
|
564
600
|
transform: translate(-2px, -2px);
|
|
@@ -705,7 +741,7 @@
|
|
|
705
741
|
color: var(--axi-text-faint);
|
|
706
742
|
}
|
|
707
743
|
.axi-card__go { margin-left: auto; color: var(--axi-accent); font-weight: 900; text-transform: uppercase; letter-spacing: .05em; }
|
|
708
|
-
.axi-card:hover .axi-card__go { color: var(--axi-text); }
|
|
744
|
+
.axi-card:where(:hover) :where(.axi-card__go) { color: var(--axi-text); }
|
|
709
745
|
|
|
710
746
|
/* ---------- drawer ---------- */
|
|
711
747
|
/* A scrim's layer is not a property of the scrim. It is "directly below the
|
|
@@ -778,7 +814,7 @@
|
|
|
778
814
|
font-size: 15px; font-weight: 900; cursor: pointer;
|
|
779
815
|
transition: transform .1s, box-shadow .1s;
|
|
780
816
|
}
|
|
781
|
-
.axi-drawer__close:hover {
|
|
817
|
+
.axi-drawer__close:where(:hover) {
|
|
782
818
|
background: var(--axi-accent); color: var(--axi-accent-ink);
|
|
783
819
|
box-shadow: var(--axi-shadow-control);
|
|
784
820
|
transform: translate(-2px, -2px);
|
|
@@ -920,7 +956,7 @@
|
|
|
920
956
|
-webkit-app-region: drag;
|
|
921
957
|
}
|
|
922
958
|
.axi-titlebar__btns { margin-left: auto; display: flex; height: 100%; -webkit-app-region: no-drag; }
|
|
923
|
-
.axi-titlebar__btns button {
|
|
959
|
+
.axi-titlebar__btns :where(button) {
|
|
924
960
|
width: 44px;
|
|
925
961
|
border: 0;
|
|
926
962
|
background: transparent;
|
|
@@ -931,10 +967,10 @@
|
|
|
931
967
|
/* No lift here. A window button is chrome, not content: it has nothing to
|
|
932
968
|
lift off, and 44px of movement in the corner of every screen is exactly
|
|
933
969
|
the scattered motion rule 4 rations. It answers on the neutral ramp. */
|
|
934
|
-
.axi-titlebar__btns button:hover { background: var(--axi-surface); color: var(--axi-text); }
|
|
970
|
+
.axi-titlebar__btns :where(button:hover) { background: var(--axi-surface); color: var(--axi-text); }
|
|
935
971
|
/* Close is the one destructive control in the chrome, and the one place a
|
|
936
972
|
status ink belongs in a titlebar. */
|
|
937
|
-
.axi-titlebar__btns button:last-child:hover { background: var(--axi-danger); color: var(--axi-ink-on-fill); }
|
|
973
|
+
.axi-titlebar__btns :where(button:last-child:hover) { background: var(--axi-danger); color: var(--axi-ink-on-fill); }
|
|
938
974
|
/* The scrolling region under the chrome. */
|
|
939
975
|
.axi-window__body { flex: 1; overflow-y: auto; }
|
|
940
976
|
|
|
@@ -953,8 +989,8 @@
|
|
|
953
989
|
flex-wrap: wrap;
|
|
954
990
|
font: var(--axi-t-small);
|
|
955
991
|
}
|
|
956
|
-
.axi-crumbs a { color: var(--axi-text-dim); text-decoration: none; }
|
|
957
|
-
.axi-crumbs a:hover { color: var(--axi-accent); }
|
|
992
|
+
.axi-crumbs :where(a) { color: var(--axi-text-dim); text-decoration: none; }
|
|
993
|
+
.axi-crumbs :where(a:hover) { color: var(--axi-accent); }
|
|
958
994
|
/* 7px, matching the prose bullet (src/prose.css) exactly, with the same
|
|
959
995
|
--axi-border-hairline outline: rule 7 calls the motif "a 45deg-rotated
|
|
960
996
|
outlined square", and a bare fill with no border does not match it. The
|
|
@@ -988,7 +1024,7 @@
|
|
|
988
1024
|
border: var(--axi-border-control) solid var(--axi-ink-line);
|
|
989
1025
|
border-radius: var(--axi-radius-sm);
|
|
990
1026
|
}
|
|
991
|
-
.axi-pages__n:hover { color: var(--axi-text); }
|
|
1027
|
+
.axi-pages__n:where(:hover) { color: var(--axi-text); }
|
|
992
1028
|
.axi-pages__n[aria-current="page"] {
|
|
993
1029
|
background: var(--axi-accent);
|
|
994
1030
|
color: var(--axi-accent-ink);
|
|
@@ -1001,7 +1037,12 @@
|
|
|
1001
1037
|
@media (max-width: 640px) {
|
|
1002
1038
|
.axi-mast__in { min-height: 0; padding: 10px 16px; gap: 12px; }
|
|
1003
1039
|
.axi-mast .axi-tabs { margin-left: 0; }
|
|
1004
|
-
|
|
1040
|
+
/* Every strip scrolls on a phone, whether or not it asked. This cannot join
|
|
1041
|
+
the shared rule above - a media-query rule and an unconditional one are
|
|
1042
|
+
different rules by construction - so the values are pinned against it by
|
|
1043
|
+
test instead: the arm may declare nothing the modifier does not, and
|
|
1044
|
+
nothing at a different value. */
|
|
1045
|
+
.axi-tabs { width: 100%; flex-shrink: 1; min-width: 0; overflow-x: auto; }
|
|
1005
1046
|
/* The offset blocks eat horizontal room a phone does not have, so the panel
|
|
1006
1047
|
step drops to the control step here rather than to an invented number.
|
|
1007
1048
|
The card's hover has to come down with it - left alone it kept deepening
|
package/src/tokens.css
CHANGED
|
@@ -118,6 +118,20 @@
|
|
|
118
118
|
--axi-ink-on-fill below, on the other side of the same token. */
|
|
119
119
|
--axi-ground-deep: var(--axi-ink-line);
|
|
120
120
|
--axi-rule: #3a4250; /* internal rules, inside an outlined panel */
|
|
121
|
+
/* A gridline is quieter than a rule. A rule divides two things; a grid is
|
|
122
|
+
scaffolding a value is read against, and it has to sit barely above the
|
|
123
|
+
ground it is drawn on or the chart becomes a grid of boxes with numbers
|
|
124
|
+
in them - the failure rule 3 names for tables, arriving in a plot.
|
|
125
|
+
|
|
126
|
+
Derived, not picked, and that is the point of its being a token. The one
|
|
127
|
+
consumer that had drawn a chart grid wrote #262c36 for it: the default
|
|
128
|
+
rule laid over the default ground at 47%, to within one step per channel,
|
|
129
|
+
and a fixed slate that stayed slate under glass and under flat while both
|
|
130
|
+
themes relit their rules. Mixing toward transparent rather than toward
|
|
131
|
+
the ground keeps it right on a panel fill too, and keeps it following
|
|
132
|
+
--axi-rule when a theme restates that - so a theme has nothing to say
|
|
133
|
+
here, and says nothing. */
|
|
134
|
+
--axi-grid: color-mix(in srgb, var(--axi-rule) 47%, transparent);
|
|
121
135
|
/* --- the well --- */
|
|
122
136
|
/* A field sunk INTO a surface rather than raised off one: a table picker's
|
|
123
137
|
list, a bar's trough, a column of cards standing on the page. The language
|
package/src/utilities.css
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* axi design language - utilities.
|
|
2
|
-
Two families of single-purpose class and one
|
|
3
|
-
the cascade
|
|
2
|
+
Two families of single-purpose class, one state and one refusal, and the file
|
|
3
|
+
is last in the cascade
|
|
4
4
|
because that is the whole point of it: a utility has to be able to overrule
|
|
5
5
|
the component it sits inside, and it is not allowed to buy that with
|
|
6
6
|
!important or with a padded selector.
|
|
@@ -103,6 +103,7 @@
|
|
|
103
103
|
.axi-chip--action:is(:disabled, [aria-disabled="true"]),
|
|
104
104
|
.axi-drawer__close:is(:disabled, [aria-disabled="true"]),
|
|
105
105
|
.axi-input:is(:disabled, [aria-disabled="true"]),
|
|
106
|
+
.axi-legend__key:where(button):is(:disabled, [aria-disabled="true"]),
|
|
106
107
|
.axi-link:is(:disabled, [aria-disabled="true"]),
|
|
107
108
|
.axi-palette__row:is(:disabled, [aria-disabled="true"]),
|
|
108
109
|
.axi-palette__trigger:is(:disabled, [aria-disabled="true"]),
|
|
@@ -115,11 +116,65 @@
|
|
|
115
116
|
.axi-select:is(:disabled, [aria-disabled="true"]),
|
|
116
117
|
.axi-switch:is(:disabled, [aria-disabled="true"]),
|
|
117
118
|
.axi-table__sort:is(:disabled, [aria-disabled="true"]),
|
|
118
|
-
.axi-menu__pop label:is(:disabled, [aria-disabled="true"]),
|
|
119
|
-
.axi-prose a:is(:disabled, [aria-disabled="true"]),
|
|
120
|
-
.axi-tabs a:is(:disabled, [aria-disabled="true"]),
|
|
121
|
-
.axi-
|
|
122
|
-
.axi-titlebar__btns button:is(:disabled, [aria-disabled="true"]) {
|
|
119
|
+
.axi-menu__pop :where(label):is(:disabled, [aria-disabled="true"]),
|
|
120
|
+
.axi-prose :where(a):is(:disabled, [aria-disabled="true"]),
|
|
121
|
+
.axi-tabs :where(a, button):is(:disabled, [aria-disabled="true"]),
|
|
122
|
+
.axi-titlebar__btns :where(button):is(:disabled, [aria-disabled="true"]) {
|
|
123
123
|
opacity: .5;
|
|
124
124
|
cursor: not-allowed;
|
|
125
125
|
}
|
|
126
|
+
|
|
127
|
+
/* ---------- the quiet scroll ---------- */
|
|
128
|
+
/* A scrollbar down the side of a narrow strip is a channel, not information.
|
|
129
|
+
|
|
130
|
+
The language had already decided this and said it in exactly one place, on
|
|
131
|
+
.axi-palette__list, with the argument written inline: at twelve rows behind
|
|
132
|
+
a fixed cap the bar reports a fact the row count in the bar has already
|
|
133
|
+
given. Said only there, it was the language's only word for the decision -
|
|
134
|
+
and the consumer that needed the same thing for its own strips could not
|
|
135
|
+
reach it, so it wrote the rule again. Five times, in its own stylesheet, for
|
|
136
|
+
two rails and a picker slot, twice with a `*` descendant arm because the
|
|
137
|
+
element that actually scrolls sits one level inside a component it does not
|
|
138
|
+
control. That is the third instance of the pattern in docs/RULES.md ("A style
|
|
139
|
+
only reachable through a layer will be re-invented"), and the remedy is the
|
|
140
|
+
same one: give the decision a name a consumer can spend, and put every
|
|
141
|
+
spelling of it in ONE rule.
|
|
142
|
+
|
|
143
|
+
So: one rule. The class is what a consumer says about its own scroller; the
|
|
144
|
+
four component parts beside it are the strips the language ships. Two rules
|
|
145
|
+
that agree today are two rules that disagree after the next edit.
|
|
146
|
+
|
|
147
|
+
This is a refusal rather than a utility - it says what NOT to draw - which is
|
|
148
|
+
why it is here rather than beside each component even though nothing needs
|
|
149
|
+
the last-in-cascade position to win. One fact about parts living in three
|
|
150
|
+
different source files has one home, the same as the dead state above.
|
|
151
|
+
|
|
152
|
+
Why these four and not everything that scrolls. The strip is the test: a
|
|
153
|
+
rail is 208px wide and a palette list sits in a small floating panel, so the
|
|
154
|
+
bar costs a measurable share of the width AND draws a second vertical line
|
|
155
|
+
beside the object's own edge. A table's horizontal bar is the opposite case
|
|
156
|
+
and .axi-table__scroll keeps it on purpose (see src/data.css) - there the bar
|
|
157
|
+
is the only thing telling you a column is off-screen. Nothing here reaches a
|
|
158
|
+
descendant: `*` would take the table's bar away the moment someone put one
|
|
159
|
+
in a rail.
|
|
160
|
+
|
|
161
|
+
`.axi-rail` and `.axi-rail__nav` both, because either can be the scroller.
|
|
162
|
+
The rail declares `overflow-y: auto` and is the scroller by default, but a
|
|
163
|
+
rail with something pinned below its list - a "back" action at the floor -
|
|
164
|
+
scrolls the nav instead and leaves the rail still. Both are the same strip
|
|
165
|
+
and the decision cannot depend on which one the consumer chose. */
|
|
166
|
+
.axi-scroll-quiet,
|
|
167
|
+
.axi-palette__list,
|
|
168
|
+
.axi-rail,
|
|
169
|
+
.axi-rail__nav,
|
|
170
|
+
.axi-split__nav {
|
|
171
|
+
scrollbar-width: none;
|
|
172
|
+
}
|
|
173
|
+
.axi-scroll-quiet::-webkit-scrollbar,
|
|
174
|
+
.axi-palette__list::-webkit-scrollbar,
|
|
175
|
+
.axi-rail::-webkit-scrollbar,
|
|
176
|
+
.axi-rail__nav::-webkit-scrollbar,
|
|
177
|
+
.axi-split__nav::-webkit-scrollbar {
|
|
178
|
+
width: 0;
|
|
179
|
+
height: 0;
|
|
180
|
+
}
|