@axiapps/axi-design 1.9.0 → 1.11.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.
@@ -10,9 +10,19 @@
10
10
  us to ship a component for it. */
11
11
  .axi-panel {
12
12
  background: var(--axi-surface);
13
+ /* What the surface does to whatever is behind it. `none` in the main theme,
14
+ which is inert: unlike a transform, `backdrop-filter: none` establishes no
15
+ containing block, so rule 4's fixed-descendant trap is not reopened here.
16
+ A theme that makes the surfaces translucent restates this token and the
17
+ blur arrives everywhere at once. Carried only by the surfaces something
18
+ can actually be behind - a panel, a popover, an overlay, the scrim. A
19
+ chip, a switch, a page number and a card glyph sit directly on their
20
+ parent's fill with nothing but that fill behind them, so the filter would
21
+ cost a compositor layer each to blur a flat colour. */
22
+ backdrop-filter: var(--axi-surface-filter);
13
23
  border: var(--axi-border-panel) solid var(--axi-ink-line);
14
24
  border-radius: var(--axi-radius);
15
- box-shadow: var(--axi-offset-panel) var(--axi-offset-panel) 0 var(--axi-ink-line);
25
+ box-shadow: var(--axi-shadow-panel);
16
26
  /* A panel owns its padding so every consumer doesn't have to invent one;
17
27
  override per instance with --axi-panel-pad where a tighter or looser
18
28
  fit is genuinely called for. */
@@ -39,13 +49,13 @@
39
49
  vision and costs no colour. */
40
50
  .axi-btn:hover {
41
51
  color: var(--axi-text);
42
- box-shadow: var(--axi-offset-control) var(--axi-offset-control) 0 var(--axi-ink-line);
52
+ box-shadow: var(--axi-shadow-control);
43
53
  transform: translate(-2px, -2px);
44
54
  }
45
55
  .axi-btn--primary {
46
56
  background: var(--axi-accent);
47
57
  color: var(--axi-accent-ink);
48
- box-shadow: var(--axi-offset-control) var(--axi-offset-control) 0 var(--axi-ink-line);
58
+ box-shadow: var(--axi-shadow-control);
49
59
  }
50
60
  /* The one button that rests with a block already under it, so the generic
51
61
  hover's translate alone would move element and block together and leave the
@@ -53,7 +63,7 @@
53
63
  rule 4 warns about. It lifts the way a panel does: the block deepens too. */
54
64
  .axi-btn--primary:hover {
55
65
  color: var(--axi-accent-ink);
56
- box-shadow: var(--axi-offset-control-hover) var(--axi-offset-control-hover) 0 var(--axi-ink-line);
66
+ box-shadow: var(--axi-shadow-control-hover);
57
67
  }
58
68
  .axi-btn--ghost { background: transparent; }
59
69
  .axi-btn--dashed { background: transparent; border-style: dashed; }
@@ -79,13 +89,13 @@
79
89
  }
80
90
  .axi-pill:hover {
81
91
  color: var(--axi-text);
82
- box-shadow: var(--axi-offset-control) var(--axi-offset-control) 0 var(--axi-ink-line);
92
+ box-shadow: var(--axi-shadow-control);
83
93
  transform: translate(-2px, -2px);
84
94
  }
85
95
  .axi-pill[aria-pressed="true"] {
86
96
  background: var(--axi-pill-fill, var(--axi-accent));
87
97
  color: var(--axi-accent-ink);
88
- box-shadow: var(--axi-offset-control) var(--axi-offset-control) 0 var(--axi-ink-line);
98
+ box-shadow: var(--axi-shadow-control);
89
99
  }
90
100
  /* Spelled out rather than left to source order: `.axi-pill:hover` and
91
101
  `.axi-pill[aria-pressed="true"]` have identical specificity, so without this
@@ -95,7 +105,7 @@
95
105
  .axi-pill[aria-pressed="true"]:hover {
96
106
  background: var(--axi-pill-fill, var(--axi-accent));
97
107
  color: var(--axi-accent-ink);
98
- box-shadow: var(--axi-offset-control-hover) var(--axi-offset-control-hover) 0 var(--axi-ink-line);
108
+ box-shadow: var(--axi-shadow-control-hover);
99
109
  transform: translate(-2px, -2px);
100
110
  }
101
111
 
@@ -169,9 +179,9 @@
169
179
  letter-spacing: var(--axi-ls-micro);
170
180
  text-transform: uppercase;
171
181
  }
172
- .axi-chip--ok { background: var(--axi-ok); color: var(--axi-ink-line); }
173
- .axi-chip--warn { background: var(--axi-warn); color: var(--axi-ink-line); }
174
- .axi-chip--danger { background: var(--axi-danger); color: var(--axi-ink-line); }
182
+ .axi-chip--ok { background: var(--axi-ok); color: var(--axi-ink-on-fill); }
183
+ .axi-chip--warn { background: var(--axi-warn); color: var(--axi-ink-on-fill); }
184
+ .axi-chip--danger { background: var(--axi-danger); color: var(--axi-ink-on-fill); }
175
185
  .axi-chip--accent { background: var(--axi-accent); color: var(--axi-accent-ink); }
176
186
  .axi-chip--meta {
177
187
  background: transparent;
@@ -234,8 +244,11 @@
234
244
  /* ---------- select ---------- */
235
245
  /* The closed box is ours everywhere: strip the native control and draw the
236
246
  caret, so a select sits alongside the other controls as just another
237
- outlined chip instead of announcing the OS. */
238
- .axi-select {
247
+ outlined chip instead of announcing the OS. .axi-picker__btn is the same
248
+ box worn by a button instead of a <select>; the two share every
249
+ declaration here so a dropdown reads the same whichever half opens it. */
250
+ .axi-select,
251
+ .axi-picker__btn {
239
252
  appearance: none;
240
253
  padding: 10px 30px 10px 9px;
241
254
  border: var(--axi-border-control) solid var(--axi-ink-line);
@@ -256,16 +269,24 @@
256
269
  background-repeat: no-repeat;
257
270
  transition: transform .1s, box-shadow .1s;
258
271
  }
259
- .axi-select:hover {
272
+ /* The open trigger keeps the lift. Rule 4 gives the raise to a pointer, but a
273
+ disclosure that drops back flat the moment the pointer moves into the list
274
+ it opened severs the two: the lift is what says this box and that popover
275
+ are one control. */
276
+ .axi-select:hover,
277
+ .axi-picker__btn:hover,
278
+ .axi-picker__btn[aria-expanded='true'] {
260
279
  color: var(--axi-text);
261
- box-shadow: var(--axi-offset-control) var(--axi-offset-control) 0 var(--axi-ink-line);
280
+ box-shadow: var(--axi-shadow-control);
262
281
  transform: translate(-2px, -2px);
263
282
  }
264
283
 
265
284
  /* The popup stays OS chrome until a browser lets us style it. Where one does
266
285
  (Chromium's base-select), the list is drawn with the same outline and offset
267
286
  block as our own popovers, so both dropdown kinds read as one family; where
268
- it does not, the closed box above is still ours and the list is native. */
287
+ it does not, the closed box above is still ours and the list is native -
288
+ and if that native list is not good enough, .axi-picker below draws the
289
+ whole thing. */
269
290
  @supports (appearance: base-select) {
270
291
  /* base-select draws its own ::picker-icon, so the hand-drawn caret above
271
292
  would be a second arrow. */
@@ -277,7 +298,8 @@
277
298
  border: var(--axi-border-panel) solid var(--axi-ink-line);
278
299
  border-radius: var(--axi-radius);
279
300
  background: var(--axi-surface-raised);
280
- box-shadow: var(--axi-offset-panel) var(--axi-offset-panel) 0 var(--axi-ink-line);
301
+ backdrop-filter: var(--axi-surface-filter);
302
+ box-shadow: var(--axi-shadow-panel);
281
303
  }
282
304
  .axi-select option {
283
305
  display: flex; align-items: center; gap: 9px;
@@ -299,3 +321,127 @@
299
321
  .axi-select option:checked { color: var(--axi-text); }
300
322
  .axi-select option::checkmark { content: "\2713"; color: var(--axi-accent); font-weight: 900; }
301
323
  }
324
+
325
+ /* ---------- picker ---------- */
326
+ /* The select's other half. Above, the native popup is left as OS chrome
327
+ wherever `appearance: base-select` is missing - and that is most places
328
+ today, including every Electron built on a Chromium older than the
329
+ property. What lands there is a raised list the language cannot reach: no
330
+ ink outline, no offset block, its own selection colour instead of the
331
+ accent. That is rule 3 broken by a box we do not own, so the fix is to stop
332
+ asking the OS to draw it. .axi-picker is a button and a popover wearing the
333
+ closed box and the list styling from the @supports branch above, so the two
334
+ kinds are the same dropdown and a consumer picks by what the platform has.
335
+
336
+ Prefer the native <select> where it works: it comes with keyboard handling,
337
+ typeahead, and a popup that can escape the window. This is what you use
338
+ when it does not.
339
+
340
+ Markup contract - the listbox pattern, and the popover carries the panel
341
+ weight because it is a raised surface, not a control:
342
+
343
+ <div class="axi-picker">
344
+ <button class="axi-picker__btn" aria-haspopup="listbox"
345
+ aria-expanded="false" aria-controls="months">Jul 2026</button>
346
+ <div class="axi-picker__pop" id="months" role="listbox" hidden>
347
+ <button class="axi-picker__opt" role="option" aria-selected="true">
348
+ Jul 2026
349
+ </button>
350
+ </div>
351
+ </div>
352
+
353
+ Like the menu and the tooltip, the language ships no script: `hidden`,
354
+ `aria-expanded` and `aria-selected` are the whole state, and the consumer
355
+ toggles them. gallery.js is the reference wiring, arrow keys and Escape
356
+ included. */
357
+ .axi-picker { position: relative; display: inline-flex; }
358
+ .axi-picker__btn {
359
+ display: inline-flex; align-items: center;
360
+ width: 100%;
361
+ text-align: left;
362
+ }
363
+ .axi-picker__pop {
364
+ /* Above .axi-mast's z-index: 40, for the same reason .axi-menu__pop is. */
365
+ position: absolute; top: calc(100% + 9px); left: 0; z-index: 41;
366
+ /* Never narrower than the box it came out of, and no wider than its
367
+ longest row needs. A list that shrinks to the text is a list that has
368
+ moved, and the eye has to find the column again. */
369
+ min-width: 100%;
370
+ max-height: 340px; overflow-y: auto;
371
+ padding: 6px;
372
+ background: var(--axi-surface-raised);
373
+ backdrop-filter: var(--axi-surface-filter);
374
+ border: var(--axi-border-panel) solid var(--axi-ink-line);
375
+ border-radius: var(--axi-radius);
376
+ /* The block falls outside the popover's own box. An ancestor that clips -
377
+ a scrolling pane, a panel with overflow: hidden - eats it, and the only
378
+ shadow on the screen is the one that goes missing. */
379
+ box-shadow: var(--axi-shadow-panel);
380
+ }
381
+ .axi-picker__pop[hidden] { display: none; }
382
+ /* A popover cannot always live beside its trigger. Inside a pane that scrolls
383
+ or a panel that clips, `position: absolute` puts the list where the
384
+ overflow can eat it - and the offset block falls outside the popover's box,
385
+ so the block is the first thing to go. The way out is the tooltip's
386
+ contract: append the popover to <body> and set left/top from script, having
387
+ measured the trigger. The box is unchanged; only who positions it moves. */
388
+ .axi-picker__pop--fixed { position: fixed; }
389
+ .axi-picker__opt {
390
+ display: flex; align-items: center; gap: 9px;
391
+ width: 100%;
392
+ padding: 8px 9px;
393
+ border: 0;
394
+ border-radius: var(--axi-radius-sm);
395
+ background: transparent;
396
+ color: var(--axi-text-dim);
397
+ font: var(--axi-t-label);
398
+ font-size: 12.5px;
399
+ letter-spacing: var(--axi-ls-label);
400
+ text-transform: uppercase;
401
+ text-align: left;
402
+ cursor: pointer;
403
+ }
404
+ /* The tick is in every row, inked only in the chosen one. Give it to the
405
+ selected row alone and every label shifts by its width as the choice moves,
406
+ which turns picking an option into the list twitching. */
407
+ .axi-picker__opt::before {
408
+ content: "\2713";
409
+ color: transparent;
410
+ font-weight: 900;
411
+ }
412
+ .axi-picker__opt:hover, .axi-picker__opt:focus {
413
+ background: var(--axi-ground); color: var(--axi-text);
414
+ }
415
+ /* The page-wide focus ring sits 2px outside its element; inside a popover
416
+ this tight that is 2px into the neighbouring row, so pull it back in. */
417
+ .axi-picker__opt:focus-visible { outline-offset: -3px; }
418
+ .axi-picker__opt[aria-selected='true'] { color: var(--axi-text); }
419
+ .axi-picker__opt[aria-selected='true']::before { color: var(--axi-accent); }
420
+
421
+ /* ---------- avatar ---------- */
422
+ /* A square, because a circle is the one shape this language does not have:
423
+ --axi-radius is 0, and a round avatar would be the only rounded thing on
424
+ the page. It follows the radius scale like everything else, so a consumer
425
+ who wants round avatars sets --axi-radius-sm and gets rounded controls
426
+ everywhere - which is the honest version of the request.
427
+ Flat: outlined, no block. An avatar is content inside a panel, the same as
428
+ .axi-stat and .axi-table__rank, not a thing raised off it. */
429
+ .axi-avatar {
430
+ width: var(--axi-avatar-size, 40px);
431
+ height: var(--axi-avatar-size, 40px);
432
+ flex: none;
433
+ display: grid;
434
+ place-items: center;
435
+ overflow: hidden;
436
+ background: var(--axi-ground);
437
+ color: var(--axi-text-dim);
438
+ border: var(--axi-border-control) solid var(--axi-ink-line);
439
+ border-radius: var(--axi-radius-sm);
440
+ font: var(--axi-t-label);
441
+ letter-spacing: var(--axi-ls-label);
442
+ text-transform: uppercase;
443
+ }
444
+ .axi-avatar--accent { background: var(--axi-accent); color: var(--axi-accent-ink); }
445
+ /* object-fit rather than a background-image, so the <img>'s alt text survives
446
+ and a broken source is visible rather than silently blank. */
447
+ .axi-avatar__img { width: 100%; height: 100%; object-fit: cover; display: block; }
package/src/prose.css CHANGED
@@ -57,7 +57,7 @@
57
57
  background: var(--axi-ground);
58
58
  border: var(--axi-border-control) solid var(--axi-ink-line);
59
59
  border-radius: var(--axi-radius-sm);
60
- box-shadow: var(--axi-offset-control) var(--axi-offset-control) 0 var(--axi-ink-line);
60
+ box-shadow: var(--axi-shadow-control);
61
61
  }
62
62
  .axi-prose pre code { background: none; border: 0; padding: 0; font-size: 12.5px; line-height: 1.6; }
63
63
 
package/src/shells.css CHANGED
@@ -63,16 +63,17 @@
63
63
  background: var(--axi-accent);
64
64
  color: var(--axi-accent-ink);
65
65
  border-color: var(--axi-ink-line);
66
- box-shadow: var(--axi-offset-control) var(--axi-offset-control) 0 var(--axi-ink-line);
66
+ box-shadow: var(--axi-shadow-control);
67
67
  }
68
68
 
69
69
  /* ---------- notice ---------- */
70
70
  .axi-notice {
71
71
  display: flex; gap: 14px; padding: 15px 18px;
72
72
  background: var(--axi-surface);
73
+ backdrop-filter: var(--axi-surface-filter);
73
74
  border: var(--axi-border-panel) solid var(--axi-ink-line);
74
75
  border-radius: var(--axi-radius);
75
- box-shadow: var(--axi-offset-panel) var(--axi-offset-panel) 0 var(--axi-ink-line);
76
+ box-shadow: var(--axi-shadow-panel);
76
77
  }
77
78
  .axi-notice__icon {
78
79
  flex: none; width: 26px; height: 26px;
@@ -90,9 +91,10 @@
90
91
  display: flex; gap: 8px; flex-wrap: wrap; align-items: center;
91
92
  padding: 14px;
92
93
  background: var(--axi-surface);
94
+ backdrop-filter: var(--axi-surface-filter);
93
95
  border: var(--axi-border-panel) solid var(--axi-ink-line);
94
96
  border-radius: var(--axi-radius);
95
- box-shadow: var(--axi-offset-panel) var(--axi-offset-panel) 0 var(--axi-ink-line);
97
+ box-shadow: var(--axi-shadow-panel);
96
98
  }
97
99
 
98
100
  /* ---------- menu popover ---------- */
@@ -107,9 +109,10 @@
107
109
  width: var(--axi-menu-width, 310px);
108
110
  max-height: 340px; overflow-y: auto; padding: 8px;
109
111
  background: var(--axi-surface-raised);
112
+ backdrop-filter: var(--axi-surface-filter);
110
113
  border: var(--axi-border-panel) solid var(--axi-ink-line);
111
114
  border-radius: var(--axi-radius);
112
- box-shadow: var(--axi-offset-panel) var(--axi-offset-panel) 0 var(--axi-ink-line);
115
+ box-shadow: var(--axi-shadow-panel);
113
116
  }
114
117
  .axi-menu__pop[hidden] { display: none; }
115
118
  .axi-menu__pop label {
@@ -118,7 +121,7 @@
118
121
  font-size: 13px; font-weight: 600; color: var(--axi-text-dim); cursor: pointer;
119
122
  }
120
123
  .axi-menu__pop label:hover { background: var(--axi-ground); color: var(--axi-text); }
121
- .axi-menu__pop input { margin: 3px 0 0; accent-color: var(--axi-accent); }
124
+ .axi-menu__pop input { margin: 3px 0 0; }
122
125
 
123
126
  /* ---------- tooltip ---------- */
124
127
  /* One line of ink on the darkest surface in the language, so it reads over
@@ -131,7 +134,11 @@
131
134
  exists nowhere else, because a keyboard or touch user may never see it. */
132
135
  .axi-tooltip {
133
136
  position: fixed;
134
- z-index: 99999;
137
+ /* Layer 70: above the drawer and its scrim, above toasts, below a
138
+ showModal()-ed dialog's top layer - which it was below anyway, since the
139
+ top layer is above every z-index. 99999 was not a layer, it was an
140
+ argument with one. */
141
+ z-index: 70;
135
142
  padding: 5px 8px;
136
143
  background: var(--axi-ink-line);
137
144
  color: var(--axi-text);
@@ -149,15 +156,16 @@
149
156
  display: flex; flex-direction: column; gap: 11px;
150
157
  padding: 22px 16px 14px;
151
158
  background: var(--axi-surface);
159
+ backdrop-filter: var(--axi-surface-filter);
152
160
  border: var(--axi-border-panel) solid var(--axi-ink-line);
153
161
  border-radius: var(--axi-radius);
154
- box-shadow: var(--axi-offset-panel) var(--axi-offset-panel) 0 var(--axi-ink-line);
162
+ box-shadow: var(--axi-shadow-panel);
155
163
  font: inherit; color: inherit; text-decoration: none; cursor: pointer;
156
164
  transition: transform .1s, box-shadow .1s;
157
165
  }
158
166
  .axi-card:hover {
159
167
  transform: translate(-3px, -3px);
160
- box-shadow: var(--axi-offset-panel-hover) var(--axi-offset-panel-hover) 0 var(--axi-ink-line);
168
+ box-shadow: var(--axi-shadow-panel-hover);
161
169
  }
162
170
  /* The top strip is drawn from the card itself so a renderer does not have to
163
171
  emit an extra element for it. It is opt-in: a card only gets a strip when
@@ -205,12 +213,13 @@
205
213
  .axi-card:hover .axi-card__go { color: var(--axi-text); }
206
214
 
207
215
  /* ---------- drawer ---------- */
208
- .axi-scrim { position: fixed; inset: 0; z-index: 50; background: var(--axi-scrim); border: 0; }
216
+ .axi-scrim { position: fixed; inset: 0; z-index: 50; background: var(--axi-scrim); backdrop-filter: var(--axi-surface-filter); border: 0; }
209
217
  .axi-drawer {
210
218
  position: fixed; top: 0; right: 0; bottom: 0; z-index: 51;
211
219
  width: min(var(--axi-drawer-width, 560px), 100vw);
212
220
  display: flex; flex-direction: column;
213
221
  background: var(--axi-surface);
222
+ backdrop-filter: var(--axi-surface-filter);
214
223
  border-left: var(--axi-border-panel) solid var(--axi-ink-line);
215
224
  }
216
225
  .axi-drawer[hidden], .axi-scrim[hidden] { display: none; }
@@ -236,7 +245,7 @@
236
245
  }
237
246
  .axi-drawer__close:hover {
238
247
  background: var(--axi-accent); color: var(--axi-accent-ink);
239
- box-shadow: var(--axi-offset-control) var(--axi-offset-control) 0 var(--axi-ink-line);
248
+ box-shadow: var(--axi-shadow-control);
240
249
  transform: translate(-2px, -2px);
241
250
  }
242
251
  .axi-drawer__body { overflow-y: auto; padding: 18px 24px 30px; }
@@ -304,10 +313,70 @@
304
313
  .axi-titlebar__btns button:hover { background: var(--axi-surface); color: var(--axi-text); }
305
314
  /* Close is the one destructive control in the chrome, and the one place a
306
315
  status ink belongs in a titlebar. */
307
- .axi-titlebar__btns button:last-child:hover { background: var(--axi-danger); color: var(--axi-ink-line); }
316
+ .axi-titlebar__btns button:last-child:hover { background: var(--axi-danger); color: var(--axi-ink-on-fill); }
308
317
  /* The scrolling region under the chrome. */
309
318
  .axi-window__body { flex: 1; overflow-y: auto; }
310
319
 
320
+ /* ---------- breadcrumbs ---------- */
321
+ /* A path, not a control strip. The separator is the family diamond (rule 7)
322
+ drawn in the rule ink, so it reads as punctuation rather than as a third
323
+ link the reader could click.
324
+ The current crumb is NOT filled. Rule 5 reserves a fill for a state
325
+ something is in - pressed, on, current-as-in-selected - and the last crumb
326
+ is a position, not a control that is on. It takes the full text ink and the
327
+ label weight instead. */
328
+ .axi-crumbs {
329
+ display: flex;
330
+ align-items: center;
331
+ gap: 10px;
332
+ flex-wrap: wrap;
333
+ font: var(--axi-t-small);
334
+ }
335
+ .axi-crumbs a { color: var(--axi-text-dim); text-decoration: none; }
336
+ .axi-crumbs a:hover { color: var(--axi-accent); }
337
+ /* 7px, matching the prose bullet (src/prose.css) exactly, with the same
338
+ --axi-border-hairline outline: rule 7 calls the motif "a 45deg-rotated
339
+ outlined square", and a bare fill with no border does not match it. The
340
+ hairline, not one of the two form steps, is the established weight for a
341
+ diamond this small - at 6px a 2px border on each side would leave only a
342
+ 2px fill, too tight to read as a diamond at all. */
343
+ .axi-crumbs__sep {
344
+ width: 7px; height: 7px; flex: none;
345
+ background: var(--axi-rule);
346
+ border: var(--axi-border-hairline) solid var(--axi-ink-line);
347
+ transform: rotate(45deg);
348
+ }
349
+ .axi-crumbs [aria-current="page"] { color: var(--axi-text); font-weight: 800; }
350
+
351
+ /* ---------- pagination ---------- */
352
+ /* The same idea .axi-tabs already expresses, applied to a row of pages: the
353
+ current one is filled and blocked, because "this one is on" is what a fill
354
+ means here. Written as its own component rather than as a tabs modifier
355
+ because a pager sits under content and a tab strip sits in a titlebar, and
356
+ the two disagree about everything except that treatment. */
357
+ .axi-pages { display: flex; gap: 7px; align-items: center; flex-wrap: wrap; }
358
+ .axi-pages__n {
359
+ min-width: 36px;
360
+ padding: 8px 10px;
361
+ text-align: center;
362
+ font: var(--axi-t-label);
363
+ letter-spacing: var(--axi-ls-label);
364
+ color: var(--axi-text-dim);
365
+ text-decoration: none;
366
+ background: var(--axi-surface-raised);
367
+ border: var(--axi-border-control) solid var(--axi-ink-line);
368
+ border-radius: var(--axi-radius-sm);
369
+ }
370
+ .axi-pages__n:hover { color: var(--axi-text); }
371
+ .axi-pages__n[aria-current="page"] {
372
+ background: var(--axi-accent);
373
+ color: var(--axi-accent-ink);
374
+ box-shadow: var(--axi-shadow-control);
375
+ }
376
+ /* The elided run between two page numbers. Not a control and never focusable:
377
+ it is the only thing in the row a reader cannot go to. */
378
+ .axi-pages__gap { color: var(--axi-text-faint); padding: 0 2px; }
379
+
311
380
  @media (max-width: 640px) {
312
381
  .axi-mast__in { min-height: 0; padding: 10px 16px; gap: 12px; }
313
382
  .axi-tabs { margin-left: 0; width: 100%; overflow-x: auto; }
@@ -317,9 +386,136 @@
317
386
  to the panel hover block, so a card on a hover-emulating touch device
318
387
  jumped further than it does on the desktop it was tuned for. */
319
388
  .axi-notice, .axi-toolbar, .axi-card {
320
- box-shadow: var(--axi-offset-control) var(--axi-offset-control) 0 var(--axi-ink-line);
389
+ box-shadow: var(--axi-shadow-control);
321
390
  }
322
391
  .axi-card:hover {
323
- box-shadow: var(--axi-offset-control-hover) var(--axi-offset-control-hover) 0 var(--axi-ink-line);
392
+ box-shadow: var(--axi-shadow-control-hover);
324
393
  }
325
394
  }
395
+
396
+ /* ---------- modal ---------- */
397
+ /* A real <dialog>. Focus trapping, Esc-to-close, inertness for everything
398
+ behind it and promotion to the top layer all come from the browser, and
399
+ none of them can be had from CSS - so the alternative was a <div> plus a
400
+ focus trap this language has no script to write.
401
+ That choice has one visible cost, and it is named here rather than left to
402
+ be discovered: a <dialog> brings its own ::backdrop, so the modal styles
403
+ that instead of reusing .axi-scrim. The two look identical and are not the
404
+ same element. Reusing .axi-scrim would have meant giving up <dialog>.
405
+ Panel weight and panel block: it is the most raised thing on the page. */
406
+ .axi-modal {
407
+ padding: 0;
408
+ max-width: min(var(--axi-modal-width, 560px), calc(100vw - 32px));
409
+ background: var(--axi-surface);
410
+ backdrop-filter: var(--axi-surface-filter);
411
+ color: var(--axi-text);
412
+ border: var(--axi-border-panel) solid var(--axi-ink-line);
413
+ border-radius: var(--axi-radius);
414
+ box-shadow: var(--axi-shadow-panel);
415
+ }
416
+ .axi-modal::backdrop { background: var(--axi-scrim); backdrop-filter: var(--axi-surface-filter); }
417
+ /* The head and foot are divided from the body by rules, not by outlines: they
418
+ are parts of one raised thing, and rule 8's reading applies - a line inside
419
+ a raised surface separates its parts, it does not raise them. */
420
+ .axi-modal__head {
421
+ display: flex; align-items: center; gap: 12px;
422
+ padding: 15px 18px;
423
+ border-bottom: var(--axi-border-control) solid var(--axi-rule);
424
+ }
425
+ .axi-modal__head h2 { font: var(--axi-t-h3); letter-spacing: var(--axi-ls-h3); margin: 0; }
426
+ .axi-modal__body { padding: 18px; font: var(--axi-t-small); color: var(--axi-text-dim); }
427
+ .axi-modal__foot {
428
+ display: flex; justify-content: flex-end; gap: 9px;
429
+ padding: 15px 18px;
430
+ border-top: var(--axi-border-control) solid var(--axi-rule);
431
+ }
432
+
433
+ /* ---------- accordion ---------- */
434
+ /* <details>/<summary>, so it opens and closes with no script and is
435
+ announced as a disclosure. The native marker is removed and replaced with
436
+ the family diamond, which rotates a quarter turn on [open] - a transform,
437
+ so it is inside rule 4's rationing without needing rule 11. */
438
+ .axi-accordion {
439
+ background: var(--axi-surface);
440
+ backdrop-filter: var(--axi-surface-filter);
441
+ border: var(--axi-border-panel) solid var(--axi-ink-line);
442
+ border-radius: var(--axi-radius);
443
+ box-shadow: var(--axi-shadow-panel);
444
+ }
445
+ /* Stacked accordions need the gap the block occupies, or each one's block
446
+ lands on the next one's outline. */
447
+ .axi-accordion + .axi-accordion { margin-top: calc(var(--axi-offset-panel) + 8px); }
448
+ .axi-accordion__head {
449
+ display: flex; align-items: center; gap: 12px;
450
+ padding: 14px 18px;
451
+ cursor: pointer;
452
+ list-style: none;
453
+ font: var(--axi-t-label);
454
+ letter-spacing: var(--axi-ls-label);
455
+ color: var(--axi-text);
456
+ }
457
+ .axi-accordion__head::-webkit-details-marker { display: none; }
458
+ .axi-accordion__head::before {
459
+ content: "";
460
+ width: 8px; height: 8px; flex: none;
461
+ background: var(--axi-accent);
462
+ border: var(--axi-border-hairline) solid var(--axi-ink-line);
463
+ transform: rotate(45deg);
464
+ transition: transform .12s;
465
+ }
466
+ /* A 135deg turn, not the quarter turn "45deg to 180deg" might suggest by the
467
+ numbers alone: a literal quarter turn on a square lands back on the same
468
+ silhouette and would erase the open/closed cue. */
469
+ .axi-accordion[open] .axi-accordion__head::before { transform: rotate(180deg); }
470
+ .axi-accordion__body {
471
+ padding: 0 18px 16px 38px;
472
+ font: var(--axi-t-small);
473
+ color: var(--axi-text-dim);
474
+ }
475
+
476
+ /* ---------- toasts ---------- */
477
+ /* Layer 60, between the drawer and the tooltip.
478
+ The region is a layout box and nothing else, sized only by its content -
479
+ it sets no width or height of its own, so an empty one is zero-area and
480
+ pointer-events: none is not there to stop it covering the page. It is
481
+ there because the flex gaps between stacked toasts would otherwise eat
482
+ clicks aimed at the corner beneath them. The toasts themselves take their
483
+ events back.
484
+ This language ships no script, so appearing, stacking and leaving on a
485
+ timer are the consumer's: insert a .axi-toast into the region and remove it
486
+ when it is done. That is the same contract .axi-menu already publishes for
487
+ its open state, not a new kind of promise. */
488
+ .axi-toasts {
489
+ position: fixed;
490
+ right: var(--axi-gutter);
491
+ bottom: var(--axi-gutter);
492
+ z-index: 60;
493
+ display: flex;
494
+ flex-direction: column;
495
+ align-items: flex-end;
496
+ gap: calc(var(--axi-offset-control) + 9px);
497
+ pointer-events: none;
498
+ }
499
+ /* Control weight and control block, not panel: a stack of four panels over
500
+ the page is a wall. A toast is a chip-sized raised thing. */
501
+ .axi-toast {
502
+ pointer-events: auto;
503
+ display: flex; align-items: center; gap: 12px;
504
+ padding: 12px 15px;
505
+ background: var(--axi-surface-raised);
506
+ backdrop-filter: var(--axi-surface-filter);
507
+ border: var(--axi-border-control) solid var(--axi-ink-line);
508
+ border-radius: var(--axi-radius-sm);
509
+ box-shadow: var(--axi-shadow-control);
510
+ font: var(--axi-t-small);
511
+ color: var(--axi-text);
512
+ }
513
+ .axi-toast__dot {
514
+ width: 10px; height: 10px; flex: none;
515
+ background: var(--axi-accent);
516
+ border: var(--axi-border-hairline) solid var(--axi-ink-line);
517
+ transform: rotate(45deg);
518
+ }
519
+ .axi-toast--ok .axi-toast__dot { background: var(--axi-ok); }
520
+ .axi-toast--warn .axi-toast__dot { background: var(--axi-warn); }
521
+ .axi-toast--danger .axi-toast__dot { background: var(--axi-danger); }
package/src/tokens.css CHANGED
@@ -20,6 +20,15 @@
20
20
  --axi-text-dim: #a7b0be;
21
21
  --axi-text-faint: #7c8695;
22
22
  --axi-scrim: rgba(6, 7, 9, .72); /* behind a drawer */
23
+ /* What a surface does to whatever is behind it. `none` here, and `none` is
24
+ genuinely inert - it paints nothing and, unlike a transform, establishes
25
+ no containing block, so the fixed-descendant trap rule 4 documents stays
26
+ closed. It exists so a theme has somewhere to put the answer: a glass
27
+ theme makes the surfaces translucent and restates this token, and the
28
+ blur arrives on every surface at once without one component learning the
29
+ word. This is the toll the Themes section describes - a theme cannot
30
+ invent a capability, so the capability is declared here first, inert. */
31
+ --axi-surface-filter: none;
23
32
 
24
33
  /* --- accent & status --- */
25
34
  --axi-accent: #ffc53d;
@@ -27,6 +36,13 @@
27
36
  enough to pass contrast on this ground; an app choosing a dark accent
28
37
  overrides this to --axi-text rather than editing components. */
29
38
  --axi-accent-ink: var(--axi-ink-line);
39
+ /* Text drawn on top of a STATUS fill (an ok/warn/danger chip, a close
40
+ button gone red). Identical to --axi-ink-line today, which is precisely
41
+ why it has to exist: components were spelling this "the outline colour"
42
+ and it happened to read correctly. A theme that makes the outline light
43
+ would otherwise put light text on a bright fill and lose the word. Same
44
+ idea as --axi-accent-ink above, for the fills that are not the accent. */
45
+ --axi-ink-on-fill: var(--axi-ink-line);
30
46
  --axi-meta: #4ec3ff; /* the one cool ink: meta only, never a status */
31
47
  --axi-ok: #2fd38a;
32
48
  --axi-warn: #ff7a2f;
@@ -45,6 +61,15 @@
45
61
  block instead of admitting bare literals. */
46
62
  --axi-offset-panel-hover: 10px;
47
63
  --axi-offset-control-hover: 6px;
64
+ /* The block itself, composed once. A component asks for "the panel block",
65
+ not for an offset it then has to assemble into a shadow correctly four
66
+ times per file - and a theme gets one place to restate what raising a
67
+ thing looks like. The shape is still rule 3's: two equal offsets, zero
68
+ blur, the outline ink. */
69
+ --axi-shadow-panel: var(--axi-offset-panel) var(--axi-offset-panel) 0 var(--axi-ink-line);
70
+ --axi-shadow-control: var(--axi-offset-control) var(--axi-offset-control) 0 var(--axi-ink-line);
71
+ --axi-shadow-panel-hover: var(--axi-offset-panel-hover) var(--axi-offset-panel-hover) 0 var(--axi-ink-line);
72
+ --axi-shadow-control-hover: var(--axi-offset-control-hover) var(--axi-offset-control-hover) 0 var(--axi-ink-line);
48
73
  /* The form is sharp. A hard outline, a hard block and a rounded corner are
49
74
  three decisions, and the rounded one softens the other two: the corner is
50
75
  the first place a flat outlined shape can start looking like a button in