@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.
- package/README.md +48 -10
- package/dist/axi.css +559 -30
- package/dist/themes/glass.css +11 -0
- package/docs/RULES.md +157 -17
- package/package.json +8 -4
- package/src/base.css +2 -0
- package/src/feedback.css +67 -0
- package/src/forms.css +89 -0
- package/src/primitives.css +162 -16
- 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
|
|
@@ -111,6 +143,17 @@ read `--axi-radius`, everything control-sized reads `--axi-radius-sm`, and
|
|
|
111
143
|
`tests/tokens.test.mjs` fails on a literal radius in a component file the same
|
|
112
144
|
way it fails on a literal border weight.
|
|
113
145
|
|
|
146
|
+
**A box the OS draws is a box that breaks this.** A native `<select>` popup is
|
|
147
|
+
a raised list the language cannot reach: no ink outline, no offset block, and
|
|
148
|
+
its own selection colour where the accent belongs. `.axi-select` styles the
|
|
149
|
+
closed box and hands the list to `appearance: base-select` where the browser
|
|
150
|
+
has it — but most do not yet, and an Electron app is pinned to whatever
|
|
151
|
+
Chromium its version shipped. `.axi-picker` is the way out: the same closed
|
|
152
|
+
box on a button, and the list drawn as a popover that takes the panel weight
|
|
153
|
+
like any other raised surface. Reach for the native select first, because it
|
|
154
|
+
brings keyboard handling and a popup that can leave the window; reach for the
|
|
155
|
+
picker when the popup it opens is not one this rule can touch.
|
|
156
|
+
|
|
114
157
|
## 4. Hover lifts
|
|
115
158
|
|
|
116
159
|
The lift is per form step, not one universal number: a control has no resting
|
|
@@ -162,8 +205,12 @@ than framing it down the left edge. A full-height stripe runs the height of the
|
|
|
162
205
|
box, so it reads as the box's border: five cards in a row become five coloured
|
|
163
206
|
frames, and the colour stops saying anything about any one number. A cap sits
|
|
164
207
|
directly over the reading it is a verdict on, identifies it once, and then gets
|
|
165
|
-
out of the way. Under rule 3 the cap
|
|
166
|
-
|
|
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.
|
|
167
214
|
|
|
168
215
|
A switch is the same rule in a slot. Its track fills to assert the setting's
|
|
169
216
|
status and is empty otherwise; the slug that moves is `--axi-ink-line` in both
|
|
@@ -225,6 +272,14 @@ Three bounds, and the rule is only sound with all three:
|
|
|
225
272
|
carries a 6px block. A second 6px block nested in the first reads as two
|
|
226
273
|
planes arguing; the 3px control step reads as the contents of a box. This is
|
|
227
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.
|
|
228
283
|
- **The outline stays `--axi-ink-line`.** Status goes on a filled shape inside
|
|
229
284
|
the row — the icon tile, a chip — and never on the row's own edge. Colouring
|
|
230
285
|
the edge is exactly the full-height stripe rule 5 rejects: five states become
|
|
@@ -317,6 +372,31 @@ If an app picks an accent dark enough that near-black text on it fails
|
|
|
317
372
|
contrast, it also sets `--axi-accent-ink: var(--axi-text)`. It should not edit
|
|
318
373
|
components.
|
|
319
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
|
+
|
|
320
400
|
## Light mode
|
|
321
401
|
|
|
322
402
|
Not shipped. The system is *structured* for it: no component contains a colour
|
|
@@ -324,6 +404,9 @@ literal, so a light theme is a second palette block, not a rewrite. It is not
|
|
|
324
404
|
a token swap either — the saturated inks that read as vivid on near-black go
|
|
325
405
|
washed out on white and would need retuning.
|
|
326
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
|
+
|
|
327
410
|
## The official accents
|
|
328
411
|
|
|
329
412
|
`--axi-accent` is the per-app theming surface, and the family now agrees on
|
|
@@ -343,12 +426,69 @@ dist/axi.css.
|
|
|
343
426
|
The default remains `#ffc53d` Axi Gold, declared in tokens.css: an app that
|
|
344
427
|
sets no `data-axi-accent` is gold, and correctly themed.
|
|
345
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
|
+
|
|
346
485
|
## Adding a component
|
|
347
486
|
|
|
348
487
|
1. Which rule justifies it? If none, write the rule first or stop.
|
|
349
488
|
2. Build it from the existing primitives. A shell that redefines `.axi-panel`
|
|
350
489
|
instead of using it will drift the first time the panel changes.
|
|
351
|
-
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).
|
|
352
492
|
4. Add it to the gallery, and check it with the accent switcher — if it does
|
|
353
493
|
not follow the accent, it hard-coded something.
|
|
354
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/base.css
CHANGED
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
|
+
}
|