@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
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/* axi design language - generated by scripts/build.mjs.
|
|
2
|
+
Edit the files in src/ and run `npm run build`; do not edit this file. */
|
|
3
|
+
|
|
4
|
+
[data-axi-theme="glass"] {
|
|
5
|
+
--axi-ground: #141924;
|
|
6
|
+
--axi-surface: rgba(255, 255, 255, .09);
|
|
7
|
+
--axi-surface-raised: rgba(255, 255, 255, .15);
|
|
8
|
+
--axi-rule: rgba(255, 255, 255, .17);
|
|
9
|
+
--axi-scrim: rgba(6, 7, 9, .55);
|
|
10
|
+
--axi-surface-filter: blur(14px) saturate(140%);
|
|
11
|
+
}
|
package/docs/RULES.md
CHANGED
|
@@ -14,7 +14,12 @@ to draw a *shape*, with no soft transition anywhere in them: the select caret
|
|
|
14
14
|
(two `linear-gradient`s meeting to make a triangle) and `.axi-plot`'s
|
|
15
15
|
gridlines (a `repeating-linear-gradient` of hard stops, which is how N evenly
|
|
16
16
|
spaced rules get drawn without asking every consumer to emit N empty divs).
|
|
17
|
-
A gradient across a surface is still forbidden
|
|
17
|
+
A gradient across a surface is still forbidden in every component file, and
|
|
18
|
+
always will be. The single relief is a *theme* restating the surface tokens
|
|
19
|
+
themselves — see [Themes](#themes) — which is what lets a glass theme exist
|
|
20
|
+
without one component ever learning the word "glass". A component cannot reach
|
|
21
|
+
for that exception, because it cannot see it: what it reads is the same surface
|
|
22
|
+
token it was already reading.
|
|
18
23
|
|
|
19
24
|
## 2. No colour at partial opacity over the ground
|
|
20
25
|
|
|
@@ -23,9 +28,19 @@ just brown, and five muted inks over near-black are five browns. When something
|
|
|
23
28
|
should be quieter, reach for a neutral from the ramp — that is what the ramp is
|
|
24
29
|
for.
|
|
25
30
|
|
|
31
|
+
A theme gets the same carve-out as rule 1 and not one inch more: it may hold the
|
|
32
|
+
*surface* tokens at partial opacity, because a translucent surface is that
|
|
33
|
+
surface's own definition rather than a colour laid over the ground. The inks are
|
|
34
|
+
untouched. A muted `--axi-warn`, a faded accent, a status ink at 60% — still
|
|
35
|
+
forbidden, in a theme exactly as in a component, because the paragraph above is
|
|
36
|
+
about what happens to meaning when five inks become five browns, and changing
|
|
37
|
+
which stylesheet does the muting does not change that.
|
|
38
|
+
|
|
26
39
|
## 3. Every raised element is outlined and blocked
|
|
27
40
|
|
|
28
41
|
An `--axi-ink-line` border plus a hard offset shadow, never a blur.
|
|
42
|
+
A component spells that shadow `var(--axi-shadow-panel)` or
|
|
43
|
+
`var(--axi-shadow-control)`; the offsets those compose are in the table below.
|
|
29
44
|
|
|
30
45
|
Two weight steps, and only two:
|
|
31
46
|
|
|
@@ -37,10 +52,21 @@ Two weight steps, and only two:
|
|
|
37
52
|
A third step is how a system stops looking like one system.
|
|
38
53
|
|
|
39
54
|
There is one weight outside the table, and it is deliberately not a step:
|
|
40
|
-
`--axi-border-hairline` (2px)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
55
|
+
`--axi-border-hairline` (2px). It is a **rule** weight — a line drawn inside
|
|
56
|
+
content to separate parts of it, where either form step would turn a list of
|
|
57
|
+
numbers into a grid of boxes. That covers inline code and the list bullet in
|
|
58
|
+
`.axi-prose`, the rules between table rows, and the gridlines inside a plot.
|
|
59
|
+
It is never an outline on a raised thing: the two steps in the table above are
|
|
60
|
+
what raise a surface, and reaching for the hairline instead is how a panel
|
|
61
|
+
stops looking raised.
|
|
62
|
+
|
|
63
|
+
There is exactly one element drawn in the line ink rather than on a surface,
|
|
64
|
+
and it is named here so it stays an exception rather than becoming a habit:
|
|
65
|
+
`.axi-tooltip` is filled with `--axi-ink-line` itself. A thing cannot be
|
|
66
|
+
outlined in the colour it is already made of, and a block in that same ink
|
|
67
|
+
under a box already made of it reads as the box being thicker rather than
|
|
68
|
+
raised — so the tooltip takes a hairline in `--axi-rule` to hold its edge
|
|
69
|
+
against the page, and carries no block. Nothing else may use that reasoning.
|
|
44
70
|
|
|
45
71
|
**What is mechanically enforced.** `tests/tokens.test.mjs` enforces both
|
|
46
72
|
columns:
|
|
@@ -52,18 +78,24 @@ columns:
|
|
|
52
78
|
`outline`/`outline-width` are checked the same way as `border`
|
|
53
79
|
(`outline-offset` and `outline-color` are not weight properties and are
|
|
54
80
|
untouched).
|
|
55
|
-
- *Offset* —
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
81
|
+
- *Offset* — the block is composed once, in `tokens.css`, as one of four
|
|
82
|
+
`--axi-shadow-*` tokens, and each must be exactly
|
|
83
|
+
`<offset> <offset> 0 var(--axi-ink-line)` with the offset drawn from an
|
|
84
|
+
enumerated list of four: the two resting steps above, plus the two hover
|
|
85
|
+
deepenings rule 4 describes (`--axi-offset-panel-hover` 10px,
|
|
59
86
|
`--axi-offset-control-hover` 6px). That is what rules out a blur, a spread,
|
|
60
|
-
an invented offset and a shadow in any colour but the ink line.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
87
|
+
an invented offset and a shadow in any colour but the ink line. A component
|
|
88
|
+
file then names one of the four — `box-shadow: var(--axi-shadow-panel)` —
|
|
89
|
+
and may write nothing else in a `box-shadow`. Two checks rather than one,
|
|
90
|
+
because either alone is hollow: the shape check protects the four
|
|
91
|
+
definitions, the naming check stops a component composing its own block
|
|
92
|
+
beside them. `filter: drop-shadow(...)` and `text-shadow` — the two other
|
|
93
|
+
CSS properties that can draw the same blurred look — are forbidden
|
|
94
|
+
outright, since nothing in this language legitimately reaches for either.
|
|
64
95
|
- *No local escape hatch* — the form tokens themselves
|
|
65
96
|
(`--axi-border-panel`, `--axi-border-control`, `--axi-border-hairline`,
|
|
66
|
-
`--axi-offset-panel`, `--axi-offset-control`,
|
|
97
|
+
`--axi-offset-panel`, `--axi-offset-control`, their `-hover` variants, and
|
|
98
|
+
the four `--axi-shadow-*` blocks composed from them)
|
|
67
99
|
may be **declared** only in `tokens.css`. A component file redeclaring one
|
|
68
100
|
of these on itself would change the value the border/offset checks above
|
|
69
101
|
are silently trusting, without changing the `var()` text those checks read
|
|
@@ -173,8 +205,12 @@ than framing it down the left edge. A full-height stripe runs the height of the
|
|
|
173
205
|
box, so it reads as the box's border: five cards in a row become five coloured
|
|
174
206
|
frames, and the colour stops saying anything about any one number. A cap sits
|
|
175
207
|
directly over the reading it is a verdict on, identifies it once, and then gets
|
|
176
|
-
out of the way. Under rule 3 the cap
|
|
177
|
-
|
|
208
|
+
out of the way. Under rule 3 both the cap's edge and the card's own outline are
|
|
209
|
+
drawn at the panel weight, because the card is a raised surface and rule 3
|
|
210
|
+
pairs the panel border with the panel offset the card already carries. A card
|
|
211
|
+
outlined at the control weight would pair a 3px border with a 6px offset —
|
|
212
|
+
a third step in everything but name — and would put a thin frame around a
|
|
213
|
+
heavier bar.
|
|
178
214
|
|
|
179
215
|
A switch is the same rule in a slot. Its track fills to assert the setting's
|
|
180
216
|
status and is empty otherwise; the slug that moves is `--axi-ink-line` in both
|
|
@@ -236,6 +272,14 @@ Three bounds, and the rule is only sound with all three:
|
|
|
236
272
|
carries a 6px block. A second 6px block nested in the first reads as two
|
|
237
273
|
planes arguing; the 3px control step reads as the contents of a box. This is
|
|
238
274
|
also the honest weight: each row is a control you press, not a surface.
|
|
275
|
+
- **Text on a status fill is `--axi-ink-on-fill`, not `--axi-ink-line`.** The
|
|
276
|
+
two hold the same near-black today, which is why this was easy to get wrong:
|
|
277
|
+
a chip that says `color: var(--axi-ink-line)` reads correctly and still means
|
|
278
|
+
the wrong thing. `--axi-ink-line` is *the colour a shape is outlined in*;
|
|
279
|
+
`--axi-ink-on-fill` is *the colour a word is written in when it sits on a
|
|
280
|
+
saturated fill*. They only have to diverge once — a theme that outlines in a
|
|
281
|
+
light colour — for the conflated spelling to put light text on a bright chip.
|
|
282
|
+
`--axi-accent-ink` is the same distinction for the accent fill specifically.
|
|
239
283
|
- **The outline stays `--axi-ink-line`.** Status goes on a filled shape inside
|
|
240
284
|
the row — the icon tile, a chip — and never on the row's own edge. Colouring
|
|
241
285
|
the edge is exactly the full-height stripe rule 5 rejects: five states become
|
|
@@ -328,6 +372,31 @@ If an app picks an accent dark enough that near-black text on it fails
|
|
|
328
372
|
contrast, it also sets `--axi-accent-ink: var(--axi-text)`. It should not edit
|
|
329
373
|
components.
|
|
330
374
|
|
|
375
|
+
### Layer stack
|
|
376
|
+
|
|
377
|
+
Every `z-index` in this language is one of the layers below, and a component
|
|
378
|
+
that needs to sit above something picks its layer here rather than inventing a
|
|
379
|
+
number. A number not in this table is a component shouting over the stack
|
|
380
|
+
instead of joining it.
|
|
381
|
+
|
|
382
|
+
| Layer | z-index | What sits here |
|
|
383
|
+
|---|---|---|
|
|
384
|
+
| Sticky chrome | 40 | `.axi-mast` |
|
|
385
|
+
| Popovers | 41 | `.axi-menu__pop`, `.axi-picker__pop` |
|
|
386
|
+
| Scrim | 50 | `.axi-scrim` |
|
|
387
|
+
| Drawer | 51 | `.axi-drawer` |
|
|
388
|
+
| Toasts | 60 | `.axi-toasts` |
|
|
389
|
+
| Tooltip | 70 | `.axi-tooltip` |
|
|
390
|
+
| Modal | top layer | `dialog.axi-modal`, promoted by `showModal()` |
|
|
391
|
+
|
|
392
|
+
The modal has no number on purpose. A `<dialog>` opened with `showModal()` is
|
|
393
|
+
promoted to the browser's top layer, which sits above every `z-index` there is;
|
|
394
|
+
writing a number in that row would describe a competition the modal is not in.
|
|
395
|
+
|
|
396
|
+
A negative `z-index` inside a component's own `isolation` context — the sigil's
|
|
397
|
+
backing shape — is not a layer and is not listed. It is invisible outside the
|
|
398
|
+
component that owns it.
|
|
399
|
+
|
|
331
400
|
## Light mode
|
|
332
401
|
|
|
333
402
|
Not shipped. The system is *structured* for it: no component contains a colour
|
|
@@ -335,6 +404,9 @@ literal, so a light theme is a second palette block, not a rewrite. It is not
|
|
|
335
404
|
a token swap either — the saturated inks that read as vivid on near-black go
|
|
336
405
|
washed out on white and would need retuning.
|
|
337
406
|
|
|
407
|
+
When it does ship it ships as a theme, under the section below, with the same
|
|
408
|
+
1:1 obligation: light mode does not get a component the dark language lacks.
|
|
409
|
+
|
|
338
410
|
## The official accents
|
|
339
411
|
|
|
340
412
|
`--axi-accent` is the per-app theming surface, and the family now agrees on
|
|
@@ -354,12 +426,69 @@ dist/axi.css.
|
|
|
354
426
|
The default remains `#ffc53d` Axi Gold, declared in tokens.css: an app that
|
|
355
427
|
sets no `data-axi-accent` is gold, and correctly themed.
|
|
356
428
|
|
|
429
|
+
## Themes
|
|
430
|
+
|
|
431
|
+
The palette in `tokens.css` is not "the default theme". It is the language, and
|
|
432
|
+
a theme is a repaint of it. Everything rules 1-11 describe — the outline, the
|
|
433
|
+
block, what a fill means, what the cool ink is reserved for — is defined once,
|
|
434
|
+
there, and a theme inherits all of it. Anything a theme cannot say by restating
|
|
435
|
+
a token is not a theme; it is a change to the language, and it goes through the
|
|
436
|
+
rules above like any other.
|
|
437
|
+
|
|
438
|
+
A theme is a generated stylesheet, `dist/themes/<id>.css`, built the way
|
|
439
|
+
`dist/accents.css` is: `themes/<id>.json` is the source of truth and the build
|
|
440
|
+
emits one `[data-axi-theme="<id>"]` block of custom properties, no structural
|
|
441
|
+
CSS. That makes `themes/*.json` the third sanctioned home for a colour literal,
|
|
442
|
+
after `tokens.css` and `accents.json`, and for the same reason as the second —
|
|
443
|
+
it is data the build generates from, not stylesheet source. Hand-editing
|
|
444
|
+
`dist/themes/glass.css` is exactly as wrong as hand-editing `dist/axi.css`. A consumer imports it beside `axi.css` and sets
|
|
445
|
+
`data-axi-theme` on its root element. That is the whole integration — which is
|
|
446
|
+
the point. A site gets the dark language by default and a different one by
|
|
447
|
+
adding an attribute, and in neither case does it author, carry or maintain a
|
|
448
|
+
line of theme CSS of its own.
|
|
449
|
+
|
|
450
|
+
**A theme mirrors the main theme one-for-one.** In both directions:
|
|
451
|
+
|
|
452
|
+
- **Nothing added.** No token a theme invents, no selector but its own root
|
|
453
|
+
hook, and above all no component that exists only under a theme. There is no
|
|
454
|
+
`.axi-panel--glass`, no glass-only card, no variant that appears when the
|
|
455
|
+
attribute is set. A component that is worth having is worth having in the
|
|
456
|
+
language; one that only makes sense translucent is a component the language
|
|
457
|
+
does not have.
|
|
458
|
+
- **Nothing dropped.** A theme may not leave a token unset and a component
|
|
459
|
+
unpainted. Every component renders under every theme, which is what makes the
|
|
460
|
+
attribute a swap rather than a migration — a site can set it, unset it, or
|
|
461
|
+
offer the choice to its users, and no markup changes either way.
|
|
462
|
+
|
|
463
|
+
The consequence worth stating plainly: **a new theme capability costs a
|
|
464
|
+
main-theme token first.** A glass theme wants a `backdrop-filter`; it does not
|
|
465
|
+
get to introduce one. `--axi-surface-filter` is declared in `tokens.css` with an
|
|
466
|
+
inert default (`none`), the surfaces read it unconditionally, and the theme
|
|
467
|
+
restates it. The main theme is unchanged in appearance and the hook is part of
|
|
468
|
+
the language rather than part of the theme. Every theme pays this toll, and it
|
|
469
|
+
is what keeps the mirror true: a token the theme could set that the main theme
|
|
470
|
+
had never heard of is the first step back toward theme-only components.
|
|
471
|
+
|
|
472
|
+
Rules 1 and 2 name the only relief a theme gets, and it is confined to the
|
|
473
|
+
surface layer: a theme may put a gradient on a surface and may hold a surface
|
|
474
|
+
token at partial opacity. It may not mute an ink. Read those two rules for why.
|
|
475
|
+
|
|
476
|
+
**What is mechanically enforced.** `tests/themes.test.mjs` reads every
|
|
477
|
+
`dist/themes/*.css` and checks the mirror rather than trusting it: the file
|
|
478
|
+
contains exactly one rule, its selector is `[data-axi-theme="<id>"]` for the
|
|
479
|
+
`<id>` in its own filename, every declaration in it is a custom property with a
|
|
480
|
+
non-empty value, and every property it declares is already declared in
|
|
481
|
+
`tokens.css`. A theme-only component fails the first check, because drawing one
|
|
482
|
+
takes a second selector. An invented token fails the last. The suite passes
|
|
483
|
+
vacuously while no theme exists, and binds the moment the first file lands.
|
|
484
|
+
|
|
357
485
|
## Adding a component
|
|
358
486
|
|
|
359
487
|
1. Which rule justifies it? If none, write the rule first or stop.
|
|
360
488
|
2. Build it from the existing primitives. A shell that redefines `.axi-panel`
|
|
361
489
|
instead of using it will drift the first time the panel changes.
|
|
362
|
-
3. No colour literals. No third form step.
|
|
490
|
+
3. No colour literals. No third form step. No theme-only variant — see
|
|
491
|
+
[Themes](#themes).
|
|
363
492
|
4. Add it to the gallery, and check it with the accent switcher — if it does
|
|
364
493
|
not follow the accent, it hard-coded something.
|
|
365
494
|
5. `npm run build` and commit `dist/axi.css` with your source change.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axiapps/axi-design",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.0",
|
|
4
4
|
"description": "The design language for the axi suite — flat and outlined, dark, drawn in saturated ink.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -17,11 +17,12 @@
|
|
|
17
17
|
"publishConfig": {
|
|
18
18
|
"access": "public"
|
|
19
19
|
},
|
|
20
|
-
"//exports": "
|
|
20
|
+
"//exports": "Every entry point is a stylesheet. Consumers import the path rather than the package root because there is no JavaScript here to be a default export - `import '@axiapps/axi-design/axi.css'` says what it does, and a bare `import '@axiapps/axi-design'` resolving to a stylesheet would not. ./tokens.css is the palette without the components, for an app that already draws its own components through its own variables and wants to point them at ours: it is the whole language for a consumer like that, and copying the token block by hand is the one way those values are guaranteed to drift. ./themes/*.css is a subpath pattern rather than one entry per theme, so adding a theme is adding themes/<id>.json and nothing else - an entry that has to be remembered is an entry that will be forgotten, and the symptom would be an import that resolves in this repo and not from an install.",
|
|
21
21
|
"exports": {
|
|
22
22
|
"./axi.css": "./dist/axi.css",
|
|
23
23
|
"./tokens.css": "./src/tokens.css",
|
|
24
24
|
"./accents.css": "./dist/accents.css",
|
|
25
|
+
"./themes/*.css": "./dist/themes/*.css",
|
|
25
26
|
"./accents.json": "./accents.json",
|
|
26
27
|
"./package.json": "./package.json"
|
|
27
28
|
},
|
|
@@ -29,19 +30,22 @@
|
|
|
29
30
|
"sideEffects": [
|
|
30
31
|
"*.css"
|
|
31
32
|
],
|
|
32
|
-
"//files": "dist/ is the artifact; src/
|
|
33
|
+
"//files": "dist/ is the artifact; src/ rides along because a consumer reading a component wants the source next to them; docs/RULES.md rides along because it is the reason any of it is shaped the way it is. The rest of docs/ - the manifest, the generator and the guides - builds the site and is no use to a consumer. No tests, no gallery. LICENSE and README are included by npm regardless of this list.",
|
|
33
34
|
"files": [
|
|
34
35
|
"dist",
|
|
35
36
|
"src",
|
|
36
|
-
"docs",
|
|
37
|
+
"docs/RULES.md",
|
|
37
38
|
"accents.json",
|
|
38
39
|
"README.md"
|
|
39
40
|
],
|
|
40
41
|
"scripts": {
|
|
41
42
|
"build": "node scripts/build.mjs",
|
|
43
|
+
"docs": "node scripts/site.mjs",
|
|
44
|
+
"serve": "node scripts/serve.mjs",
|
|
42
45
|
"test": "vitest run"
|
|
43
46
|
},
|
|
44
47
|
"devDependencies": {
|
|
48
|
+
"marked": "^18.0.14",
|
|
45
49
|
"vitest": "^2.1.0"
|
|
46
50
|
}
|
|
47
51
|
}
|
package/src/feedback.css
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/* axi design language - feedback.
|
|
2
|
+
Things that report on work, and things that report on how work went. Rule
|
|
3
|
+
11 governs the first kind and is the one rule in this language described as
|
|
4
|
+
non-negotiable: an indicator of work may animate only `transform` and
|
|
5
|
+
`opacity`, because those are the two the compositor runs off the main
|
|
6
|
+
thread. A spinner animated by layout freezes with the work it reports on,
|
|
7
|
+
and a frozen spinner tells the reader the app crashed at the exact moment
|
|
8
|
+
it was working hardest. tests/motion.test.mjs enforces it. */
|
|
9
|
+
|
|
10
|
+
/* ---------- spinner ---------- */
|
|
11
|
+
/* The family motif, turning. A square rotated 45deg is the diamond, so the
|
|
12
|
+
loop runs 45deg -> 405deg: one full turn that begins and ends on the
|
|
13
|
+
motif rather than on a square.
|
|
14
|
+
That start and end also make it correct under reduced motion for free.
|
|
15
|
+
base.css only shortens the animation to one .01ms iteration; it never sets
|
|
16
|
+
animation-fill-mode, so once that instant run finishes the element reverts
|
|
17
|
+
to its specified transform: rotate(45deg) - the motif at rest, and visually
|
|
18
|
+
identical to where the loop begins. */
|
|
19
|
+
@keyframes axi-spin { from { transform: rotate(45deg); } to { transform: rotate(405deg); } }
|
|
20
|
+
.axi-spinner {
|
|
21
|
+
display: inline-block;
|
|
22
|
+
flex: none;
|
|
23
|
+
width: var(--axi-spinner-size, 20px);
|
|
24
|
+
height: var(--axi-spinner-size, 20px);
|
|
25
|
+
border: var(--axi-border-control) solid var(--axi-accent);
|
|
26
|
+
border-radius: var(--axi-radius-sm);
|
|
27
|
+
transform: rotate(45deg);
|
|
28
|
+
animation: axi-spin 1.1s linear infinite;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/* ---------- indeterminate meter ---------- */
|
|
32
|
+
/* The .axi-meter that does not know how far along it is. The fill keeps a
|
|
33
|
+
fixed width and travels; `width` is never animated, which is the whole of
|
|
34
|
+
rule 11. --axi-meter-v is ignored in this mode - there is no value to
|
|
35
|
+
express, which is what indeterminate means. */
|
|
36
|
+
@keyframes axi-meter-busy {
|
|
37
|
+
from { transform: translateX(-100%); }
|
|
38
|
+
to { transform: translateX(400%); }
|
|
39
|
+
}
|
|
40
|
+
.axi-meter--busy .axi-meter__fill {
|
|
41
|
+
width: 25%;
|
|
42
|
+
animation: axi-meter-busy 1.3s ease-in-out infinite;
|
|
43
|
+
}
|
|
44
|
+
/* base.css only ever sets animation-duration and animation-iteration-count
|
|
45
|
+
under reduced motion; it never touches animation-fill-mode. The shorthand
|
|
46
|
+
above resets animation-fill-mode to its default, none, so once the .01ms
|
|
47
|
+
run finishes the element reverts to its specified style rather than
|
|
48
|
+
holding the keyframe it ended on - here that is transform: none and the
|
|
49
|
+
fill's own width: 25%, a left-anchored quarter-width bar. That is already
|
|
50
|
+
the right thing to show: visible, and unmistakably not finished. This rule
|
|
51
|
+
states transform: none explicitly rather than relying on the revert, so
|
|
52
|
+
the "at rest" case is not accidental. */
|
|
53
|
+
@media (prefers-reduced-motion: reduce) {
|
|
54
|
+
.axi-meter--busy .axi-meter__fill { transform: none; }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* ---------- notice status ---------- */
|
|
58
|
+
/* Rule 5: the icon is the part of a notice that asserts something, so the
|
|
59
|
+
status lives there. The body text stays in the reading ink at every status
|
|
60
|
+
- a whole paragraph in the danger ink is the tinted-everything failure rule
|
|
61
|
+
2 exists to prevent, and it is unreadable besides. */
|
|
62
|
+
.axi-notice--ok .axi-notice__icon { background: var(--axi-ok); }
|
|
63
|
+
.axi-notice--warn .axi-notice__icon { background: var(--axi-warn); }
|
|
64
|
+
.axi-notice--danger .axi-notice__icon { background: var(--axi-danger); }
|
|
65
|
+
.axi-notice--ok b { color: var(--axi-ok); }
|
|
66
|
+
.axi-notice--warn b { color: var(--axi-warn); }
|
|
67
|
+
.axi-notice--danger b { color: var(--axi-danger); }
|
package/src/forms.css
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/* axi design language - forms.
|
|
2
|
+
Real inputs, styled. Every control here is an actual <input> or <textarea>
|
|
3
|
+
with `appearance: none`, never a <div> wearing a class: a checkbox that is
|
|
4
|
+
not an <input> does not submit with its form, does not toggle from the
|
|
5
|
+
keyboard, and is not announced as a checkbox. The language styles controls;
|
|
6
|
+
it does not reimplement them.
|
|
7
|
+
|
|
8
|
+
Rule 3's flat case governs the geometry: a control inside a panel is
|
|
9
|
+
outlined and carries no block, exactly as .axi-input and .axi-switch
|
|
10
|
+
already are. Rule 5 governs the state: the fill is what says "on".
|
|
11
|
+
|
|
12
|
+
The focus ring is deliberately absent from this file. base.css draws one
|
|
13
|
+
page-wide on :focus-visible, and appearance: none does not remove it - it
|
|
14
|
+
removes the browser's own. Suppressing the outline anywhere here would make
|
|
15
|
+
every control in it unusable by keyboard. */
|
|
16
|
+
|
|
17
|
+
/* ---------- checkbox and radio ---------- */
|
|
18
|
+
/* One box, two marks. --axi-radius is 0, so both are square and a round radio
|
|
19
|
+
would be the only rounded shape in the language - the first place a flat
|
|
20
|
+
outlined form starts looking like some other framework's. So they are told
|
|
21
|
+
apart by their mark instead: the checkbox gets a check, the radio gets the
|
|
22
|
+
family diamond (rule 7).
|
|
23
|
+
|
|
24
|
+
Both read the same two knobs on purpose. A form holding checkboxes and
|
|
25
|
+
radios together should not need two properties set to the same value to
|
|
26
|
+
keep its controls the same size. */
|
|
27
|
+
.axi-check,
|
|
28
|
+
.axi-radio {
|
|
29
|
+
appearance: none;
|
|
30
|
+
-webkit-appearance: none;
|
|
31
|
+
margin: 0;
|
|
32
|
+
flex: none;
|
|
33
|
+
width: var(--axi-check-size, 22px);
|
|
34
|
+
height: var(--axi-check-size, 22px);
|
|
35
|
+
display: inline-grid;
|
|
36
|
+
place-items: center;
|
|
37
|
+
background: var(--axi-ground);
|
|
38
|
+
border: var(--axi-border-control) solid var(--axi-ink-line);
|
|
39
|
+
border-radius: var(--axi-radius-sm);
|
|
40
|
+
cursor: pointer;
|
|
41
|
+
}
|
|
42
|
+
/* The mark is always in the DOM and revealed with opacity rather than being
|
|
43
|
+
created on :checked, so the box never changes size as it toggles. */
|
|
44
|
+
.axi-check::after {
|
|
45
|
+
content: "";
|
|
46
|
+
width: 55%;
|
|
47
|
+
height: 30%;
|
|
48
|
+
opacity: 0;
|
|
49
|
+
border-left: var(--axi-border-control) solid var(--axi-accent-ink);
|
|
50
|
+
border-bottom: var(--axi-border-control) solid var(--axi-accent-ink);
|
|
51
|
+
transform: translateY(-12%) rotate(-45deg);
|
|
52
|
+
}
|
|
53
|
+
.axi-check:checked { background: var(--axi-check-fill, var(--axi-accent)); }
|
|
54
|
+
.axi-check:checked::after { opacity: 1; }
|
|
55
|
+
/* The radio's mark is the diamond itself, so it is drawn in the fill rather
|
|
56
|
+
than on it - there is no filled box underneath it to sit on. It takes the
|
|
57
|
+
hairline outline rule 7's diamond wears at this scale (see the prose
|
|
58
|
+
bullet, src/prose.css, the other place a diamond this small is drawn) -
|
|
59
|
+
the mark is revealed with opacity, so the hairline is hidden along with
|
|
60
|
+
the fill in the unchecked state and never leaks out on its own. */
|
|
61
|
+
.axi-radio::after {
|
|
62
|
+
content: "";
|
|
63
|
+
width: 46%;
|
|
64
|
+
height: 46%;
|
|
65
|
+
opacity: 0;
|
|
66
|
+
background: var(--axi-check-fill, var(--axi-accent));
|
|
67
|
+
border: var(--axi-border-hairline) solid var(--axi-ink-line);
|
|
68
|
+
transform: rotate(45deg);
|
|
69
|
+
}
|
|
70
|
+
.axi-radio:checked::after { opacity: 1; }
|
|
71
|
+
.axi-check:disabled,
|
|
72
|
+
.axi-radio:disabled { cursor: not-allowed; opacity: .5; }
|
|
73
|
+
|
|
74
|
+
/* ---------- textarea ---------- */
|
|
75
|
+
/* Not a component: the same .axi-input a single-line field uses, with the two
|
|
76
|
+
things a <textarea> needs that an <input> does not. The font is restated
|
|
77
|
+
because a textarea does not inherit the page font in any browser, and a
|
|
78
|
+
form whose notes field is monospace while its name field is not is a bug
|
|
79
|
+
nobody files and everybody sees.
|
|
80
|
+
|
|
81
|
+
Resizing is vertical only. A textarea dragged wider than its field breaks
|
|
82
|
+
the column its form is laid out in, and the reader who did it has no way
|
|
83
|
+
to discover that the layout is not at fault. */
|
|
84
|
+
textarea.axi-input {
|
|
85
|
+
min-height: var(--axi-textarea-h, 90px);
|
|
86
|
+
resize: vertical;
|
|
87
|
+
font-family: var(--axi-sans);
|
|
88
|
+
line-height: 1.5;
|
|
89
|
+
}
|
package/src/primitives.css
CHANGED
|
@@ -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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
173
|
-
.axi-chip--warn { background: var(--axi-warn); color: var(--axi-ink-
|
|
174
|
-
.axi-chip--danger { background: var(--axi-danger); color: var(--axi-ink-
|
|
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;
|
|
@@ -267,7 +277,7 @@
|
|
|
267
277
|
.axi-picker__btn:hover,
|
|
268
278
|
.axi-picker__btn[aria-expanded='true'] {
|
|
269
279
|
color: var(--axi-text);
|
|
270
|
-
box-shadow: var(--axi-
|
|
280
|
+
box-shadow: var(--axi-shadow-control);
|
|
271
281
|
transform: translate(-2px, -2px);
|
|
272
282
|
}
|
|
273
283
|
|
|
@@ -288,7 +298,8 @@
|
|
|
288
298
|
border: var(--axi-border-panel) solid var(--axi-ink-line);
|
|
289
299
|
border-radius: var(--axi-radius);
|
|
290
300
|
background: var(--axi-surface-raised);
|
|
291
|
-
|
|
301
|
+
backdrop-filter: var(--axi-surface-filter);
|
|
302
|
+
box-shadow: var(--axi-shadow-panel);
|
|
292
303
|
}
|
|
293
304
|
.axi-select option {
|
|
294
305
|
display: flex; align-items: center; gap: 9px;
|
|
@@ -359,12 +370,13 @@
|
|
|
359
370
|
max-height: 340px; overflow-y: auto;
|
|
360
371
|
padding: 6px;
|
|
361
372
|
background: var(--axi-surface-raised);
|
|
373
|
+
backdrop-filter: var(--axi-surface-filter);
|
|
362
374
|
border: var(--axi-border-panel) solid var(--axi-ink-line);
|
|
363
375
|
border-radius: var(--axi-radius);
|
|
364
376
|
/* The block falls outside the popover's own box. An ancestor that clips -
|
|
365
377
|
a scrolling pane, a panel with overflow: hidden - eats it, and the only
|
|
366
378
|
shadow on the screen is the one that goes missing. */
|
|
367
|
-
box-shadow: var(--axi-
|
|
379
|
+
box-shadow: var(--axi-shadow-panel);
|
|
368
380
|
}
|
|
369
381
|
.axi-picker__pop[hidden] { display: none; }
|
|
370
382
|
/* A popover cannot always live beside its trigger. Inside a pane that scrolls
|
|
@@ -405,3 +417,31 @@
|
|
|
405
417
|
.axi-picker__opt:focus-visible { outline-offset: -3px; }
|
|
406
418
|
.axi-picker__opt[aria-selected='true'] { color: var(--axi-text); }
|
|
407
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-
|
|
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
|
|