@axiapps/axi-design 1.10.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.
- package/README.md +48 -10
- package/dist/axi.css +448 -27
- package/dist/themes/glass.css +11 -0
- package/docs/RULES.md +146 -17
- package/package.json +8 -4
- package/src/feedback.css +67 -0
- package/src/forms.css +89 -0
- package/src/primitives.css +53 -13
- package/src/prose.css +1 -1
- package/src/shells.css +209 -13
- package/src/tokens.css +25 -0
- package/docs/superpowers/plans/2026-09-23-axi-docs-site.md +0 -2376
- package/docs/superpowers/specs/2026-09-23-axi-docs-site-design.md +0 -333
package/dist/axi.css
CHANGED
|
@@ -24,6 +24,15 @@
|
|
|
24
24
|
--axi-text-dim: #a7b0be;
|
|
25
25
|
--axi-text-faint: #7c8695;
|
|
26
26
|
--axi-scrim: rgba(6, 7, 9, .72); /* behind a drawer */
|
|
27
|
+
/* What a surface does to whatever is behind it. `none` here, and `none` is
|
|
28
|
+
genuinely inert - it paints nothing and, unlike a transform, establishes
|
|
29
|
+
no containing block, so the fixed-descendant trap rule 4 documents stays
|
|
30
|
+
closed. It exists so a theme has somewhere to put the answer: a glass
|
|
31
|
+
theme makes the surfaces translucent and restates this token, and the
|
|
32
|
+
blur arrives on every surface at once without one component learning the
|
|
33
|
+
word. This is the toll the Themes section describes - a theme cannot
|
|
34
|
+
invent a capability, so the capability is declared here first, inert. */
|
|
35
|
+
--axi-surface-filter: none;
|
|
27
36
|
|
|
28
37
|
/* --- accent & status --- */
|
|
29
38
|
--axi-accent: #ffc53d;
|
|
@@ -31,6 +40,13 @@
|
|
|
31
40
|
enough to pass contrast on this ground; an app choosing a dark accent
|
|
32
41
|
overrides this to --axi-text rather than editing components. */
|
|
33
42
|
--axi-accent-ink: var(--axi-ink-line);
|
|
43
|
+
/* Text drawn on top of a STATUS fill (an ok/warn/danger chip, a close
|
|
44
|
+
button gone red). Identical to --axi-ink-line today, which is precisely
|
|
45
|
+
why it has to exist: components were spelling this "the outline colour"
|
|
46
|
+
and it happened to read correctly. A theme that makes the outline light
|
|
47
|
+
would otherwise put light text on a bright fill and lose the word. Same
|
|
48
|
+
idea as --axi-accent-ink above, for the fills that are not the accent. */
|
|
49
|
+
--axi-ink-on-fill: var(--axi-ink-line);
|
|
34
50
|
--axi-meta: #4ec3ff; /* the one cool ink: meta only, never a status */
|
|
35
51
|
--axi-ok: #2fd38a;
|
|
36
52
|
--axi-warn: #ff7a2f;
|
|
@@ -49,6 +65,15 @@
|
|
|
49
65
|
block instead of admitting bare literals. */
|
|
50
66
|
--axi-offset-panel-hover: 10px;
|
|
51
67
|
--axi-offset-control-hover: 6px;
|
|
68
|
+
/* The block itself, composed once. A component asks for "the panel block",
|
|
69
|
+
not for an offset it then has to assemble into a shadow correctly four
|
|
70
|
+
times per file - and a theme gets one place to restate what raising a
|
|
71
|
+
thing looks like. The shape is still rule 3's: two equal offsets, zero
|
|
72
|
+
blur, the outline ink. */
|
|
73
|
+
--axi-shadow-panel: var(--axi-offset-panel) var(--axi-offset-panel) 0 var(--axi-ink-line);
|
|
74
|
+
--axi-shadow-control: var(--axi-offset-control) var(--axi-offset-control) 0 var(--axi-ink-line);
|
|
75
|
+
--axi-shadow-panel-hover: var(--axi-offset-panel-hover) var(--axi-offset-panel-hover) 0 var(--axi-ink-line);
|
|
76
|
+
--axi-shadow-control-hover: var(--axi-offset-control-hover) var(--axi-offset-control-hover) 0 var(--axi-ink-line);
|
|
52
77
|
/* The form is sharp. A hard outline, a hard block and a rounded corner are
|
|
53
78
|
three decisions, and the rounded one softens the other two: the corner is
|
|
54
79
|
the first place a flat outlined shape can start looking like a button in
|
|
@@ -178,9 +203,19 @@ a { color: inherit; }
|
|
|
178
203
|
us to ship a component for it. */
|
|
179
204
|
.axi-panel {
|
|
180
205
|
background: var(--axi-surface);
|
|
206
|
+
/* What the surface does to whatever is behind it. `none` in the main theme,
|
|
207
|
+
which is inert: unlike a transform, `backdrop-filter: none` establishes no
|
|
208
|
+
containing block, so rule 4's fixed-descendant trap is not reopened here.
|
|
209
|
+
A theme that makes the surfaces translucent restates this token and the
|
|
210
|
+
blur arrives everywhere at once. Carried only by the surfaces something
|
|
211
|
+
can actually be behind - a panel, a popover, an overlay, the scrim. A
|
|
212
|
+
chip, a switch, a page number and a card glyph sit directly on their
|
|
213
|
+
parent's fill with nothing but that fill behind them, so the filter would
|
|
214
|
+
cost a compositor layer each to blur a flat colour. */
|
|
215
|
+
backdrop-filter: var(--axi-surface-filter);
|
|
181
216
|
border: var(--axi-border-panel) solid var(--axi-ink-line);
|
|
182
217
|
border-radius: var(--axi-radius);
|
|
183
|
-
box-shadow: var(--axi-
|
|
218
|
+
box-shadow: var(--axi-shadow-panel);
|
|
184
219
|
/* A panel owns its padding so every consumer doesn't have to invent one;
|
|
185
220
|
override per instance with --axi-panel-pad where a tighter or looser
|
|
186
221
|
fit is genuinely called for. */
|
|
@@ -207,13 +242,13 @@ a { color: inherit; }
|
|
|
207
242
|
vision and costs no colour. */
|
|
208
243
|
.axi-btn:hover {
|
|
209
244
|
color: var(--axi-text);
|
|
210
|
-
box-shadow: var(--axi-
|
|
245
|
+
box-shadow: var(--axi-shadow-control);
|
|
211
246
|
transform: translate(-2px, -2px);
|
|
212
247
|
}
|
|
213
248
|
.axi-btn--primary {
|
|
214
249
|
background: var(--axi-accent);
|
|
215
250
|
color: var(--axi-accent-ink);
|
|
216
|
-
box-shadow: var(--axi-
|
|
251
|
+
box-shadow: var(--axi-shadow-control);
|
|
217
252
|
}
|
|
218
253
|
/* The one button that rests with a block already under it, so the generic
|
|
219
254
|
hover's translate alone would move element and block together and leave the
|
|
@@ -221,7 +256,7 @@ a { color: inherit; }
|
|
|
221
256
|
rule 4 warns about. It lifts the way a panel does: the block deepens too. */
|
|
222
257
|
.axi-btn--primary:hover {
|
|
223
258
|
color: var(--axi-accent-ink);
|
|
224
|
-
box-shadow: var(--axi-
|
|
259
|
+
box-shadow: var(--axi-shadow-control-hover);
|
|
225
260
|
}
|
|
226
261
|
.axi-btn--ghost { background: transparent; }
|
|
227
262
|
.axi-btn--dashed { background: transparent; border-style: dashed; }
|
|
@@ -247,13 +282,13 @@ a { color: inherit; }
|
|
|
247
282
|
}
|
|
248
283
|
.axi-pill:hover {
|
|
249
284
|
color: var(--axi-text);
|
|
250
|
-
box-shadow: var(--axi-
|
|
285
|
+
box-shadow: var(--axi-shadow-control);
|
|
251
286
|
transform: translate(-2px, -2px);
|
|
252
287
|
}
|
|
253
288
|
.axi-pill[aria-pressed="true"] {
|
|
254
289
|
background: var(--axi-pill-fill, var(--axi-accent));
|
|
255
290
|
color: var(--axi-accent-ink);
|
|
256
|
-
box-shadow: var(--axi-
|
|
291
|
+
box-shadow: var(--axi-shadow-control);
|
|
257
292
|
}
|
|
258
293
|
/* Spelled out rather than left to source order: `.axi-pill:hover` and
|
|
259
294
|
`.axi-pill[aria-pressed="true"]` have identical specificity, so without this
|
|
@@ -263,7 +298,7 @@ a { color: inherit; }
|
|
|
263
298
|
.axi-pill[aria-pressed="true"]:hover {
|
|
264
299
|
background: var(--axi-pill-fill, var(--axi-accent));
|
|
265
300
|
color: var(--axi-accent-ink);
|
|
266
|
-
box-shadow: var(--axi-
|
|
301
|
+
box-shadow: var(--axi-shadow-control-hover);
|
|
267
302
|
transform: translate(-2px, -2px);
|
|
268
303
|
}
|
|
269
304
|
|
|
@@ -337,9 +372,9 @@ a { color: inherit; }
|
|
|
337
372
|
letter-spacing: var(--axi-ls-micro);
|
|
338
373
|
text-transform: uppercase;
|
|
339
374
|
}
|
|
340
|
-
.axi-chip--ok { background: var(--axi-ok); color: var(--axi-ink-
|
|
341
|
-
.axi-chip--warn { background: var(--axi-warn); color: var(--axi-ink-
|
|
342
|
-
.axi-chip--danger { background: var(--axi-danger); color: var(--axi-ink-
|
|
375
|
+
.axi-chip--ok { background: var(--axi-ok); color: var(--axi-ink-on-fill); }
|
|
376
|
+
.axi-chip--warn { background: var(--axi-warn); color: var(--axi-ink-on-fill); }
|
|
377
|
+
.axi-chip--danger { background: var(--axi-danger); color: var(--axi-ink-on-fill); }
|
|
343
378
|
.axi-chip--accent { background: var(--axi-accent); color: var(--axi-accent-ink); }
|
|
344
379
|
.axi-chip--meta {
|
|
345
380
|
background: transparent;
|
|
@@ -435,7 +470,7 @@ a { color: inherit; }
|
|
|
435
470
|
.axi-picker__btn:hover,
|
|
436
471
|
.axi-picker__btn[aria-expanded='true'] {
|
|
437
472
|
color: var(--axi-text);
|
|
438
|
-
box-shadow: var(--axi-
|
|
473
|
+
box-shadow: var(--axi-shadow-control);
|
|
439
474
|
transform: translate(-2px, -2px);
|
|
440
475
|
}
|
|
441
476
|
|
|
@@ -456,7 +491,8 @@ a { color: inherit; }
|
|
|
456
491
|
border: var(--axi-border-panel) solid var(--axi-ink-line);
|
|
457
492
|
border-radius: var(--axi-radius);
|
|
458
493
|
background: var(--axi-surface-raised);
|
|
459
|
-
|
|
494
|
+
backdrop-filter: var(--axi-surface-filter);
|
|
495
|
+
box-shadow: var(--axi-shadow-panel);
|
|
460
496
|
}
|
|
461
497
|
.axi-select option {
|
|
462
498
|
display: flex; align-items: center; gap: 9px;
|
|
@@ -527,12 +563,13 @@ a { color: inherit; }
|
|
|
527
563
|
max-height: 340px; overflow-y: auto;
|
|
528
564
|
padding: 6px;
|
|
529
565
|
background: var(--axi-surface-raised);
|
|
566
|
+
backdrop-filter: var(--axi-surface-filter);
|
|
530
567
|
border: var(--axi-border-panel) solid var(--axi-ink-line);
|
|
531
568
|
border-radius: var(--axi-radius);
|
|
532
569
|
/* The block falls outside the popover's own box. An ancestor that clips -
|
|
533
570
|
a scrolling pane, a panel with overflow: hidden - eats it, and the only
|
|
534
571
|
shadow on the screen is the one that goes missing. */
|
|
535
|
-
box-shadow: var(--axi-
|
|
572
|
+
box-shadow: var(--axi-shadow-panel);
|
|
536
573
|
}
|
|
537
574
|
.axi-picker__pop[hidden] { display: none; }
|
|
538
575
|
/* A popover cannot always live beside its trigger. Inside a pane that scrolls
|
|
@@ -574,6 +611,125 @@ a { color: inherit; }
|
|
|
574
611
|
.axi-picker__opt[aria-selected='true'] { color: var(--axi-text); }
|
|
575
612
|
.axi-picker__opt[aria-selected='true']::before { color: var(--axi-accent); }
|
|
576
613
|
|
|
614
|
+
/* ---------- avatar ---------- */
|
|
615
|
+
/* A square, because a circle is the one shape this language does not have:
|
|
616
|
+
--axi-radius is 0, and a round avatar would be the only rounded thing on
|
|
617
|
+
the page. It follows the radius scale like everything else, so a consumer
|
|
618
|
+
who wants round avatars sets --axi-radius-sm and gets rounded controls
|
|
619
|
+
everywhere - which is the honest version of the request.
|
|
620
|
+
Flat: outlined, no block. An avatar is content inside a panel, the same as
|
|
621
|
+
.axi-stat and .axi-table__rank, not a thing raised off it. */
|
|
622
|
+
.axi-avatar {
|
|
623
|
+
width: var(--axi-avatar-size, 40px);
|
|
624
|
+
height: var(--axi-avatar-size, 40px);
|
|
625
|
+
flex: none;
|
|
626
|
+
display: grid;
|
|
627
|
+
place-items: center;
|
|
628
|
+
overflow: hidden;
|
|
629
|
+
background: var(--axi-ground);
|
|
630
|
+
color: var(--axi-text-dim);
|
|
631
|
+
border: var(--axi-border-control) solid var(--axi-ink-line);
|
|
632
|
+
border-radius: var(--axi-radius-sm);
|
|
633
|
+
font: var(--axi-t-label);
|
|
634
|
+
letter-spacing: var(--axi-ls-label);
|
|
635
|
+
text-transform: uppercase;
|
|
636
|
+
}
|
|
637
|
+
.axi-avatar--accent { background: var(--axi-accent); color: var(--axi-accent-ink); }
|
|
638
|
+
/* object-fit rather than a background-image, so the <img>'s alt text survives
|
|
639
|
+
and a broken source is visible rather than silently blank. */
|
|
640
|
+
.axi-avatar__img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
|
641
|
+
|
|
642
|
+
/* --- forms.css --- */
|
|
643
|
+
/* axi design language - forms.
|
|
644
|
+
Real inputs, styled. Every control here is an actual <input> or <textarea>
|
|
645
|
+
with `appearance: none`, never a <div> wearing a class: a checkbox that is
|
|
646
|
+
not an <input> does not submit with its form, does not toggle from the
|
|
647
|
+
keyboard, and is not announced as a checkbox. The language styles controls;
|
|
648
|
+
it does not reimplement them.
|
|
649
|
+
|
|
650
|
+
Rule 3's flat case governs the geometry: a control inside a panel is
|
|
651
|
+
outlined and carries no block, exactly as .axi-input and .axi-switch
|
|
652
|
+
already are. Rule 5 governs the state: the fill is what says "on".
|
|
653
|
+
|
|
654
|
+
The focus ring is deliberately absent from this file. base.css draws one
|
|
655
|
+
page-wide on :focus-visible, and appearance: none does not remove it - it
|
|
656
|
+
removes the browser's own. Suppressing the outline anywhere here would make
|
|
657
|
+
every control in it unusable by keyboard. */
|
|
658
|
+
|
|
659
|
+
/* ---------- checkbox and radio ---------- */
|
|
660
|
+
/* One box, two marks. --axi-radius is 0, so both are square and a round radio
|
|
661
|
+
would be the only rounded shape in the language - the first place a flat
|
|
662
|
+
outlined form starts looking like some other framework's. So they are told
|
|
663
|
+
apart by their mark instead: the checkbox gets a check, the radio gets the
|
|
664
|
+
family diamond (rule 7).
|
|
665
|
+
|
|
666
|
+
Both read the same two knobs on purpose. A form holding checkboxes and
|
|
667
|
+
radios together should not need two properties set to the same value to
|
|
668
|
+
keep its controls the same size. */
|
|
669
|
+
.axi-check,
|
|
670
|
+
.axi-radio {
|
|
671
|
+
appearance: none;
|
|
672
|
+
-webkit-appearance: none;
|
|
673
|
+
margin: 0;
|
|
674
|
+
flex: none;
|
|
675
|
+
width: var(--axi-check-size, 22px);
|
|
676
|
+
height: var(--axi-check-size, 22px);
|
|
677
|
+
display: inline-grid;
|
|
678
|
+
place-items: center;
|
|
679
|
+
background: var(--axi-ground);
|
|
680
|
+
border: var(--axi-border-control) solid var(--axi-ink-line);
|
|
681
|
+
border-radius: var(--axi-radius-sm);
|
|
682
|
+
cursor: pointer;
|
|
683
|
+
}
|
|
684
|
+
/* The mark is always in the DOM and revealed with opacity rather than being
|
|
685
|
+
created on :checked, so the box never changes size as it toggles. */
|
|
686
|
+
.axi-check::after {
|
|
687
|
+
content: "";
|
|
688
|
+
width: 55%;
|
|
689
|
+
height: 30%;
|
|
690
|
+
opacity: 0;
|
|
691
|
+
border-left: var(--axi-border-control) solid var(--axi-accent-ink);
|
|
692
|
+
border-bottom: var(--axi-border-control) solid var(--axi-accent-ink);
|
|
693
|
+
transform: translateY(-12%) rotate(-45deg);
|
|
694
|
+
}
|
|
695
|
+
.axi-check:checked { background: var(--axi-check-fill, var(--axi-accent)); }
|
|
696
|
+
.axi-check:checked::after { opacity: 1; }
|
|
697
|
+
/* The radio's mark is the diamond itself, so it is drawn in the fill rather
|
|
698
|
+
than on it - there is no filled box underneath it to sit on. It takes the
|
|
699
|
+
hairline outline rule 7's diamond wears at this scale (see the prose
|
|
700
|
+
bullet, src/prose.css, the other place a diamond this small is drawn) -
|
|
701
|
+
the mark is revealed with opacity, so the hairline is hidden along with
|
|
702
|
+
the fill in the unchecked state and never leaks out on its own. */
|
|
703
|
+
.axi-radio::after {
|
|
704
|
+
content: "";
|
|
705
|
+
width: 46%;
|
|
706
|
+
height: 46%;
|
|
707
|
+
opacity: 0;
|
|
708
|
+
background: var(--axi-check-fill, var(--axi-accent));
|
|
709
|
+
border: var(--axi-border-hairline) solid var(--axi-ink-line);
|
|
710
|
+
transform: rotate(45deg);
|
|
711
|
+
}
|
|
712
|
+
.axi-radio:checked::after { opacity: 1; }
|
|
713
|
+
.axi-check:disabled,
|
|
714
|
+
.axi-radio:disabled { cursor: not-allowed; opacity: .5; }
|
|
715
|
+
|
|
716
|
+
/* ---------- textarea ---------- */
|
|
717
|
+
/* Not a component: the same .axi-input a single-line field uses, with the two
|
|
718
|
+
things a <textarea> needs that an <input> does not. The font is restated
|
|
719
|
+
because a textarea does not inherit the page font in any browser, and a
|
|
720
|
+
form whose notes field is monospace while its name field is not is a bug
|
|
721
|
+
nobody files and everybody sees.
|
|
722
|
+
|
|
723
|
+
Resizing is vertical only. A textarea dragged wider than its field breaks
|
|
724
|
+
the column its form is laid out in, and the reader who did it has no way
|
|
725
|
+
to discover that the layout is not at fault. */
|
|
726
|
+
textarea.axi-input {
|
|
727
|
+
min-height: var(--axi-textarea-h, 90px);
|
|
728
|
+
resize: vertical;
|
|
729
|
+
font-family: var(--axi-sans);
|
|
730
|
+
line-height: 1.5;
|
|
731
|
+
}
|
|
732
|
+
|
|
577
733
|
/* --- layout.css --- */
|
|
578
734
|
/* axi design language - layout.
|
|
579
735
|
Three measures, one grid, two spacing helpers. Deliberately small: a
|
|
@@ -689,16 +845,17 @@ a { color: inherit; }
|
|
|
689
845
|
background: var(--axi-accent);
|
|
690
846
|
color: var(--axi-accent-ink);
|
|
691
847
|
border-color: var(--axi-ink-line);
|
|
692
|
-
box-shadow: var(--axi-
|
|
848
|
+
box-shadow: var(--axi-shadow-control);
|
|
693
849
|
}
|
|
694
850
|
|
|
695
851
|
/* ---------- notice ---------- */
|
|
696
852
|
.axi-notice {
|
|
697
853
|
display: flex; gap: 14px; padding: 15px 18px;
|
|
698
854
|
background: var(--axi-surface);
|
|
855
|
+
backdrop-filter: var(--axi-surface-filter);
|
|
699
856
|
border: var(--axi-border-panel) solid var(--axi-ink-line);
|
|
700
857
|
border-radius: var(--axi-radius);
|
|
701
|
-
box-shadow: var(--axi-
|
|
858
|
+
box-shadow: var(--axi-shadow-panel);
|
|
702
859
|
}
|
|
703
860
|
.axi-notice__icon {
|
|
704
861
|
flex: none; width: 26px; height: 26px;
|
|
@@ -716,9 +873,10 @@ a { color: inherit; }
|
|
|
716
873
|
display: flex; gap: 8px; flex-wrap: wrap; align-items: center;
|
|
717
874
|
padding: 14px;
|
|
718
875
|
background: var(--axi-surface);
|
|
876
|
+
backdrop-filter: var(--axi-surface-filter);
|
|
719
877
|
border: var(--axi-border-panel) solid var(--axi-ink-line);
|
|
720
878
|
border-radius: var(--axi-radius);
|
|
721
|
-
box-shadow: var(--axi-
|
|
879
|
+
box-shadow: var(--axi-shadow-panel);
|
|
722
880
|
}
|
|
723
881
|
|
|
724
882
|
/* ---------- menu popover ---------- */
|
|
@@ -733,9 +891,10 @@ a { color: inherit; }
|
|
|
733
891
|
width: var(--axi-menu-width, 310px);
|
|
734
892
|
max-height: 340px; overflow-y: auto; padding: 8px;
|
|
735
893
|
background: var(--axi-surface-raised);
|
|
894
|
+
backdrop-filter: var(--axi-surface-filter);
|
|
736
895
|
border: var(--axi-border-panel) solid var(--axi-ink-line);
|
|
737
896
|
border-radius: var(--axi-radius);
|
|
738
|
-
box-shadow: var(--axi-
|
|
897
|
+
box-shadow: var(--axi-shadow-panel);
|
|
739
898
|
}
|
|
740
899
|
.axi-menu__pop[hidden] { display: none; }
|
|
741
900
|
.axi-menu__pop label {
|
|
@@ -744,7 +903,7 @@ a { color: inherit; }
|
|
|
744
903
|
font-size: 13px; font-weight: 600; color: var(--axi-text-dim); cursor: pointer;
|
|
745
904
|
}
|
|
746
905
|
.axi-menu__pop label:hover { background: var(--axi-ground); color: var(--axi-text); }
|
|
747
|
-
.axi-menu__pop input { margin: 3px 0 0;
|
|
906
|
+
.axi-menu__pop input { margin: 3px 0 0; }
|
|
748
907
|
|
|
749
908
|
/* ---------- tooltip ---------- */
|
|
750
909
|
/* One line of ink on the darkest surface in the language, so it reads over
|
|
@@ -757,7 +916,11 @@ a { color: inherit; }
|
|
|
757
916
|
exists nowhere else, because a keyboard or touch user may never see it. */
|
|
758
917
|
.axi-tooltip {
|
|
759
918
|
position: fixed;
|
|
760
|
-
|
|
919
|
+
/* Layer 70: above the drawer and its scrim, above toasts, below a
|
|
920
|
+
showModal()-ed dialog's top layer - which it was below anyway, since the
|
|
921
|
+
top layer is above every z-index. 99999 was not a layer, it was an
|
|
922
|
+
argument with one. */
|
|
923
|
+
z-index: 70;
|
|
761
924
|
padding: 5px 8px;
|
|
762
925
|
background: var(--axi-ink-line);
|
|
763
926
|
color: var(--axi-text);
|
|
@@ -775,15 +938,16 @@ a { color: inherit; }
|
|
|
775
938
|
display: flex; flex-direction: column; gap: 11px;
|
|
776
939
|
padding: 22px 16px 14px;
|
|
777
940
|
background: var(--axi-surface);
|
|
941
|
+
backdrop-filter: var(--axi-surface-filter);
|
|
778
942
|
border: var(--axi-border-panel) solid var(--axi-ink-line);
|
|
779
943
|
border-radius: var(--axi-radius);
|
|
780
|
-
box-shadow: var(--axi-
|
|
944
|
+
box-shadow: var(--axi-shadow-panel);
|
|
781
945
|
font: inherit; color: inherit; text-decoration: none; cursor: pointer;
|
|
782
946
|
transition: transform .1s, box-shadow .1s;
|
|
783
947
|
}
|
|
784
948
|
.axi-card:hover {
|
|
785
949
|
transform: translate(-3px, -3px);
|
|
786
|
-
box-shadow: var(--axi-
|
|
950
|
+
box-shadow: var(--axi-shadow-panel-hover);
|
|
787
951
|
}
|
|
788
952
|
/* The top strip is drawn from the card itself so a renderer does not have to
|
|
789
953
|
emit an extra element for it. It is opt-in: a card only gets a strip when
|
|
@@ -831,12 +995,13 @@ a { color: inherit; }
|
|
|
831
995
|
.axi-card:hover .axi-card__go { color: var(--axi-text); }
|
|
832
996
|
|
|
833
997
|
/* ---------- drawer ---------- */
|
|
834
|
-
.axi-scrim { position: fixed; inset: 0; z-index: 50; background: var(--axi-scrim); border: 0; }
|
|
998
|
+
.axi-scrim { position: fixed; inset: 0; z-index: 50; background: var(--axi-scrim); backdrop-filter: var(--axi-surface-filter); border: 0; }
|
|
835
999
|
.axi-drawer {
|
|
836
1000
|
position: fixed; top: 0; right: 0; bottom: 0; z-index: 51;
|
|
837
1001
|
width: min(var(--axi-drawer-width, 560px), 100vw);
|
|
838
1002
|
display: flex; flex-direction: column;
|
|
839
1003
|
background: var(--axi-surface);
|
|
1004
|
+
backdrop-filter: var(--axi-surface-filter);
|
|
840
1005
|
border-left: var(--axi-border-panel) solid var(--axi-ink-line);
|
|
841
1006
|
}
|
|
842
1007
|
.axi-drawer[hidden], .axi-scrim[hidden] { display: none; }
|
|
@@ -862,7 +1027,7 @@ a { color: inherit; }
|
|
|
862
1027
|
}
|
|
863
1028
|
.axi-drawer__close:hover {
|
|
864
1029
|
background: var(--axi-accent); color: var(--axi-accent-ink);
|
|
865
|
-
box-shadow: var(--axi-
|
|
1030
|
+
box-shadow: var(--axi-shadow-control);
|
|
866
1031
|
transform: translate(-2px, -2px);
|
|
867
1032
|
}
|
|
868
1033
|
.axi-drawer__body { overflow-y: auto; padding: 18px 24px 30px; }
|
|
@@ -930,10 +1095,70 @@ a { color: inherit; }
|
|
|
930
1095
|
.axi-titlebar__btns button:hover { background: var(--axi-surface); color: var(--axi-text); }
|
|
931
1096
|
/* Close is the one destructive control in the chrome, and the one place a
|
|
932
1097
|
status ink belongs in a titlebar. */
|
|
933
|
-
.axi-titlebar__btns button:last-child:hover { background: var(--axi-danger); color: var(--axi-ink-
|
|
1098
|
+
.axi-titlebar__btns button:last-child:hover { background: var(--axi-danger); color: var(--axi-ink-on-fill); }
|
|
934
1099
|
/* The scrolling region under the chrome. */
|
|
935
1100
|
.axi-window__body { flex: 1; overflow-y: auto; }
|
|
936
1101
|
|
|
1102
|
+
/* ---------- breadcrumbs ---------- */
|
|
1103
|
+
/* A path, not a control strip. The separator is the family diamond (rule 7)
|
|
1104
|
+
drawn in the rule ink, so it reads as punctuation rather than as a third
|
|
1105
|
+
link the reader could click.
|
|
1106
|
+
The current crumb is NOT filled. Rule 5 reserves a fill for a state
|
|
1107
|
+
something is in - pressed, on, current-as-in-selected - and the last crumb
|
|
1108
|
+
is a position, not a control that is on. It takes the full text ink and the
|
|
1109
|
+
label weight instead. */
|
|
1110
|
+
.axi-crumbs {
|
|
1111
|
+
display: flex;
|
|
1112
|
+
align-items: center;
|
|
1113
|
+
gap: 10px;
|
|
1114
|
+
flex-wrap: wrap;
|
|
1115
|
+
font: var(--axi-t-small);
|
|
1116
|
+
}
|
|
1117
|
+
.axi-crumbs a { color: var(--axi-text-dim); text-decoration: none; }
|
|
1118
|
+
.axi-crumbs a:hover { color: var(--axi-accent); }
|
|
1119
|
+
/* 7px, matching the prose bullet (src/prose.css) exactly, with the same
|
|
1120
|
+
--axi-border-hairline outline: rule 7 calls the motif "a 45deg-rotated
|
|
1121
|
+
outlined square", and a bare fill with no border does not match it. The
|
|
1122
|
+
hairline, not one of the two form steps, is the established weight for a
|
|
1123
|
+
diamond this small - at 6px a 2px border on each side would leave only a
|
|
1124
|
+
2px fill, too tight to read as a diamond at all. */
|
|
1125
|
+
.axi-crumbs__sep {
|
|
1126
|
+
width: 7px; height: 7px; flex: none;
|
|
1127
|
+
background: var(--axi-rule);
|
|
1128
|
+
border: var(--axi-border-hairline) solid var(--axi-ink-line);
|
|
1129
|
+
transform: rotate(45deg);
|
|
1130
|
+
}
|
|
1131
|
+
.axi-crumbs [aria-current="page"] { color: var(--axi-text); font-weight: 800; }
|
|
1132
|
+
|
|
1133
|
+
/* ---------- pagination ---------- */
|
|
1134
|
+
/* The same idea .axi-tabs already expresses, applied to a row of pages: the
|
|
1135
|
+
current one is filled and blocked, because "this one is on" is what a fill
|
|
1136
|
+
means here. Written as its own component rather than as a tabs modifier
|
|
1137
|
+
because a pager sits under content and a tab strip sits in a titlebar, and
|
|
1138
|
+
the two disagree about everything except that treatment. */
|
|
1139
|
+
.axi-pages { display: flex; gap: 7px; align-items: center; flex-wrap: wrap; }
|
|
1140
|
+
.axi-pages__n {
|
|
1141
|
+
min-width: 36px;
|
|
1142
|
+
padding: 8px 10px;
|
|
1143
|
+
text-align: center;
|
|
1144
|
+
font: var(--axi-t-label);
|
|
1145
|
+
letter-spacing: var(--axi-ls-label);
|
|
1146
|
+
color: var(--axi-text-dim);
|
|
1147
|
+
text-decoration: none;
|
|
1148
|
+
background: var(--axi-surface-raised);
|
|
1149
|
+
border: var(--axi-border-control) solid var(--axi-ink-line);
|
|
1150
|
+
border-radius: var(--axi-radius-sm);
|
|
1151
|
+
}
|
|
1152
|
+
.axi-pages__n:hover { color: var(--axi-text); }
|
|
1153
|
+
.axi-pages__n[aria-current="page"] {
|
|
1154
|
+
background: var(--axi-accent);
|
|
1155
|
+
color: var(--axi-accent-ink);
|
|
1156
|
+
box-shadow: var(--axi-shadow-control);
|
|
1157
|
+
}
|
|
1158
|
+
/* The elided run between two page numbers. Not a control and never focusable:
|
|
1159
|
+
it is the only thing in the row a reader cannot go to. */
|
|
1160
|
+
.axi-pages__gap { color: var(--axi-text-faint); padding: 0 2px; }
|
|
1161
|
+
|
|
937
1162
|
@media (max-width: 640px) {
|
|
938
1163
|
.axi-mast__in { min-height: 0; padding: 10px 16px; gap: 12px; }
|
|
939
1164
|
.axi-tabs { margin-left: 0; width: 100%; overflow-x: auto; }
|
|
@@ -943,13 +1168,140 @@ a { color: inherit; }
|
|
|
943
1168
|
to the panel hover block, so a card on a hover-emulating touch device
|
|
944
1169
|
jumped further than it does on the desktop it was tuned for. */
|
|
945
1170
|
.axi-notice, .axi-toolbar, .axi-card {
|
|
946
|
-
box-shadow: var(--axi-
|
|
1171
|
+
box-shadow: var(--axi-shadow-control);
|
|
947
1172
|
}
|
|
948
1173
|
.axi-card:hover {
|
|
949
|
-
box-shadow: var(--axi-
|
|
1174
|
+
box-shadow: var(--axi-shadow-control-hover);
|
|
950
1175
|
}
|
|
951
1176
|
}
|
|
952
1177
|
|
|
1178
|
+
/* ---------- modal ---------- */
|
|
1179
|
+
/* A real <dialog>. Focus trapping, Esc-to-close, inertness for everything
|
|
1180
|
+
behind it and promotion to the top layer all come from the browser, and
|
|
1181
|
+
none of them can be had from CSS - so the alternative was a <div> plus a
|
|
1182
|
+
focus trap this language has no script to write.
|
|
1183
|
+
That choice has one visible cost, and it is named here rather than left to
|
|
1184
|
+
be discovered: a <dialog> brings its own ::backdrop, so the modal styles
|
|
1185
|
+
that instead of reusing .axi-scrim. The two look identical and are not the
|
|
1186
|
+
same element. Reusing .axi-scrim would have meant giving up <dialog>.
|
|
1187
|
+
Panel weight and panel block: it is the most raised thing on the page. */
|
|
1188
|
+
.axi-modal {
|
|
1189
|
+
padding: 0;
|
|
1190
|
+
max-width: min(var(--axi-modal-width, 560px), calc(100vw - 32px));
|
|
1191
|
+
background: var(--axi-surface);
|
|
1192
|
+
backdrop-filter: var(--axi-surface-filter);
|
|
1193
|
+
color: var(--axi-text);
|
|
1194
|
+
border: var(--axi-border-panel) solid var(--axi-ink-line);
|
|
1195
|
+
border-radius: var(--axi-radius);
|
|
1196
|
+
box-shadow: var(--axi-shadow-panel);
|
|
1197
|
+
}
|
|
1198
|
+
.axi-modal::backdrop { background: var(--axi-scrim); backdrop-filter: var(--axi-surface-filter); }
|
|
1199
|
+
/* The head and foot are divided from the body by rules, not by outlines: they
|
|
1200
|
+
are parts of one raised thing, and rule 8's reading applies - a line inside
|
|
1201
|
+
a raised surface separates its parts, it does not raise them. */
|
|
1202
|
+
.axi-modal__head {
|
|
1203
|
+
display: flex; align-items: center; gap: 12px;
|
|
1204
|
+
padding: 15px 18px;
|
|
1205
|
+
border-bottom: var(--axi-border-control) solid var(--axi-rule);
|
|
1206
|
+
}
|
|
1207
|
+
.axi-modal__head h2 { font: var(--axi-t-h3); letter-spacing: var(--axi-ls-h3); margin: 0; }
|
|
1208
|
+
.axi-modal__body { padding: 18px; font: var(--axi-t-small); color: var(--axi-text-dim); }
|
|
1209
|
+
.axi-modal__foot {
|
|
1210
|
+
display: flex; justify-content: flex-end; gap: 9px;
|
|
1211
|
+
padding: 15px 18px;
|
|
1212
|
+
border-top: var(--axi-border-control) solid var(--axi-rule);
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
/* ---------- accordion ---------- */
|
|
1216
|
+
/* <details>/<summary>, so it opens and closes with no script and is
|
|
1217
|
+
announced as a disclosure. The native marker is removed and replaced with
|
|
1218
|
+
the family diamond, which rotates a quarter turn on [open] - a transform,
|
|
1219
|
+
so it is inside rule 4's rationing without needing rule 11. */
|
|
1220
|
+
.axi-accordion {
|
|
1221
|
+
background: var(--axi-surface);
|
|
1222
|
+
backdrop-filter: var(--axi-surface-filter);
|
|
1223
|
+
border: var(--axi-border-panel) solid var(--axi-ink-line);
|
|
1224
|
+
border-radius: var(--axi-radius);
|
|
1225
|
+
box-shadow: var(--axi-shadow-panel);
|
|
1226
|
+
}
|
|
1227
|
+
/* Stacked accordions need the gap the block occupies, or each one's block
|
|
1228
|
+
lands on the next one's outline. */
|
|
1229
|
+
.axi-accordion + .axi-accordion { margin-top: calc(var(--axi-offset-panel) + 8px); }
|
|
1230
|
+
.axi-accordion__head {
|
|
1231
|
+
display: flex; align-items: center; gap: 12px;
|
|
1232
|
+
padding: 14px 18px;
|
|
1233
|
+
cursor: pointer;
|
|
1234
|
+
list-style: none;
|
|
1235
|
+
font: var(--axi-t-label);
|
|
1236
|
+
letter-spacing: var(--axi-ls-label);
|
|
1237
|
+
color: var(--axi-text);
|
|
1238
|
+
}
|
|
1239
|
+
.axi-accordion__head::-webkit-details-marker { display: none; }
|
|
1240
|
+
.axi-accordion__head::before {
|
|
1241
|
+
content: "";
|
|
1242
|
+
width: 8px; height: 8px; flex: none;
|
|
1243
|
+
background: var(--axi-accent);
|
|
1244
|
+
border: var(--axi-border-hairline) solid var(--axi-ink-line);
|
|
1245
|
+
transform: rotate(45deg);
|
|
1246
|
+
transition: transform .12s;
|
|
1247
|
+
}
|
|
1248
|
+
/* A 135deg turn, not the quarter turn "45deg to 180deg" might suggest by the
|
|
1249
|
+
numbers alone: a literal quarter turn on a square lands back on the same
|
|
1250
|
+
silhouette and would erase the open/closed cue. */
|
|
1251
|
+
.axi-accordion[open] .axi-accordion__head::before { transform: rotate(180deg); }
|
|
1252
|
+
.axi-accordion__body {
|
|
1253
|
+
padding: 0 18px 16px 38px;
|
|
1254
|
+
font: var(--axi-t-small);
|
|
1255
|
+
color: var(--axi-text-dim);
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
/* ---------- toasts ---------- */
|
|
1259
|
+
/* Layer 60, between the drawer and the tooltip.
|
|
1260
|
+
The region is a layout box and nothing else, sized only by its content -
|
|
1261
|
+
it sets no width or height of its own, so an empty one is zero-area and
|
|
1262
|
+
pointer-events: none is not there to stop it covering the page. It is
|
|
1263
|
+
there because the flex gaps between stacked toasts would otherwise eat
|
|
1264
|
+
clicks aimed at the corner beneath them. The toasts themselves take their
|
|
1265
|
+
events back.
|
|
1266
|
+
This language ships no script, so appearing, stacking and leaving on a
|
|
1267
|
+
timer are the consumer's: insert a .axi-toast into the region and remove it
|
|
1268
|
+
when it is done. That is the same contract .axi-menu already publishes for
|
|
1269
|
+
its open state, not a new kind of promise. */
|
|
1270
|
+
.axi-toasts {
|
|
1271
|
+
position: fixed;
|
|
1272
|
+
right: var(--axi-gutter);
|
|
1273
|
+
bottom: var(--axi-gutter);
|
|
1274
|
+
z-index: 60;
|
|
1275
|
+
display: flex;
|
|
1276
|
+
flex-direction: column;
|
|
1277
|
+
align-items: flex-end;
|
|
1278
|
+
gap: calc(var(--axi-offset-control) + 9px);
|
|
1279
|
+
pointer-events: none;
|
|
1280
|
+
}
|
|
1281
|
+
/* Control weight and control block, not panel: a stack of four panels over
|
|
1282
|
+
the page is a wall. A toast is a chip-sized raised thing. */
|
|
1283
|
+
.axi-toast {
|
|
1284
|
+
pointer-events: auto;
|
|
1285
|
+
display: flex; align-items: center; gap: 12px;
|
|
1286
|
+
padding: 12px 15px;
|
|
1287
|
+
background: var(--axi-surface-raised);
|
|
1288
|
+
backdrop-filter: var(--axi-surface-filter);
|
|
1289
|
+
border: var(--axi-border-control) solid var(--axi-ink-line);
|
|
1290
|
+
border-radius: var(--axi-radius-sm);
|
|
1291
|
+
box-shadow: var(--axi-shadow-control);
|
|
1292
|
+
font: var(--axi-t-small);
|
|
1293
|
+
color: var(--axi-text);
|
|
1294
|
+
}
|
|
1295
|
+
.axi-toast__dot {
|
|
1296
|
+
width: 10px; height: 10px; flex: none;
|
|
1297
|
+
background: var(--axi-accent);
|
|
1298
|
+
border: var(--axi-border-hairline) solid var(--axi-ink-line);
|
|
1299
|
+
transform: rotate(45deg);
|
|
1300
|
+
}
|
|
1301
|
+
.axi-toast--ok .axi-toast__dot { background: var(--axi-ok); }
|
|
1302
|
+
.axi-toast--warn .axi-toast__dot { background: var(--axi-warn); }
|
|
1303
|
+
.axi-toast--danger .axi-toast__dot { background: var(--axi-danger); }
|
|
1304
|
+
|
|
953
1305
|
/* --- data.css --- */
|
|
954
1306
|
/* axi design language - data.
|
|
955
1307
|
Numbers, and the shapes numbers are drawn as. Everything here is panel
|
|
@@ -1231,6 +1583,75 @@ a { color: inherit; }
|
|
|
1231
1583
|
color: var(--axi-text-dim);
|
|
1232
1584
|
}
|
|
1233
1585
|
|
|
1586
|
+
/* --- feedback.css --- */
|
|
1587
|
+
/* axi design language - feedback.
|
|
1588
|
+
Things that report on work, and things that report on how work went. Rule
|
|
1589
|
+
11 governs the first kind and is the one rule in this language described as
|
|
1590
|
+
non-negotiable: an indicator of work may animate only `transform` and
|
|
1591
|
+
`opacity`, because those are the two the compositor runs off the main
|
|
1592
|
+
thread. A spinner animated by layout freezes with the work it reports on,
|
|
1593
|
+
and a frozen spinner tells the reader the app crashed at the exact moment
|
|
1594
|
+
it was working hardest. tests/motion.test.mjs enforces it. */
|
|
1595
|
+
|
|
1596
|
+
/* ---------- spinner ---------- */
|
|
1597
|
+
/* The family motif, turning. A square rotated 45deg is the diamond, so the
|
|
1598
|
+
loop runs 45deg -> 405deg: one full turn that begins and ends on the
|
|
1599
|
+
motif rather than on a square.
|
|
1600
|
+
That start and end also make it correct under reduced motion for free.
|
|
1601
|
+
base.css only shortens the animation to one .01ms iteration; it never sets
|
|
1602
|
+
animation-fill-mode, so once that instant run finishes the element reverts
|
|
1603
|
+
to its specified transform: rotate(45deg) - the motif at rest, and visually
|
|
1604
|
+
identical to where the loop begins. */
|
|
1605
|
+
@keyframes axi-spin { from { transform: rotate(45deg); } to { transform: rotate(405deg); } }
|
|
1606
|
+
.axi-spinner {
|
|
1607
|
+
display: inline-block;
|
|
1608
|
+
flex: none;
|
|
1609
|
+
width: var(--axi-spinner-size, 20px);
|
|
1610
|
+
height: var(--axi-spinner-size, 20px);
|
|
1611
|
+
border: var(--axi-border-control) solid var(--axi-accent);
|
|
1612
|
+
border-radius: var(--axi-radius-sm);
|
|
1613
|
+
transform: rotate(45deg);
|
|
1614
|
+
animation: axi-spin 1.1s linear infinite;
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
/* ---------- indeterminate meter ---------- */
|
|
1618
|
+
/* The .axi-meter that does not know how far along it is. The fill keeps a
|
|
1619
|
+
fixed width and travels; `width` is never animated, which is the whole of
|
|
1620
|
+
rule 11. --axi-meter-v is ignored in this mode - there is no value to
|
|
1621
|
+
express, which is what indeterminate means. */
|
|
1622
|
+
@keyframes axi-meter-busy {
|
|
1623
|
+
from { transform: translateX(-100%); }
|
|
1624
|
+
to { transform: translateX(400%); }
|
|
1625
|
+
}
|
|
1626
|
+
.axi-meter--busy .axi-meter__fill {
|
|
1627
|
+
width: 25%;
|
|
1628
|
+
animation: axi-meter-busy 1.3s ease-in-out infinite;
|
|
1629
|
+
}
|
|
1630
|
+
/* base.css only ever sets animation-duration and animation-iteration-count
|
|
1631
|
+
under reduced motion; it never touches animation-fill-mode. The shorthand
|
|
1632
|
+
above resets animation-fill-mode to its default, none, so once the .01ms
|
|
1633
|
+
run finishes the element reverts to its specified style rather than
|
|
1634
|
+
holding the keyframe it ended on - here that is transform: none and the
|
|
1635
|
+
fill's own width: 25%, a left-anchored quarter-width bar. That is already
|
|
1636
|
+
the right thing to show: visible, and unmistakably not finished. This rule
|
|
1637
|
+
states transform: none explicitly rather than relying on the revert, so
|
|
1638
|
+
the "at rest" case is not accidental. */
|
|
1639
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1640
|
+
.axi-meter--busy .axi-meter__fill { transform: none; }
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
/* ---------- notice status ---------- */
|
|
1644
|
+
/* Rule 5: the icon is the part of a notice that asserts something, so the
|
|
1645
|
+
status lives there. The body text stays in the reading ink at every status
|
|
1646
|
+
- a whole paragraph in the danger ink is the tinted-everything failure rule
|
|
1647
|
+
2 exists to prevent, and it is unreadable besides. */
|
|
1648
|
+
.axi-notice--ok .axi-notice__icon { background: var(--axi-ok); }
|
|
1649
|
+
.axi-notice--warn .axi-notice__icon { background: var(--axi-warn); }
|
|
1650
|
+
.axi-notice--danger .axi-notice__icon { background: var(--axi-danger); }
|
|
1651
|
+
.axi-notice--ok b { color: var(--axi-ok); }
|
|
1652
|
+
.axi-notice--warn b { color: var(--axi-warn); }
|
|
1653
|
+
.axi-notice--danger b { color: var(--axi-danger); }
|
|
1654
|
+
|
|
1234
1655
|
/* --- prose.css --- */
|
|
1235
1656
|
/* axi design language - prose.
|
|
1236
1657
|
A single class wrapping rendered markdown, so a content pipeline can emit
|
|
@@ -1291,7 +1712,7 @@ a { color: inherit; }
|
|
|
1291
1712
|
background: var(--axi-ground);
|
|
1292
1713
|
border: var(--axi-border-control) solid var(--axi-ink-line);
|
|
1293
1714
|
border-radius: var(--axi-radius-sm);
|
|
1294
|
-
box-shadow: var(--axi-
|
|
1715
|
+
box-shadow: var(--axi-shadow-control);
|
|
1295
1716
|
}
|
|
1296
1717
|
.axi-prose pre code { background: none; border: 0; padding: 0; font-size: 12.5px; line-height: 1.6; }
|
|
1297
1718
|
|