@axiapps/axi-design 1.42.0 → 1.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/accents.json +2 -1
- package/dist/accents.css +1 -0
- package/dist/axi.css +542 -46
- package/dist/themes/flat.css +1 -0
- package/docs/RULES.md +214 -5
- package/package.json +1 -1
- package/src/data.css +204 -2
- package/src/layout.css +68 -0
- package/src/primitives.css +101 -8
- package/src/shells.css +107 -29
- package/src/utilities.css +62 -7
package/src/primitives.css
CHANGED
|
@@ -75,13 +75,69 @@ 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 one of these you picked. A grid of panels, tiles or cards where one is
|
|
79
|
+
chosen - a theme picker, a fight picker, a leader you have opened - and the
|
|
80
|
+
surface has to say which.
|
|
81
|
+
|
|
82
|
+
Not the accent fill a rail item takes. A rail item is a word, and a word can
|
|
83
|
+
be printed on the accent; a panel carries content, and an accent fill drowns
|
|
84
|
+
it. The answer is the third weight .axi-rail__nav--quiet already found for
|
|
85
|
+
the same reason: the surface rises and the accent arrives on the edge. A
|
|
86
|
+
whole edge here rather than the leading one, because a grid is read in two
|
|
87
|
+
directions and has no leading side to light.
|
|
88
|
+
|
|
89
|
+
Keyed off what the element already says, the way .axi-panel--tile keys its
|
|
90
|
+
press off being an <a> or a <button> rather than off a second class. The
|
|
91
|
+
language has one spelling per selection semantics, and this rule takes all
|
|
92
|
+
of them rather than inventing a fourth: `aria-current` for a place you
|
|
93
|
+
navigated to (a rail item, a tab, a table row), `aria-pressed` for a toggle
|
|
94
|
+
(a pill), `aria-selected` for an option in a listbox (a picker option). A
|
|
95
|
+
consumer that marks a card by appending a border utility has told assistive
|
|
96
|
+
tech nothing at all, and that is the defect this replaces rather than the
|
|
97
|
+
missing fill - ten sites in one consumer, six different bases, three
|
|
98
|
+
invented extra marks, and not one of them announced.
|
|
99
|
+
|
|
100
|
+
The fourth arm is a <label> wrapping its own radio, which is the correct
|
|
101
|
+
markup for a picker made of panels: the input carries the state, so the
|
|
102
|
+
label has no attribute to set and copying the state onto one would be a
|
|
103
|
+
second source of truth that can disagree. This is the language's only
|
|
104
|
+
:has(), and the child combinator is the whole point - `> input:checked`
|
|
105
|
+
means this panel IS the control, where a descendant match would light a
|
|
106
|
+
panel up for any checkbox buried in its content.
|
|
107
|
+
|
|
108
|
+
Both spellings on one rule, per "a style only reachable through a layer will
|
|
109
|
+
be re-invented": .axi-card is a shell and .axi-panel a primitive, and a
|
|
110
|
+
consumer marking one of them must not have to find out which file its
|
|
111
|
+
selected state was written in. The fill is the `background` shorthand and
|
|
112
|
+
not the -paint companion, because --axi-surface-raised is a surface token
|
|
113
|
+
and a theme is allowed to make a surface a gradient. */
|
|
114
|
+
:is(.axi-card, .axi-panel):is(
|
|
115
|
+
[aria-current],
|
|
116
|
+
[aria-pressed='true'],
|
|
117
|
+
[aria-selected='true'],
|
|
118
|
+
:has(> input:checked)
|
|
119
|
+
) {
|
|
120
|
+
background: var(--axi-surface-raised);
|
|
121
|
+
border-color: var(--axi-accent);
|
|
122
|
+
}
|
|
123
|
+
|
|
78
124
|
/* ---------- well ---------- */
|
|
79
125
|
/* The panel's inverse: a field sunk into the surface around it rather than
|
|
80
126
|
raised off it. No block, because a recess casts nothing, and the rule rather
|
|
81
127
|
than the ink for its edge - see --axi-well-fill in src/tokens.css for why
|
|
82
128
|
both of those follow from the same fact. Reach for this wherever a second
|
|
83
129
|
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.
|
|
130
|
+
of cards standing on the page, a trough a bar fills.
|
|
131
|
+
|
|
132
|
+
.axi-split__nav is the first of those three named as a part rather than left
|
|
133
|
+
to the consumer to remember, and it is in THIS rule rather than restating the
|
|
134
|
+
fill next to the split pane. Two rules that agree today are two rules that
|
|
135
|
+
disagree after the next edit - see "A style only reachable through a layer
|
|
136
|
+
will be re-invented" in docs/RULES.md, which is the same argument arriving
|
|
137
|
+
from the other direction. The split pane's slot cannot be a well by
|
|
138
|
+
convention: a consumer who forgets the second class gets a picker with no
|
|
139
|
+
recess, and the language has no way to notice. */
|
|
140
|
+
.axi-split__nav,
|
|
85
141
|
.axi-well {
|
|
86
142
|
background: var(--axi-well-fill);
|
|
87
143
|
border: var(--axi-border-control) solid var(--axi-well-line);
|
|
@@ -180,6 +236,43 @@ button.axi-panel--tile:hover,
|
|
|
180
236
|
icon-only button has no label to pad around, so the 20px sides meant for
|
|
181
237
|
one produced a wide rectangle around a single glyph - visible in this
|
|
182
238
|
language's own gallery, in the example titled "Alone in a button". */
|
|
239
|
+
/* Icon over label rather than beside it. Not a style choice - it is the only
|
|
240
|
+
thing that makes a bar of equal actions fit a phone. Measured on the
|
|
241
|
+
consumer that needed it: four icon+label actions side by side want 387px of
|
|
242
|
+
the 337px available at 393px wide, and every label is one unbreakable word,
|
|
243
|
+
so min-content equals max-content and flex-shrink has nothing to give. The
|
|
244
|
+
last action simply runs off the screen. Stacked, the same four come to
|
|
245
|
+
~291px.
|
|
246
|
+
|
|
247
|
+
min-width: 0 because that failure has a second half. A flex item's default
|
|
248
|
+
min-width is its content, so a button in a row refuses to shrink below its
|
|
249
|
+
own label even when the row is told to divide the space - which is the same
|
|
250
|
+
trap .axi-tabs__tab documents for its label and .axi-split__body for its
|
|
251
|
+
track.
|
|
252
|
+
|
|
253
|
+
The label is addressed as an element because a stacked button has two
|
|
254
|
+
children and only one of them is words; naming a part would make every
|
|
255
|
+
consumer add a class to say what the span already says. :where() so it
|
|
256
|
+
spends no specificity - the ink layer and any modifier still out-rank it,
|
|
257
|
+
which is the contract src/utilities.css is owed. */
|
|
258
|
+
.axi-btn--stack {
|
|
259
|
+
flex-direction: column;
|
|
260
|
+
gap: 5px;
|
|
261
|
+
min-width: 0;
|
|
262
|
+
}
|
|
263
|
+
.axi-btn--stack > :where(span) {
|
|
264
|
+
/* Both, and the pair is the point: max-width caps it against the button,
|
|
265
|
+
min-width: 0 is what lets it shrink at all. A flex item's default
|
|
266
|
+
min-width is its content, so max-width alone leaves the ellipsis
|
|
267
|
+
unreachable - the label overflows the cap instead of truncating inside
|
|
268
|
+
it. tests/tokens.test.mjs pins the pair for every truncating rule in the
|
|
269
|
+
language, and it caught this one. */
|
|
270
|
+
max-width: 100%;
|
|
271
|
+
min-width: 0;
|
|
272
|
+
overflow: hidden;
|
|
273
|
+
text-overflow: ellipsis;
|
|
274
|
+
white-space: nowrap;
|
|
275
|
+
}
|
|
183
276
|
.axi-btn--sm { --axi-btn-pad: 8px 13px; --axi-btn-size: 12px; }
|
|
184
277
|
.axi-btn--xs { --axi-btn-pad: 5px 9px; --axi-btn-size: 11px; }
|
|
185
278
|
.axi-btn--icon { --axi-btn-pad: 7px; }
|
|
@@ -478,7 +571,7 @@ button.axi-panel--tile:hover,
|
|
|
478
571
|
says `axi-link axi-ink-dim` must stay dim under the cursor, and the plain
|
|
479
572
|
`:hover` form is what made the danger button read white under one. */
|
|
480
573
|
.axi-link,
|
|
481
|
-
.axi-prose a {
|
|
574
|
+
.axi-prose :where(a) {
|
|
482
575
|
padding: 0;
|
|
483
576
|
background: none;
|
|
484
577
|
border: 0;
|
|
@@ -491,7 +584,7 @@ button.axi-panel--tile:hover,
|
|
|
491
584
|
cursor: pointer;
|
|
492
585
|
}
|
|
493
586
|
.axi-link:where(:hover),
|
|
494
|
-
.axi-prose
|
|
587
|
+
.axi-prose :where(a:hover) { color: var(--axi-text); }
|
|
495
588
|
|
|
496
589
|
/* ---------- quiet action ---------- */
|
|
497
590
|
/* A control that is only its label. Not a button - it draws no box, takes no
|
|
@@ -617,8 +710,8 @@ button.axi-panel--tile:hover,
|
|
|
617
710
|
disclosure that drops back flat the moment the pointer moves into the list
|
|
618
711
|
it opened severs the two: the lift is what says this box and that popover
|
|
619
712
|
are one control. */
|
|
620
|
-
.axi-select:hover,
|
|
621
|
-
.axi-picker__btn:hover,
|
|
713
|
+
.axi-select:where(:hover),
|
|
714
|
+
.axi-picker__btn:where(:hover),
|
|
622
715
|
.axi-picker__btn[aria-expanded='true'] {
|
|
623
716
|
color: var(--axi-text);
|
|
624
717
|
box-shadow: var(--axi-shadow-control);
|
|
@@ -645,7 +738,7 @@ button.axi-panel--tile:hover,
|
|
|
645
738
|
backdrop-filter: var(--axi-surface-filter);
|
|
646
739
|
box-shadow: var(--axi-shadow-panel);
|
|
647
740
|
}
|
|
648
|
-
.axi-select option {
|
|
741
|
+
.axi-select :where(option) {
|
|
649
742
|
display: flex; align-items: center; gap: 9px;
|
|
650
743
|
padding: 8px 9px;
|
|
651
744
|
border-radius: var(--axi-radius-sm);
|
|
@@ -660,7 +753,7 @@ button.axi-panel--tile:hover,
|
|
|
660
753
|
level's fill - and this popover is filled with --axi-surface-raised, which
|
|
661
754
|
a theme may take translucent, so an opaque row here is the black patch
|
|
662
755
|
--axi-well-fill exists to prevent. Same for the picker's rows below. */
|
|
663
|
-
.axi-select option:hover,
|
|
756
|
+
.axi-select :where(option:hover, option:focus) {
|
|
664
757
|
background: var(--axi-well-fill); color: var(--axi-text);
|
|
665
758
|
}
|
|
666
759
|
/* The page-wide focus ring sits 2px outside its element; inside a picker
|
|
@@ -759,7 +852,7 @@ button.axi-panel--tile:hover,
|
|
|
759
852
|
color: transparent;
|
|
760
853
|
font-weight: 900;
|
|
761
854
|
}
|
|
762
|
-
.axi-picker__opt:hover,
|
|
855
|
+
.axi-picker__opt:where(:hover, :focus) {
|
|
763
856
|
background: var(--axi-well-fill); color: var(--axi-text);
|
|
764
857
|
}
|
|
765
858
|
/* 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,10 +741,47 @@
|
|
|
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 ---------- */
|
|
747
|
+
/* A scrim's layer is not a property of the scrim. It is "directly below the
|
|
748
|
+
thing I dismiss", and this language has two things that get dismissed at two
|
|
749
|
+
different rungs, so the scrim has two.
|
|
750
|
+
|
|
751
|
+
The default is the drawer-and-modal rung at 50, which is where
|
|
752
|
+
--axi-scrim's own value was tuned (its token comment reads "behind a
|
|
753
|
+
drawer"). `.axi-scrim--sheet` is the same scrim one rung below the sheet at
|
|
754
|
+
45.
|
|
755
|
+
|
|
756
|
+
Without that modifier a sheet cannot be scrimmed at all, and the failure is
|
|
757
|
+
not cosmetic - it is a dead interface. Both elements are `position: fixed` in
|
|
758
|
+
the same stacking context with no isolation between them, so at 50 over 45
|
|
759
|
+
the scrim covers the sheet completely: measured with
|
|
760
|
+
`document.elementFromPoint` at the centre of an open sheet, the element
|
|
761
|
+
returned is the scrim. Every click lands on the dismiss handler instead of on
|
|
762
|
+
the content, so the sheet opens and then does nothing.
|
|
763
|
+
|
|
764
|
+
The sheet still wants a scrim even though it is opaque and covers the view,
|
|
765
|
+
for two reasons the sheet's own note explains. --axi-sheet-top pushes it down
|
|
766
|
+
under an app's titlebar, so a strip of live page stays visible above it and
|
|
767
|
+
wants darkening; and the scrim is the click-to-dismiss target for that strip,
|
|
768
|
+
which is a function rather than an appearance.
|
|
769
|
+
|
|
770
|
+
A sheet that opens a modal or a drawer is unaffected: those land at 50 and
|
|
771
|
+
51, above both the sheet and its scrim, which is the ordering the sheet's own
|
|
772
|
+
comment argues for. That is why this is a second rung and not a change to the
|
|
773
|
+
first - moving the scrim below 45 outright would put it under the drawer it
|
|
774
|
+
was measured for.
|
|
775
|
+
|
|
776
|
+
No motion here, deliberately. This language has no transition vocabulary -
|
|
777
|
+
no duration or easing tokens - and rule 11 is a restriction on work
|
|
778
|
+
indicators rather than a system for animating chrome. A consumer that fades
|
|
779
|
+
a scrim in owns that animation, and owns keeping its keyframes off the fill:
|
|
780
|
+
an enter/exit class that also sets a background is a colour literal standing
|
|
781
|
+
on top of --axi-scrim, which is how the one consumer here had quietly
|
|
782
|
+
replaced the token with rgba(10, 14, 18, .85). */
|
|
711
783
|
.axi-scrim { position: fixed; inset: 0; z-index: 50; background: var(--axi-scrim); backdrop-filter: var(--axi-surface-filter); border: 0; }
|
|
784
|
+
.axi-scrim--sheet { z-index: 44; }
|
|
712
785
|
.axi-drawer {
|
|
713
786
|
position: fixed; top: 0; right: 0; bottom: 0; z-index: 51;
|
|
714
787
|
width: min(var(--axi-drawer-width, 560px), 100vw);
|
|
@@ -741,7 +814,7 @@
|
|
|
741
814
|
font-size: 15px; font-weight: 900; cursor: pointer;
|
|
742
815
|
transition: transform .1s, box-shadow .1s;
|
|
743
816
|
}
|
|
744
|
-
.axi-drawer__close:hover {
|
|
817
|
+
.axi-drawer__close:where(:hover) {
|
|
745
818
|
background: var(--axi-accent); color: var(--axi-accent-ink);
|
|
746
819
|
box-shadow: var(--axi-shadow-control);
|
|
747
820
|
transform: translate(-2px, -2px);
|
|
@@ -883,7 +956,7 @@
|
|
|
883
956
|
-webkit-app-region: drag;
|
|
884
957
|
}
|
|
885
958
|
.axi-titlebar__btns { margin-left: auto; display: flex; height: 100%; -webkit-app-region: no-drag; }
|
|
886
|
-
.axi-titlebar__btns button {
|
|
959
|
+
.axi-titlebar__btns :where(button) {
|
|
887
960
|
width: 44px;
|
|
888
961
|
border: 0;
|
|
889
962
|
background: transparent;
|
|
@@ -894,10 +967,10 @@
|
|
|
894
967
|
/* No lift here. A window button is chrome, not content: it has nothing to
|
|
895
968
|
lift off, and 44px of movement in the corner of every screen is exactly
|
|
896
969
|
the scattered motion rule 4 rations. It answers on the neutral ramp. */
|
|
897
|
-
.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); }
|
|
898
971
|
/* Close is the one destructive control in the chrome, and the one place a
|
|
899
972
|
status ink belongs in a titlebar. */
|
|
900
|
-
.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); }
|
|
901
974
|
/* The scrolling region under the chrome. */
|
|
902
975
|
.axi-window__body { flex: 1; overflow-y: auto; }
|
|
903
976
|
|
|
@@ -916,8 +989,8 @@
|
|
|
916
989
|
flex-wrap: wrap;
|
|
917
990
|
font: var(--axi-t-small);
|
|
918
991
|
}
|
|
919
|
-
.axi-crumbs a { color: var(--axi-text-dim); text-decoration: none; }
|
|
920
|
-
.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); }
|
|
921
994
|
/* 7px, matching the prose bullet (src/prose.css) exactly, with the same
|
|
922
995
|
--axi-border-hairline outline: rule 7 calls the motif "a 45deg-rotated
|
|
923
996
|
outlined square", and a bare fill with no border does not match it. The
|
|
@@ -951,7 +1024,7 @@
|
|
|
951
1024
|
border: var(--axi-border-control) solid var(--axi-ink-line);
|
|
952
1025
|
border-radius: var(--axi-radius-sm);
|
|
953
1026
|
}
|
|
954
|
-
.axi-pages__n:hover { color: var(--axi-text); }
|
|
1027
|
+
.axi-pages__n:where(:hover) { color: var(--axi-text); }
|
|
955
1028
|
.axi-pages__n[aria-current="page"] {
|
|
956
1029
|
background: var(--axi-accent);
|
|
957
1030
|
color: var(--axi-accent-ink);
|
|
@@ -964,7 +1037,12 @@
|
|
|
964
1037
|
@media (max-width: 640px) {
|
|
965
1038
|
.axi-mast__in { min-height: 0; padding: 10px 16px; gap: 12px; }
|
|
966
1039
|
.axi-mast .axi-tabs { margin-left: 0; }
|
|
967
|
-
|
|
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; }
|
|
968
1046
|
/* The offset blocks eat horizontal room a phone does not have, so the panel
|
|
969
1047
|
step drops to the control step here rather than to an invented number.
|
|
970
1048
|
The card's hover has to come down with it - left alone it kept deepening
|
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
|
+
}
|