@axiapps/axi-design 1.6.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/docs/RULES.md ADDED
@@ -0,0 +1,275 @@
1
+ # The axi design language
2
+
3
+ Flat and outlined. Every fill is a saturated ink at full strength, every raised
4
+ element is drawn with a near-black outline and a hard offset block instead of a
5
+ blur.
6
+
7
+ These are the rules. A component that cannot be justified by one of them either
8
+ needs a new rule written for it, or does not belong in the system.
9
+
10
+ ## 1. No gradients on surfaces
11
+
12
+ Flat fills only. The two exceptions in the codebase are both a gradient used
13
+ to draw a *shape*, with no soft transition anywhere in them: the select caret
14
+ (two `linear-gradient`s meeting to make a triangle) and `.axi-plot`'s
15
+ gridlines (a `repeating-linear-gradient` of hard stops, which is how N evenly
16
+ spaced rules get drawn without asking every consumer to emit N empty divs).
17
+ A gradient across a surface is still forbidden, and always will be.
18
+
19
+ ## 2. No colour at partial opacity over the ground
20
+
21
+ If a colour is present it is at full strength. A muted gold over near-black is
22
+ just brown, and five muted inks over near-black are five browns. When something
23
+ should be quieter, reach for a neutral from the ramp — that is what the ramp is
24
+ for.
25
+
26
+ ## 3. Every raised element is outlined and blocked
27
+
28
+ An `--axi-ink-line` border plus a hard offset shadow, never a blur.
29
+
30
+ Two weight steps, and only two:
31
+
32
+ | Step | Border | Offset |
33
+ |---|---|---|
34
+ | Panel | `--axi-border-panel` (4px) | `--axi-offset-panel` (6px) |
35
+ | Control | `--axi-border-control` (3px) | `--axi-offset-control` (3px) |
36
+
37
+ A third step is how a system stops looking like one system.
38
+
39
+ There is one weight outside the table, and it is deliberately not a step:
40
+ `--axi-border-hairline` (2px), used only inside `.axi-prose` — for inline
41
+ code, table rules and the list bullet — where either form step reads as too
42
+ heavy for a line of running text. It is a prose rule weight, never an outline
43
+ on a raised thing.
44
+
45
+ **What is mechanically enforced.** `tests/tokens.test.mjs` enforces both
46
+ columns:
47
+
48
+ - *Border* — no literal border/outline weight may appear in any component
49
+ file: not a `px` value, not another length unit (`rem`, `em`, ...), and not
50
+ a `thin`/`medium`/`thick` keyword. A width has to come through
51
+ `--axi-border-panel`, `--axi-border-control` or `--axi-border-hairline`.
52
+ `outline`/`outline-width` are checked the same way as `border`
53
+ (`outline-offset` and `outline-color` are not weight properties and are
54
+ untouched).
55
+ - *Offset* — every `box-shadow` in a component file must be exactly
56
+ `<offset> <offset> 0 var(--axi-ink-line)`, with the offset drawn from an
57
+ enumerated list of four tokens: the two resting steps above, plus the two
58
+ hover deepenings rule 4 describes (`--axi-offset-panel-hover` 10px,
59
+ `--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
+ `filter: drop-shadow(...)` and `text-shadow` — the two other CSS properties
62
+ that can draw the same blurred look — are forbidden outright, since nothing
63
+ in this language legitimately reaches for either.
64
+ - *No local escape hatch* — the form tokens themselves
65
+ (`--axi-border-panel`, `--axi-border-control`, `--axi-border-hairline`,
66
+ `--axi-offset-panel`, `--axi-offset-control`, and their `-hover` variants)
67
+ may be **declared** only in `tokens.css`. A component file redeclaring one
68
+ of these on itself would change the value the border/offset checks above
69
+ are silently trusting, without changing the `var()` text those checks read
70
+ — that is the escape hatch, and it is what the mechanical checks above
71
+ cannot see on their own, so it is checked directly instead.
72
+
73
+ Adding a fifth legal block means adding a token *and* adding it to `OFFSETS`
74
+ in the test — there is no escape hatch that admits a bare literal, local
75
+ redeclaration included.
76
+
77
+ **Colour literal scan, precisely.** The colour check in the same file only
78
+ scans the *value* of a declaration whose property can legally carry a colour
79
+ (an allowlist: `color`, `background`/`background-image`, the `border*-color`
80
+ family, `outline-color`, `box-shadow`, `text-shadow`, `filter`, `fill`,
81
+ `stroke`, `caret-color`, `column-rule-color`, `text-decoration-color`,
82
+ `accent-color`, `scrollbar-color`). A selector (`.card:not(.plum)`) or an
83
+ at-rule prelude (`@supports (color: ...)`) never reaches the scan at all,
84
+ because neither one's text starts with a colour-carrying property name — this
85
+ is why a pseudo-class's colon is harmless. A bare named colour (`gold`,
86
+ `tan`, `linen`, ...) is only trusted inside the plain value: never inside a
87
+ quoted string or a `url(...)`, since those routinely contain a colour *word*
88
+ with no colour *meaning* (a font stack, a `content` string, a texture
89
+ filename, a cursor list). Hex and the colour functions (`#fff`, `oklch(...)`,
90
+ ...) have no other meaning in CSS, so they are still caught even inside a
91
+ string or `url(...)` — this is what catches a colour hard-coded into a
92
+ data-URI SVG, which would otherwise be a free pass for exactly the same
93
+ reason the string/url() exemption exists for named colours. This is a
94
+ deliberate asymmetry: it costs the (rare, deliberate) case of a bare named
95
+ colour smuggled inside a data-URI SVG, in exchange for never blocking a font
96
+ stack, a filename or a `content` string again.
97
+
98
+ Radii are the one part of the form that is **not** enforced: `--axi-radius`
99
+ and `--axi-radius-sm` exist, but controls carry a literal `8px` (and `9px`,
100
+ `5px`, `4px` appear elsewhere). Treat the radius scale as convention, not
101
+ contract, until it is tokenised.
102
+
103
+ ## 4. Hover lifts
104
+
105
+ The lift is per form step, not one universal number: a control has no resting
106
+ shadow, so a control's hover both moves it and draws its block for the first
107
+ time — `translate(-2px, -2px)` together with gaining the `--axi-offset-control`
108
+ (3px) block from nothing. A panel already carries its 6px block at rest, so its
109
+ hover only needs to deepen it — `translate(-3px, -3px)` with the block growing
110
+ from 6px to 10px (`--axi-offset-panel-hover`). Applying the panel's flat `-3px` to a control would lift it
111
+ by exactly the depth of its own 3px block, leaving the lower-right edge where
112
+ it started — that reads as the element growing, not lifting.
113
+
114
+ There is a third case the two-step framing misses: a *control that already
115
+ rests on a block* — `.axi-btn--primary`, a pressed `.axi-pill`. Translating it
116
+ without deepening its block moves element and block together and leaves the
117
+ lower-right edge exactly where it was, which is the same "grows rather than
118
+ lifts" failure. Those deepen 3px to 6px (`--axi-offset-control-hover`) while
119
+ keeping the control's `translate(-2px, -2px)`.
120
+
121
+ Nothing in this language fades, glows or pulses. The movement reads in
122
+ peripheral vision and costs no colour.
123
+
124
+ Every lift is turned off under `@media (prefers-reduced-motion: reduce)`, in
125
+ `base.css`, once, for every consumer. Resting appearance is untouched: the
126
+ diamond still rotates, because a rotation that never changes is geometry and
127
+ not motion.
128
+
129
+ ## 5. Filled means status, outlined means annotation
130
+
131
+ A filled chip asserts a value about the thing. An outlined chip in the cool ink
132
+ is commentary *about* the thing — a maintainer's judgment, a source, a caveat.
133
+ A reader must be able to tell which they are looking at before reading either.
134
+
135
+ The same rule governs coloured strips on cards: a strip must encode real data.
136
+ A strip that carries "category" is decoration impersonating data, and it takes
137
+ the first position the eye lands on.
138
+
139
+ And where the strip goes is part of the rule. Status colour **caps** the thing
140
+ it judges — a short bar across the head of the card, above the value — rather
141
+ than framing it down the left edge. A full-height stripe runs the height of the
142
+ box, so it reads as the box's border: five cards in a row become five coloured
143
+ frames, and the colour stops saying anything about any one number. A cap sits
144
+ directly over the reading it is a verdict on, identifies it once, and then gets
145
+ out of the way. Under rule 3 the cap is drawn at the panel weight; the card
146
+ itself keeps its plain ink outline at the control weight.
147
+
148
+ A switch is the same rule in a slot. Its track fills to assert the setting's
149
+ status and is empty otherwise; the slug that moves is `--axi-ink-line` in both
150
+ states, so on and off differ in what colour is *in* the slot and never in how
151
+ bright the moving part is. It carries no block — a block belongs to things you
152
+ press, and a switch is a slot with something sitting in it — but it keeps a
153
+ full ink edge at the control weight, because an off switch inside a panel is a
154
+ surface on a surface and without the edge the track disappears and all you can
155
+ see is a slug floating in the card.
156
+
157
+ ## 6. One cool ink is reserved for meta
158
+
159
+ `--axi-meta` marks metadata and annotation, and may never carry a status
160
+ meaning. It is the only ink guaranteed not to mean "how bad is this" — which is
161
+ what makes it readable as commentary at a glance.
162
+
163
+ ## 7. The diamond is the family motif
164
+
165
+ A 45°-rotated outlined square. Bullet, status dot, language marker, and scaled
166
+ up behind a glyph, the brand sigil.
167
+
168
+ ## 8. A table is the panel's interior
169
+
170
+ Forty rows of numbers are not forty raised things. A table is drawn in rules —
171
+ `--axi-rule` for the row lines, `--axi-border-hairline` for their weight — and
172
+ never in outlines or blocks: the panel around it is the raised element, and the
173
+ rows are what is inside it. Outlining the rows turns a list into a grid of
174
+ boxes and costs the eye the vertical run down a column that makes a table worth
175
+ using.
176
+
177
+ One fill is allowed, in the rank column, and only where the rank is real — a
178
+ podium position the data earned. A row *number* is not a rank, and filling it
179
+ spends the brightest thing on screen on the fact that a list has a first line.
180
+
181
+ The hover on a row is the neutral ramp, not an ink, for the same reason: moving
182
+ the cursor down a table is not a series of status changes.
183
+
184
+ ## 9. A quantity is drawn as length, never intensity
185
+
186
+ A proportion is a bar: the track is the ground, the fill is the value, and the
187
+ fill is one ink at full strength. This is rule 2 applied to data — a bar faded
188
+ to 30% to mean "30%" encodes the number twice, once legibly and once not, and
189
+ the illegible copy is the one the eye reads first.
190
+
191
+ The corollary is that this language does not draw a heatmap. Intensity-by-tint
192
+ is the one chart type that cannot be built without the thing rule 2 forbids, so
193
+ a distribution is drawn as bars, or as a table sorted by the value, or not at
194
+ all.
195
+
196
+ ## 10. A chart's ink is the accent
197
+
198
+ One series is the accent. A second, for comparison, is the neutral ramp —
199
+ `--axi-text-faint` against the accent reads instantly as "this one, versus
200
+ that one", and costs no new colour.
201
+
202
+ Beyond two, stop and ask whether the data owns its own palette. A profession,
203
+ a team, a map colour is domain data: it comes in per-instance through
204
+ `--axi-series`, the way a card strip does, and it is the data's colour rather
205
+ than the system's. If the data does *not* own a palette, a nine-colour chart is
206
+ nine arbitrary inks competing with the five that already mean something —
207
+ `--axi-ok`, `--axi-warn`, `--axi-danger`, `--axi-meta` and the accent keep
208
+ their meanings inside a chart, so nothing else may borrow them for a category.
209
+
210
+ The status inks still mean status inside a plot: a line drawn in `--axi-danger`
211
+ is asserting that the quantity is bad, not that it is the third series.
212
+
213
+ ## 11. An indicator of work animates a composited property
214
+
215
+ Spinners, progress strips and pulses almost always report on something
216
+ expensive — a parse, a build, an upload. If the work blocks the main thread,
217
+ anything animated by layout or paint freezes with it, and a frozen spinner is
218
+ worse than no spinner: it is the app telling the reader it has crashed at the
219
+ exact moment it is working hardest.
220
+
221
+ So an indicator that reports on work may animate only `transform` and
222
+ `opacity`, which the compositor runs off the main thread. No animated `width`,
223
+ `left`, `background-position` or `background-color`. This is the one rule here
224
+ that is about honesty rather than composition, and it is not negotiable for
225
+ anything that claims to show liveness.
226
+
227
+ Motion elsewhere is still rationed by rule 4.
228
+
229
+ ## Tokens
230
+
231
+ Three layers, in `src/tokens.css` — the only file permitted to contain a colour
232
+ literal.
233
+
234
+ - **Surface & text** — `--axi-ground`, `--axi-surface`, `--axi-surface-raised`,
235
+ `--axi-ink-line`, `--axi-rule`, `--axi-text`, `--axi-text-dim`,
236
+ `--axi-text-faint`, `--axi-scrim`
237
+ - **Accent & status** — `--axi-accent`, `--axi-accent-ink`, `--axi-meta`,
238
+ `--axi-ok`, `--axi-warn`, `--axi-danger`. **This is the per-app override
239
+ surface.** An app that sets `--axi-accent` and nothing else is correctly
240
+ themed.
241
+ - **Form** — outline and offset steps, radii, measures (`--axi-page`,
242
+ `--axi-page-narrow`, `--axi-page-wide`, `--axi-gutter`) and the type scale.
243
+ Overriding these means leaving the language, not theming it.
244
+
245
+ Per-instance knobs (`--axi-pill-fill`, `--axi-grid-min`, `--axi-page-pad`, …)
246
+ are a separate surface from these tokens: they are set on one element, or on
247
+ an ancestor, with a `style=""` attribute rather than in `:root`. The full list
248
+ is [the consumer API table in the README](../README.md#per-instance-knobs).
249
+
250
+ ### Theming an app
251
+
252
+ ```css
253
+ :root { --axi-accent: #b06bff; }
254
+ ```
255
+
256
+ If an app picks an accent dark enough that near-black text on it fails
257
+ contrast, it also sets `--axi-accent-ink: var(--axi-text)`. It should not edit
258
+ components.
259
+
260
+ ## Light mode
261
+
262
+ Not shipped. The system is *structured* for it: no component contains a colour
263
+ literal, so a light theme is a second palette block, not a rewrite. It is not
264
+ a token swap either — the saturated inks that read as vivid on near-black go
265
+ washed out on white and would need retuning.
266
+
267
+ ## Adding a component
268
+
269
+ 1. Which rule justifies it? If none, write the rule first or stop.
270
+ 2. Build it from the existing primitives. A shell that redefines `.axi-panel`
271
+ instead of using it will drift the first time the panel changes.
272
+ 3. No colour literals. No third form step.
273
+ 4. Add it to the gallery, and check it with the accent switcher — if it does
274
+ not follow the accent, it hard-coded something.
275
+ 5. `npm run build` and commit `dist/axi.css` with your source change.
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@axiapps/axi-design",
3
+ "version": "1.6.0",
4
+ "description": "The design language for the axi suite — flat and outlined, dark, drawn in saturated ink.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "darkharasho",
8
+ "repository": { "type": "git", "url": "git+https://github.com/darkharasho/axi-design.git" },
9
+ "homepage": "https://darkharasho.github.io/axi-design/",
10
+ "engines": { "node": ">=22" },
11
+ "//publishConfig": "A scoped package defaults to a restricted publish, and a restricted design language is no use to the apps that consume it. Pinned here rather than passed as --access public on the command line, so it cannot be forgotten on a later release.",
12
+ "publishConfig": { "access": "public" },
13
+ "//exports": "Two entry points, and both are stylesheets. 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.",
14
+ "exports": {
15
+ "./axi.css": "./dist/axi.css",
16
+ "./tokens.css": "./src/tokens.css",
17
+ "./package.json": "./package.json"
18
+ },
19
+ "//sideEffects": "A stylesheet is nothing but a side effect. Without this a bundler treating the import as dead code would drop the whole design language from a production build.",
20
+ "sideEffects": [
21
+ "*.css"
22
+ ],
23
+ "//files": "dist/ is the artifact; src/ and docs/ ride along because RULES.md is the reason any of it is shaped the way it is, and a consumer reading a component wants it next to them. No tests, no gallery. LICENSE and README are included by npm regardless of this list.",
24
+ "files": [
25
+ "dist",
26
+ "src",
27
+ "docs",
28
+ "README.md"
29
+ ],
30
+ "scripts": {
31
+ "build": "node scripts/build.mjs",
32
+ "test": "vitest run"
33
+ },
34
+ "devDependencies": { "vitest": "^2.1.0" }
35
+ }
package/src/base.css ADDED
@@ -0,0 +1,66 @@
1
+ /* axi design language - base.
2
+ Element-level defaults every axi property inherits. Nothing here is a
3
+ component; if it needs a class, it belongs in a later layer. */
4
+
5
+ *, *::before, *::after { box-sizing: border-box; }
6
+
7
+ /* Dark is the only theme shipped today. Declaring the scheme means form
8
+ controls, scrollbars and the like come up dark from the first paint rather
9
+ than flashing light, and it is the one line a light theme will flip. */
10
+ html { color-scheme: dark; }
11
+
12
+ body {
13
+ margin: 0;
14
+ background: var(--axi-ground);
15
+ color: var(--axi-text);
16
+ font: var(--axi-t-body);
17
+ -webkit-font-smoothing: antialiased;
18
+ }
19
+
20
+ /* Links inherit their colour by default: in this language a link is usually
21
+ inside something that has already chosen an ink, and a globally accented
22
+ link would fight every card title and nav item. Components opt into the
23
+ accent where a link should read as one. */
24
+ a { color: inherit; }
25
+
26
+ /* The focus ring is accent-coloured and thick enough to read against a
27
+ near-black outline, which a 1px ring does not. It is drawn entirely with
28
+ `outline`, which follows whatever radius the element already has: a
29
+ `border-radius` here would reshape the focused element itself, which was
30
+ harmless for our components only because each one's own radius happens to
31
+ land later in the build ORDER, and visibly wrong for an unclassed
32
+ checkbox or link. */
33
+ :focus-visible {
34
+ outline: var(--axi-border-control) solid var(--axi-accent);
35
+ outline-offset: 2px;
36
+ }
37
+
38
+ .axi-sr-only {
39
+ position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
40
+ overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
41
+ }
42
+
43
+ /* Everything this language animates is the hover lift, and the lift is the
44
+ one thing a reader who has asked for less motion is asking not to see.
45
+ Neutralise the movement, not the appearance: the resting transforms - the
46
+ diamond's rotation, the sigil, the search glyph's centring - are geometry
47
+ rather than motion and are deliberately untouched, so nothing looks
48
+ different until you point at it. The !important is what lets this sit in
49
+ the base layer and still outrank component hover rules that come later in
50
+ the build ORDER at equal specificity. */
51
+ @media (prefers-reduced-motion: reduce) {
52
+ *, *::before, *::after {
53
+ transition-duration: .01ms !important;
54
+ animation-duration: .01ms !important;
55
+ animation-iteration-count: 1 !important;
56
+ scroll-behavior: auto !important;
57
+ }
58
+ .axi-btn:hover,
59
+ .axi-pill:hover,
60
+ .axi-pill[aria-pressed="true"]:hover,
61
+ .axi-select:hover,
62
+ .axi-card:hover,
63
+ .axi-drawer__close:hover {
64
+ transform: none !important;
65
+ }
66
+ }
package/src/data.css ADDED
@@ -0,0 +1,243 @@
1
+ /* axi design language - data.
2
+ Numbers, and the shapes numbers are drawn as. Everything here is panel
3
+ INTERIOR: a table, a meter and a plot all live inside a .axi-panel, and
4
+ none of them is a raised thing in its own right, so none of them carries a
5
+ block. They are drawn in rules (--axi-rule) and in the ink line, which is
6
+ what keeps a screen of forty metrics from reading as forty floating cards.
7
+
8
+ Three rules govern the file - docs/RULES.md 8, 9 and 10:
9
+ a table is the panel's interior; a quantity is drawn as length, never as
10
+ intensity; and a chart's ink is the accent, with the neutral ramp for
11
+ comparison and a domain palette only where the data owns its own colours. */
12
+
13
+ /* ---------- stat tile ---------- */
14
+ /* One number and its name. Flat on the ground inside its panel, so it takes
15
+ an outline and no block - it is content, not something raised off the
16
+ surface it sits on. The number stays in --axi-text unless it has a real
17
+ state: rule 5 applies to a figure exactly as it applies to a chip, and a
18
+ tile coloured for emphasis is decoration impersonating status. */
19
+ .axi-stat {
20
+ padding: 12px 14px;
21
+ background: var(--axi-ground);
22
+ border: var(--axi-border-control) solid var(--axi-ink-line);
23
+ border-radius: 8px;
24
+ }
25
+ .axi-stat__n {
26
+ display: block;
27
+ font: var(--axi-t-h1);
28
+ letter-spacing: var(--axi-ls-h1);
29
+ color: var(--axi-text);
30
+ font-variant-numeric: tabular-nums;
31
+ }
32
+ .axi-stat__k {
33
+ display: block;
34
+ margin-top: 5px;
35
+ font: var(--axi-t-micro);
36
+ letter-spacing: var(--axi-ls-micro);
37
+ text-transform: uppercase;
38
+ color: var(--axi-text-faint);
39
+ }
40
+ .axi-stat--accent .axi-stat__n { color: var(--axi-accent); }
41
+ .axi-stat--ok .axi-stat__n { color: var(--axi-ok); }
42
+ .axi-stat--warn .axi-stat__n { color: var(--axi-warn); }
43
+ .axi-stat--danger .axi-stat__n { color: var(--axi-danger); }
44
+ /* The one tile that annotates instead of asserting: a count of something the
45
+ app knows *about* the data rather than a measurement of it. */
46
+ .axi-stat--meta .axi-stat__n { color: var(--axi-meta); }
47
+
48
+ /* ---------- table ---------- */
49
+ /* Rule 8. Rows are separated by rules, never outlined and never blocked: the
50
+ panel is the raised thing and the table is what is inside it. The header
51
+ rule is the control weight so the head reads as a lid on the column; the
52
+ row rules are the hairline, which is exactly the case --axi-border-hairline
53
+ exists for - a line inside running content, where either form step would
54
+ turn a list of numbers into a grid of boxes. */
55
+ .axi-table {
56
+ width: 100%;
57
+ border-collapse: collapse;
58
+ font-variant-numeric: tabular-nums;
59
+ }
60
+ /* Numbers right, names left. Set on the element rather than asked of every
61
+ consumer, because a numeric column aligned left is unreadable and it is
62
+ the mistake every hand-built table makes. */
63
+ .axi-table th,
64
+ .axi-table td { text-align: right; white-space: nowrap; }
65
+ .axi-table th:first-child,
66
+ .axi-table td:first-child { text-align: left; }
67
+ .axi-table th {
68
+ padding: 0 10px 10px;
69
+ font: var(--axi-t-micro);
70
+ letter-spacing: var(--axi-ls-micro);
71
+ text-transform: uppercase;
72
+ color: var(--axi-text-faint);
73
+ border-bottom: var(--axi-border-control) solid var(--axi-rule);
74
+ }
75
+ .axi-table td {
76
+ padding: 9px 10px;
77
+ font: var(--axi-t-small);
78
+ font-weight: 700;
79
+ color: var(--axi-text-dim);
80
+ border-bottom: var(--axi-border-hairline) solid var(--axi-rule);
81
+ }
82
+ /* The row under the cursor comes forward on the neutral ramp. Not an ink:
83
+ hovering a row is not a status, and forty rows that each flash a colour on
84
+ the way past the one you want is the tinted-everything failure rule 2 is
85
+ about. */
86
+ .axi-table tbody tr:hover td { background: var(--axi-surface-raised); color: var(--axi-text); }
87
+ /* The measured value in a row, as opposed to its supporting numbers. */
88
+ .axi-table__num { color: var(--axi-text); }
89
+ /* A name cell: an icon, a diamond or a rank beside the label. */
90
+ .axi-table__who { display: flex; align-items: center; gap: 9px; }
91
+ /* Rank is the only fill a table gets, and only where the position is real -
92
+ a podium, not a row number. An outlined rank is the ordinary case. */
93
+ .axi-table__rank {
94
+ width: 22px; height: 22px; flex: none;
95
+ display: grid; place-items: center;
96
+ border: var(--axi-border-control) solid var(--axi-ink-line);
97
+ border-radius: var(--axi-radius-sm);
98
+ background: var(--axi-ground);
99
+ font: var(--axi-t-micro);
100
+ color: var(--axi-text-faint);
101
+ }
102
+ .axi-table__rank--top { background: var(--axi-accent); color: var(--axi-accent-ink); }
103
+
104
+ /* ---------- meter ---------- */
105
+ /* Rule 9: a proportion is a length. The track is the ground, the fill is the
106
+ value, and the fill is one ink at full strength - a tinted or faded bar is
107
+ the same lie as a tinted surface, and it is unreadable at the small sizes a
108
+ table of them is actually used at.
109
+ A meter holds one fill (a value) or several (a composition). There is no
110
+ divider between adjacent fills: two saturated inks already separate
111
+ themselves, and a line between them would be a third form step. */
112
+ .axi-meter {
113
+ display: flex;
114
+ height: var(--axi-meter-h, 12px);
115
+ background: var(--axi-ground);
116
+ border: var(--axi-border-control) solid var(--axi-ink-line);
117
+ border-radius: var(--axi-radius-sm);
118
+ overflow: hidden;
119
+ }
120
+ .axi-meter__fill {
121
+ flex: none;
122
+ height: 100%;
123
+ width: var(--axi-meter-v, 0%);
124
+ background: var(--axi-series, var(--axi-accent));
125
+ }
126
+
127
+ /* A labelled run of meters: the shape almost every "who did most of X" view
128
+ in an axi property turns out to be. Three columns, so the names, the bars
129
+ and the values each line up down the list; both outer columns are knobs
130
+ because a list of account names and a list of boon names disagree about
131
+ how much room a label needs. */
132
+ .axi-meter-list {
133
+ display: grid;
134
+ grid-template-columns: var(--axi-meter-label, 132px) 1fr var(--axi-meter-value, 62px);
135
+ align-items: center;
136
+ gap: 9px 12px;
137
+ }
138
+ .axi-meter-list__name {
139
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
140
+ font: var(--axi-t-small);
141
+ font-weight: 700;
142
+ color: var(--axi-text-dim);
143
+ }
144
+ .axi-meter-list__value {
145
+ text-align: right;
146
+ font: var(--axi-t-micro);
147
+ letter-spacing: var(--axi-ls-micro);
148
+ color: var(--axi-text);
149
+ font-variant-numeric: tabular-nums;
150
+ }
151
+
152
+ /* ---------- bars ---------- */
153
+ /* The same rule stood on end. The baseline is drawn at the control weight
154
+ because it is an axis - the one line in a chart that is structure rather
155
+ than data - and every column is outlined in the ink line so a short column
156
+ is still a shape and not a smear. Columns share their baseline with it, so
157
+ they drop their own bottom border rather than doubling it. */
158
+ .axi-bars {
159
+ display: flex;
160
+ align-items: flex-end;
161
+ gap: var(--axi-bars-gap, 6px);
162
+ height: var(--axi-plot-h, 180px);
163
+ border-bottom: var(--axi-border-control) solid var(--axi-ink-line);
164
+ }
165
+ .axi-bars__col {
166
+ flex: 1 1 0;
167
+ min-width: 4px;
168
+ height: var(--axi-bar-v, 0%);
169
+ display: flex;
170
+ flex-direction: column-reverse;
171
+ overflow: hidden;
172
+ background: var(--axi-series, var(--axi-accent));
173
+ border: var(--axi-border-control) solid var(--axi-ink-line);
174
+ border-bottom: 0;
175
+ border-radius: var(--axi-radius-sm) var(--axi-radius-sm) 0 0;
176
+ }
177
+ /* A stacked column. The parts are laid out from the baseline up, in source
178
+ order, so the markup reads bottom-to-top the way the chart does. */
179
+ .axi-bars__part {
180
+ flex: none;
181
+ width: 100%;
182
+ height: var(--axi-bar-part, 0%);
183
+ background: var(--axi-series, var(--axi-accent));
184
+ }
185
+
186
+ /* ---------- plot ---------- */
187
+ /* A frame for a line or an area, with its horizontal rules drawn in. The
188
+ rules are hard stops in a repeating gradient, which is the select caret's
189
+ exception to rule 1 - a gradient used to draw a shape, with no soft
190
+ transition anywhere in it - and the only way to get N evenly spaced rules
191
+ without asking every consumer to emit N empty divs. */
192
+ .axi-plot {
193
+ position: relative;
194
+ height: var(--axi-plot-h, 180px);
195
+ background-color: var(--axi-ground);
196
+ background-image: repeating-linear-gradient(
197
+ to top,
198
+ var(--axi-rule) 0 var(--axi-border-hairline),
199
+ transparent var(--axi-border-hairline) calc(100% / var(--axi-plot-rows, 4))
200
+ );
201
+ border: var(--axi-border-control) solid var(--axi-ink-line);
202
+ border-radius: var(--axi-radius-sm);
203
+ overflow: hidden;
204
+ }
205
+ /* The geometry itself is the consumer's - this language does not compute a
206
+ path - but its ink and weight are ours, so a line in an axi property is
207
+ the same line everywhere. */
208
+ .axi-plot__svg { position: absolute; inset: 0; width: 100%; height: 100%; }
209
+ .axi-plot__line {
210
+ fill: none;
211
+ stroke: var(--axi-series, var(--axi-accent));
212
+ stroke-width: var(--axi-border-control);
213
+ stroke-linejoin: round;
214
+ stroke-linecap: round;
215
+ vector-effect: non-scaling-stroke;
216
+ }
217
+ /* An area is the region under a line, filled at full strength like every
218
+ other fill in this language. It is opaque, so two overlapping areas are
219
+ not a chart this system draws - that is what the second line, or a second
220
+ plot, is for. */
221
+ .axi-plot__area { fill: var(--axi-series, var(--axi-accent)); stroke: none; }
222
+
223
+ /* ---------- axis and legend ---------- */
224
+ /* The x labels under a plot. There is no y-axis component: the scale of a
225
+ chart belongs in the label above it, in words, where it is readable at a
226
+ glance and survives being screenshotted into Discord. */
227
+ .axi-axis {
228
+ display: flex;
229
+ justify-content: space-between;
230
+ margin-top: 8px;
231
+ font: var(--axi-t-micro);
232
+ letter-spacing: var(--axi-ls-micro);
233
+ text-transform: uppercase;
234
+ color: var(--axi-text-faint);
235
+ }
236
+ .axi-legend { display: flex; flex-wrap: wrap; gap: 7px 16px; }
237
+ .axi-legend__key {
238
+ display: inline-flex; align-items: center; gap: 8px;
239
+ font: var(--axi-t-micro);
240
+ letter-spacing: var(--axi-ls-micro);
241
+ text-transform: uppercase;
242
+ color: var(--axi-text-dim);
243
+ }
package/src/layout.css ADDED
@@ -0,0 +1,47 @@
1
+ /* axi design language - layout.
2
+ Three measures, one grid, two spacing helpers. Deliberately small: a
3
+ layout system large enough to express any page is a framework, and every
4
+ property here can reach for plain CSS grid the moment it needs something
5
+ these do not cover. */
6
+
7
+ /* The page wrapper. --axi-page is the default because most axi surfaces are
8
+ browsing views; the two modifiers exist because prose and dense catalogs
9
+ genuinely disagree about measure, and hard-coding either default made one
10
+ of them wrong. */
11
+ .axi-page {
12
+ max-width: var(--axi-page);
13
+ margin-inline: auto;
14
+ /* The gutter is a knob because measures nest: a --narrow prose column or a
15
+ --wide grid placed inside a page that has already paid the gutter would
16
+ otherwise pay it twice, with no way to say so but an inline
17
+ `padding-inline: 0`. Set --axi-page-pad: 0 on the inner one. */
18
+ padding-inline: var(--axi-page-pad, var(--axi-gutter));
19
+ }
20
+ /* Prose. Beyond this measure a line of body text gets hard to track back to
21
+ the start of the next one. */
22
+ .axi-page--narrow { max-width: var(--axi-page-narrow); }
23
+ /* Dense card grids, where width spent on margins is a column not shown. */
24
+ .axi-page--wide { max-width: var(--axi-page-wide); }
25
+
26
+ /* Auto-fill card grid. The minimum column width is per-instance rather than
27
+ global: a grid of ten app cards and a grid of sixty catalog entries want
28
+ genuinely different minimums, and both are this same component. */
29
+ .axi-grid {
30
+ display: grid;
31
+ grid-template-columns: repeat(auto-fill, minmax(var(--axi-grid-min, 300px), 1fr));
32
+ gap: var(--axi-gutter);
33
+ }
34
+
35
+ /* Vertical rhythm between siblings, set per-instance. */
36
+ .axi-stack { display: flex; flex-direction: column; gap: var(--axi-stack-gap, 12px); }
37
+ /* A horizontal run that wraps rather than overflowing. */
38
+ .axi-row { display: flex; align-items: center; flex-wrap: wrap; gap: var(--axi-row-gap, 10px); }
39
+
40
+ @media (max-width: 640px) {
41
+ /* The fallback must match the resting rule's (var(--axi-gutter), 18px) -
42
+ README.md documents --axi-page-pad's fallback as --axi-gutter, and a
43
+ literal here silently ignores a consumer's --axi-gutter override on
44
+ mobile, which is the one viewport where the gutter matters most. */
45
+ .axi-page { padding-inline: var(--axi-page-pad, var(--axi-gutter)); }
46
+ .axi-grid { grid-template-columns: 1fr; }
47
+ }