@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/LICENSE +21 -0
- package/README.md +121 -0
- package/dist/axi.css +1152 -0
- package/docs/RULES.md +275 -0
- package/package.json +35 -0
- package/src/base.css +66 -0
- package/src/data.css +243 -0
- package/src/layout.css +47 -0
- package/src/primitives.css +301 -0
- package/src/prose.css +93 -0
- package/src/shells.css +303 -0
- package/src/tokens.css +83 -0
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
|
+
}
|