@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
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/* axi design language - primitives.
|
|
2
|
+
The pieces that recur in every property. Two rules govern the whole file:
|
|
3
|
+
a raised thing is outlined in --axi-ink-line and carries a hard offset
|
|
4
|
+
block, never a blur; and a filled chip asserts a value while an outlined
|
|
5
|
+
one annotates. */
|
|
6
|
+
|
|
7
|
+
/* ---------- panel ---------- */
|
|
8
|
+
/* The surface every larger component is built on. Kept as its own class so a
|
|
9
|
+
consumer can raise an arbitrary block into the language without waiting for
|
|
10
|
+
us to ship a component for it. */
|
|
11
|
+
.axi-panel {
|
|
12
|
+
background: var(--axi-surface);
|
|
13
|
+
border: var(--axi-border-panel) solid var(--axi-ink-line);
|
|
14
|
+
border-radius: var(--axi-radius);
|
|
15
|
+
box-shadow: var(--axi-offset-panel) var(--axi-offset-panel) 0 var(--axi-ink-line);
|
|
16
|
+
/* A panel owns its padding so every consumer doesn't have to invent one;
|
|
17
|
+
override per instance with --axi-panel-pad where a tighter or looser
|
|
18
|
+
fit is genuinely called for. */
|
|
19
|
+
padding: var(--axi-panel-pad, 26px);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* ---------- button ---------- */
|
|
23
|
+
.axi-btn {
|
|
24
|
+
display: inline-flex; align-items: center; gap: 8px;
|
|
25
|
+
padding: 12px 20px;
|
|
26
|
+
border: var(--axi-border-control) solid var(--axi-ink-line);
|
|
27
|
+
border-radius: 8px;
|
|
28
|
+
background: var(--axi-ground);
|
|
29
|
+
color: var(--axi-text-dim);
|
|
30
|
+
font: var(--axi-t-label);
|
|
31
|
+
letter-spacing: var(--axi-ls-label);
|
|
32
|
+
text-transform: uppercase;
|
|
33
|
+
text-decoration: none;
|
|
34
|
+
cursor: pointer;
|
|
35
|
+
transition: transform .1s, box-shadow .1s;
|
|
36
|
+
}
|
|
37
|
+
/* Hover lifts. Nothing in this language fades or glows: the block deepens and
|
|
38
|
+
the element moves against it, which reads instantly even in peripheral
|
|
39
|
+
vision and costs no colour. */
|
|
40
|
+
.axi-btn:hover {
|
|
41
|
+
color: var(--axi-text);
|
|
42
|
+
box-shadow: var(--axi-offset-control) var(--axi-offset-control) 0 var(--axi-ink-line);
|
|
43
|
+
transform: translate(-2px, -2px);
|
|
44
|
+
}
|
|
45
|
+
.axi-btn--primary {
|
|
46
|
+
background: var(--axi-accent);
|
|
47
|
+
color: var(--axi-accent-ink);
|
|
48
|
+
box-shadow: var(--axi-offset-control) var(--axi-offset-control) 0 var(--axi-ink-line);
|
|
49
|
+
}
|
|
50
|
+
/* The one button that rests with a block already under it, so the generic
|
|
51
|
+
hover's translate alone would move element and block together and leave the
|
|
52
|
+
lower-right edge where it started - the "grows rather than lifts" failure
|
|
53
|
+
rule 4 warns about. It lifts the way a panel does: the block deepens too. */
|
|
54
|
+
.axi-btn--primary:hover {
|
|
55
|
+
color: var(--axi-accent-ink);
|
|
56
|
+
box-shadow: var(--axi-offset-control-hover) var(--axi-offset-control-hover) 0 var(--axi-ink-line);
|
|
57
|
+
}
|
|
58
|
+
.axi-btn--ghost { background: transparent; }
|
|
59
|
+
.axi-btn--dashed { background: transparent; border-style: dashed; }
|
|
60
|
+
|
|
61
|
+
/* ---------- pill ---------- */
|
|
62
|
+
/* A filter toggle. Its pressed state fills with whatever colour the consumer
|
|
63
|
+
has put on it, so the control reads as the thing it filters to rather than
|
|
64
|
+
as a generic "selected". Consumers set that colour with --axi-pill-fill,
|
|
65
|
+
on the pill or on any ancestor - it is read through a fallback and never
|
|
66
|
+
declared on the component, which is what keeps both placements working. */
|
|
67
|
+
.axi-pill {
|
|
68
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
69
|
+
padding: 10px 9px;
|
|
70
|
+
border: var(--axi-border-control) solid var(--axi-ink-line);
|
|
71
|
+
border-radius: 8px;
|
|
72
|
+
background: var(--axi-ground);
|
|
73
|
+
color: var(--axi-text-dim);
|
|
74
|
+
font: var(--axi-t-label);
|
|
75
|
+
letter-spacing: var(--axi-ls-label);
|
|
76
|
+
text-transform: uppercase;
|
|
77
|
+
cursor: pointer;
|
|
78
|
+
transition: transform .1s, box-shadow .1s;
|
|
79
|
+
}
|
|
80
|
+
.axi-pill:hover {
|
|
81
|
+
color: var(--axi-text);
|
|
82
|
+
box-shadow: var(--axi-offset-control) var(--axi-offset-control) 0 var(--axi-ink-line);
|
|
83
|
+
transform: translate(-2px, -2px);
|
|
84
|
+
}
|
|
85
|
+
.axi-pill[aria-pressed="true"] {
|
|
86
|
+
background: var(--axi-pill-fill, var(--axi-accent));
|
|
87
|
+
color: var(--axi-accent-ink);
|
|
88
|
+
box-shadow: var(--axi-offset-control) var(--axi-offset-control) 0 var(--axi-ink-line);
|
|
89
|
+
}
|
|
90
|
+
/* Spelled out rather than left to source order: `.axi-pill:hover` and
|
|
91
|
+
`.axi-pill[aria-pressed="true"]` have identical specificity, so without this
|
|
92
|
+
rule a pressed pill under the cursor kept its pressed block while still
|
|
93
|
+
taking the hover's translate - a slide, not a lift, decided by which rule
|
|
94
|
+
happened to come last in the file. A pressed pill keeps its fill and lifts. */
|
|
95
|
+
.axi-pill[aria-pressed="true"]:hover {
|
|
96
|
+
background: var(--axi-pill-fill, var(--axi-accent));
|
|
97
|
+
color: var(--axi-accent-ink);
|
|
98
|
+
box-shadow: var(--axi-offset-control-hover) var(--axi-offset-control-hover) 0 var(--axi-ink-line);
|
|
99
|
+
transform: translate(-2px, -2px);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* ---------- switch ---------- */
|
|
103
|
+
/* On/off for a setting, where a pill's "pressed" is not enough: a pill says
|
|
104
|
+
which of several filters you chose, a switch says what state one thing is
|
|
105
|
+
in when nothing else is on screen to compare it against. Rule 5 is what
|
|
106
|
+
makes it legible - the filled track asserts the setting's status, and the
|
|
107
|
+
status is the only thing that changes between the two states.
|
|
108
|
+
|
|
109
|
+
It is a slot with something sitting in it, so it takes no block: the block
|
|
110
|
+
belongs to things you press. The track carries a full ink edge at the
|
|
111
|
+
control weight and the raised surface, because an off switch inside a panel
|
|
112
|
+
is a surface on a surface - without the edge the track vanishes and all you
|
|
113
|
+
see is a slug floating in the card.
|
|
114
|
+
|
|
115
|
+
The slug is ink in both states. That is deliberate: on and off differ in
|
|
116
|
+
what colour is in the slot, never in how bright the part that moves is. */
|
|
117
|
+
.axi-switch {
|
|
118
|
+
position: relative;
|
|
119
|
+
flex: none;
|
|
120
|
+
display: inline-block;
|
|
121
|
+
width: var(--axi-switch-w, 46px);
|
|
122
|
+
height: var(--axi-switch-h, 26px);
|
|
123
|
+
padding: 0;
|
|
124
|
+
border: var(--axi-border-control) solid var(--axi-ink-line);
|
|
125
|
+
border-radius: var(--axi-radius-sm);
|
|
126
|
+
background: var(--axi-surface-raised);
|
|
127
|
+
cursor: pointer;
|
|
128
|
+
}
|
|
129
|
+
/* Read through a fallback and never declared on the component, so a consumer
|
|
130
|
+
can set the on-colour on the switch or on any ancestor - the same contract
|
|
131
|
+
as --axi-pill-fill. A switch that turns something dangerous on can fill in
|
|
132
|
+
--axi-danger and say so. */
|
|
133
|
+
.axi-switch[aria-checked="true"] { background: var(--axi-switch-fill, var(--axi-accent)); }
|
|
134
|
+
/* Pinned to the track's insets rather than given a height and centred, so the
|
|
135
|
+
slug stays correct if the track is resized. Only its travel is a transform,
|
|
136
|
+
which is the one thing rule 11 lets move. */
|
|
137
|
+
.axi-switch__knob {
|
|
138
|
+
position: absolute;
|
|
139
|
+
top: 2px; bottom: 2px; left: 2px;
|
|
140
|
+
width: var(--axi-switch-knob, 16px);
|
|
141
|
+
border-radius: 2px;
|
|
142
|
+
background: var(--axi-ink-line);
|
|
143
|
+
transition: transform .1s;
|
|
144
|
+
}
|
|
145
|
+
/* The travel is the track's inner width less the slug and its two insets.
|
|
146
|
+
Spelled as a calc over the same tokens the track is built from so it stays
|
|
147
|
+
right at any size, and - the part that matters - at any border weight: a
|
|
148
|
+
hard-coded travel measured against one weight puts the slug through the
|
|
149
|
+
right-hand edge the moment rule 3's numbers change. */
|
|
150
|
+
.axi-switch[aria-checked="true"] .axi-switch__knob {
|
|
151
|
+
transform: translateX(calc(
|
|
152
|
+
var(--axi-switch-w, 46px) - 2 * var(--axi-border-control)
|
|
153
|
+
- var(--axi-switch-knob, 16px) - 4px));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/* ---------- chip ---------- */
|
|
157
|
+
/* Filled asserts, outlined annotates. The unmodified chip is a neutral fact;
|
|
158
|
+
the status modifiers assert a value; --meta is the only outlined variant and
|
|
159
|
+
is drawn in the reserved cool ink, so a reader can tell commentary from
|
|
160
|
+
data without reading either. */
|
|
161
|
+
.axi-chip {
|
|
162
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
163
|
+
padding: 5px 9px;
|
|
164
|
+
border: var(--axi-border-control) solid var(--axi-ink-line);
|
|
165
|
+
border-radius: var(--axi-radius-sm);
|
|
166
|
+
background: var(--axi-surface-raised);
|
|
167
|
+
color: var(--axi-text-dim);
|
|
168
|
+
font: var(--axi-t-micro);
|
|
169
|
+
letter-spacing: var(--axi-ls-micro);
|
|
170
|
+
text-transform: uppercase;
|
|
171
|
+
}
|
|
172
|
+
.axi-chip--ok { background: var(--axi-ok); color: var(--axi-ink-line); }
|
|
173
|
+
.axi-chip--warn { background: var(--axi-warn); color: var(--axi-ink-line); }
|
|
174
|
+
.axi-chip--danger { background: var(--axi-danger); color: var(--axi-ink-line); }
|
|
175
|
+
.axi-chip--accent { background: var(--axi-accent); color: var(--axi-accent-ink); }
|
|
176
|
+
.axi-chip--meta {
|
|
177
|
+
background: transparent;
|
|
178
|
+
border-color: var(--axi-meta);
|
|
179
|
+
color: var(--axi-meta);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/* ---------- count badge ---------- */
|
|
183
|
+
.axi-badge-count {
|
|
184
|
+
display: inline-block;
|
|
185
|
+
padding: 1px 7px;
|
|
186
|
+
border-radius: 99px;
|
|
187
|
+
background: var(--axi-accent);
|
|
188
|
+
color: var(--axi-accent-ink);
|
|
189
|
+
font: var(--axi-t-micro);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/* ---------- diamond ---------- */
|
|
193
|
+
/* The family motif: a rotated outlined square. Used as a bullet, a status dot
|
|
194
|
+
and, scaled up behind a glyph, the brand sigil. */
|
|
195
|
+
.axi-diamond {
|
|
196
|
+
display: inline-block;
|
|
197
|
+
width: 9px; height: 9px; flex: none;
|
|
198
|
+
transform: rotate(45deg);
|
|
199
|
+
background: var(--axi-meta);
|
|
200
|
+
border: var(--axi-border-control) solid var(--axi-ink-line);
|
|
201
|
+
}
|
|
202
|
+
.axi-diamond--accent { background: var(--axi-accent); }
|
|
203
|
+
.axi-diamond--ok { background: var(--axi-ok); }
|
|
204
|
+
.axi-diamond--warn { background: var(--axi-warn); }
|
|
205
|
+
.axi-diamond--danger { background: var(--axi-danger); }
|
|
206
|
+
/* A legend key, where the diamond stands for a series rather than a state.
|
|
207
|
+
Opt-in rather than a default on .axi-diamond: --axi-series is read from
|
|
208
|
+
whatever ancestor sets it, and a chart panel that sets one would otherwise
|
|
209
|
+
silently recolour every bullet inside it. */
|
|
210
|
+
.axi-diamond--series { background: var(--axi-series, var(--axi-accent)); }
|
|
211
|
+
|
|
212
|
+
/* ---------- input ---------- */
|
|
213
|
+
.axi-input {
|
|
214
|
+
width: 100%;
|
|
215
|
+
padding: 11px 12px;
|
|
216
|
+
background: var(--axi-ground);
|
|
217
|
+
border: var(--axi-border-control) solid var(--axi-ink-line);
|
|
218
|
+
border-radius: 8px;
|
|
219
|
+
color: var(--axi-text);
|
|
220
|
+
font: var(--axi-t-label);
|
|
221
|
+
font-size: 14px;
|
|
222
|
+
}
|
|
223
|
+
.axi-input::placeholder { color: var(--axi-text-faint); font-weight: 500; }
|
|
224
|
+
|
|
225
|
+
/* A search input with the magnifier drawn inside it. The glyph is the
|
|
226
|
+
consumer's; this only reserves the room and positions it. */
|
|
227
|
+
.axi-search { position: relative; }
|
|
228
|
+
.axi-search .axi-input { padding-left: 38px; }
|
|
229
|
+
.axi-search__icon {
|
|
230
|
+
position: absolute; left: 13px; top: 50%; transform: translateY(-50%);
|
|
231
|
+
color: var(--axi-accent); font-weight: 900; pointer-events: none;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/* ---------- select ---------- */
|
|
235
|
+
/* The closed box is ours everywhere: strip the native control and draw the
|
|
236
|
+
caret, so a select sits alongside the other controls as just another
|
|
237
|
+
outlined chip instead of announcing the OS. */
|
|
238
|
+
.axi-select {
|
|
239
|
+
appearance: none;
|
|
240
|
+
padding: 10px 30px 10px 9px;
|
|
241
|
+
border: var(--axi-border-control) solid var(--axi-ink-line);
|
|
242
|
+
border-radius: 8px;
|
|
243
|
+
background-color: var(--axi-ground);
|
|
244
|
+
color: var(--axi-text-dim);
|
|
245
|
+
font: var(--axi-t-label);
|
|
246
|
+
font-size: 12.5px;
|
|
247
|
+
letter-spacing: var(--axi-ls-label);
|
|
248
|
+
text-transform: uppercase;
|
|
249
|
+
cursor: pointer;
|
|
250
|
+
/* Two triangles meeting: a caret with no image and no icon font. */
|
|
251
|
+
background-image:
|
|
252
|
+
linear-gradient(45deg, transparent 50%, var(--axi-accent) 50%),
|
|
253
|
+
linear-gradient(135deg, var(--axi-accent) 50%, transparent 50%);
|
|
254
|
+
background-position: calc(100% - 16px) calc(50% + 2px), calc(100% - 11px) calc(50% + 2px);
|
|
255
|
+
background-size: 5px 5px, 5px 5px;
|
|
256
|
+
background-repeat: no-repeat;
|
|
257
|
+
transition: transform .1s, box-shadow .1s;
|
|
258
|
+
}
|
|
259
|
+
.axi-select:hover {
|
|
260
|
+
color: var(--axi-text);
|
|
261
|
+
box-shadow: var(--axi-offset-control) var(--axi-offset-control) 0 var(--axi-ink-line);
|
|
262
|
+
transform: translate(-2px, -2px);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/* The popup stays OS chrome until a browser lets us style it. Where one does
|
|
266
|
+
(Chromium's base-select), the list is drawn with the same outline and offset
|
|
267
|
+
block as our own popovers, so both dropdown kinds read as one family; where
|
|
268
|
+
it does not, the closed box above is still ours and the list is native. */
|
|
269
|
+
@supports (appearance: base-select) {
|
|
270
|
+
/* base-select draws its own ::picker-icon, so the hand-drawn caret above
|
|
271
|
+
would be a second arrow. */
|
|
272
|
+
.axi-select, .axi-select::picker(select) { appearance: base-select; }
|
|
273
|
+
.axi-select { background-image: none; padding-right: 9px; }
|
|
274
|
+
.axi-select::picker-icon { color: var(--axi-accent); transition: none; }
|
|
275
|
+
.axi-select::picker(select) {
|
|
276
|
+
margin-top: 9px; padding: 6px;
|
|
277
|
+
border: var(--axi-border-panel) solid var(--axi-ink-line);
|
|
278
|
+
border-radius: 9px;
|
|
279
|
+
background: var(--axi-surface-raised);
|
|
280
|
+
box-shadow: var(--axi-offset-panel) var(--axi-offset-panel) 0 var(--axi-ink-line);
|
|
281
|
+
}
|
|
282
|
+
.axi-select option {
|
|
283
|
+
display: flex; align-items: center; gap: 9px;
|
|
284
|
+
padding: 8px 9px;
|
|
285
|
+
border-radius: var(--axi-radius-sm);
|
|
286
|
+
background: transparent;
|
|
287
|
+
color: var(--axi-text-dim);
|
|
288
|
+
font: var(--axi-t-label);
|
|
289
|
+
font-size: 12.5px;
|
|
290
|
+
letter-spacing: var(--axi-ls-label);
|
|
291
|
+
text-transform: uppercase;
|
|
292
|
+
}
|
|
293
|
+
.axi-select option:hover, .axi-select option:focus {
|
|
294
|
+
background: var(--axi-ground); color: var(--axi-text);
|
|
295
|
+
}
|
|
296
|
+
/* The page-wide focus ring sits 2px outside its element; inside a picker
|
|
297
|
+
that is 2px into the neighbouring row, so pull it back in. */
|
|
298
|
+
.axi-select option:focus-visible { outline-offset: -3px; }
|
|
299
|
+
.axi-select option:checked { color: var(--axi-text); }
|
|
300
|
+
.axi-select option::checkmark { content: "\2713"; color: var(--axi-accent); font-weight: 900; }
|
|
301
|
+
}
|
package/src/prose.css
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/* axi design language - prose.
|
|
2
|
+
A single class wrapping rendered markdown, so a content pipeline can emit
|
|
3
|
+
plain HTML with no classes at all and still land inside the language.
|
|
4
|
+
|
|
5
|
+
This layer has no ancestor in the source site, which was all cards and
|
|
6
|
+
controls. The restraint it needs is different: long-form reading wants
|
|
7
|
+
generous leading and quiet rules, and the offset blocks that make a card
|
|
8
|
+
feel physical would make a paragraph feel shouted. So nothing in here is
|
|
9
|
+
blocked except the code block and the table, which are genuinely objects
|
|
10
|
+
dropped into the text rather than part of its flow. */
|
|
11
|
+
.axi-prose { color: var(--axi-text-dim); font: var(--axi-t-body); }
|
|
12
|
+
|
|
13
|
+
.axi-prose > :first-child { margin-top: 0; }
|
|
14
|
+
.axi-prose > :last-child { margin-bottom: 0; }
|
|
15
|
+
|
|
16
|
+
.axi-prose h1 { margin: 0 0 18px; font: var(--axi-t-h1); letter-spacing: var(--axi-ls-h1); color: var(--axi-text); }
|
|
17
|
+
.axi-prose h2 {
|
|
18
|
+
margin: 38px 0 14px; padding-top: 22px;
|
|
19
|
+
border-top: var(--axi-border-control) solid var(--axi-rule);
|
|
20
|
+
font: var(--axi-t-h2); letter-spacing: var(--axi-ls-h2); color: var(--axi-text);
|
|
21
|
+
}
|
|
22
|
+
.axi-prose h3 { margin: 28px 0 10px; font: var(--axi-t-h3); letter-spacing: var(--axi-ls-h3); color: var(--axi-text); }
|
|
23
|
+
.axi-prose h4 {
|
|
24
|
+
margin: 22px 0 8px;
|
|
25
|
+
font: var(--axi-t-eyebrow); letter-spacing: var(--axi-ls-eyebrow);
|
|
26
|
+
text-transform: uppercase; color: var(--axi-text-faint);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.axi-prose p { margin: 0 0 16px; }
|
|
30
|
+
.axi-prose strong { color: var(--axi-text); font-weight: 800; }
|
|
31
|
+
/* Prose is the one place a link should announce itself: a reader scanning an
|
|
32
|
+
article is looking for them, and inheriting the body ink would hide them. */
|
|
33
|
+
.axi-prose a { color: var(--axi-accent); font-weight: 600; text-underline-offset: 2px; }
|
|
34
|
+
.axi-prose a:hover { color: var(--axi-text); }
|
|
35
|
+
|
|
36
|
+
.axi-prose ul, .axi-prose ol { margin: 0 0 16px; padding-left: 22px; }
|
|
37
|
+
.axi-prose li { margin: 0 0 7px; }
|
|
38
|
+
/* The family motif as a bullet. Drawn with ::marker's replacement rather than
|
|
39
|
+
a background image so it inherits the list's own indentation. */
|
|
40
|
+
.axi-prose ul { list-style: none; padding-left: 20px; }
|
|
41
|
+
.axi-prose ul > li { position: relative; }
|
|
42
|
+
.axi-prose ul > li::before {
|
|
43
|
+
content: ""; position: absolute; left: -20px; top: .55em;
|
|
44
|
+
width: 7px; height: 7px; transform: rotate(45deg);
|
|
45
|
+
background: var(--axi-accent);
|
|
46
|
+
border: var(--axi-border-hairline) solid var(--axi-ink-line);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.axi-prose code {
|
|
50
|
+
font-family: var(--axi-mono); font-size: .88em;
|
|
51
|
+
background: var(--axi-ground); color: var(--axi-text);
|
|
52
|
+
border: var(--axi-border-hairline) solid var(--axi-ink-line);
|
|
53
|
+
border-radius: 4px; padding: 1px 5px;
|
|
54
|
+
}
|
|
55
|
+
.axi-prose pre {
|
|
56
|
+
margin: 0 0 18px; padding: 14px 16px; overflow-x: auto;
|
|
57
|
+
background: var(--axi-ground);
|
|
58
|
+
border: var(--axi-border-control) solid var(--axi-ink-line);
|
|
59
|
+
border-radius: var(--axi-radius-sm);
|
|
60
|
+
box-shadow: var(--axi-offset-control) var(--axi-offset-control) 0 var(--axi-ink-line);
|
|
61
|
+
}
|
|
62
|
+
.axi-prose pre code { background: none; border: 0; padding: 0; font-size: 12.5px; line-height: 1.6; }
|
|
63
|
+
|
|
64
|
+
.axi-prose blockquote {
|
|
65
|
+
margin: 0 0 18px;
|
|
66
|
+
padding: 11px 13px;
|
|
67
|
+
border-left: var(--axi-border-panel) solid var(--axi-accent);
|
|
68
|
+
background: var(--axi-ground);
|
|
69
|
+
border-radius: 0 var(--axi-radius-sm) var(--axi-radius-sm) 0;
|
|
70
|
+
}
|
|
71
|
+
.axi-prose blockquote p { margin: 0; font-style: italic; }
|
|
72
|
+
|
|
73
|
+
.axi-prose table {
|
|
74
|
+
width: 100%; margin: 0 0 18px;
|
|
75
|
+
border-collapse: collapse;
|
|
76
|
+
border: var(--axi-border-control) solid var(--axi-ink-line);
|
|
77
|
+
border-radius: var(--axi-radius-sm);
|
|
78
|
+
overflow: hidden;
|
|
79
|
+
font-size: 13.5px;
|
|
80
|
+
}
|
|
81
|
+
.axi-prose th {
|
|
82
|
+
text-align: left; padding: 10px 12px;
|
|
83
|
+
background: var(--axi-surface-raised); color: var(--axi-text);
|
|
84
|
+
font: var(--axi-t-micro); letter-spacing: var(--axi-ls-micro); text-transform: uppercase;
|
|
85
|
+
}
|
|
86
|
+
.axi-prose td { padding: 10px 12px; border-top: var(--axi-border-hairline) solid var(--axi-rule); }
|
|
87
|
+
|
|
88
|
+
.axi-prose hr {
|
|
89
|
+
margin: 30px 0; height: 0;
|
|
90
|
+
border: 0; border-top: var(--axi-border-control) solid var(--axi-rule);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.axi-prose img { max-width: 100%; height: auto; border-radius: var(--axi-radius-sm); }
|
package/src/shells.css
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/* axi design language - shells.
|
|
2
|
+
Assembled components. Each is built from the primitives rather than
|
|
3
|
+
redefining them, so a change to .axi-panel moves the notice, the toolbar,
|
|
4
|
+
the popover and the card together. */
|
|
5
|
+
|
|
6
|
+
/* The small uppercase heading that labels a panel's contents. */
|
|
7
|
+
.axi-eyebrow {
|
|
8
|
+
margin: 0 0 14px;
|
|
9
|
+
font: var(--axi-t-eyebrow);
|
|
10
|
+
letter-spacing: var(--axi-ls-eyebrow);
|
|
11
|
+
text-transform: uppercase;
|
|
12
|
+
color: var(--axi-text-faint);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/* ---------- masthead ---------- */
|
|
16
|
+
.axi-mast {
|
|
17
|
+
position: sticky; top: 0; z-index: 40;
|
|
18
|
+
background: var(--axi-ground);
|
|
19
|
+
border-bottom: var(--axi-border-panel) solid var(--axi-ink-line);
|
|
20
|
+
}
|
|
21
|
+
.axi-mast__in {
|
|
22
|
+
max-width: var(--axi-page); margin-inline: auto;
|
|
23
|
+
padding-inline: var(--axi-gutter);
|
|
24
|
+
display: flex; align-items: center; gap: 22px; min-height: 66px; flex-wrap: wrap;
|
|
25
|
+
}
|
|
26
|
+
.axi-brand { display: flex; align-items: center; gap: 12px; text-decoration: none; }
|
|
27
|
+
/* The sigil is the diamond motif scaled up with a glyph on top. The diamond is
|
|
28
|
+
a rotated pseudo-element behind the text rather than a rotated box, so the
|
|
29
|
+
glyph stays upright. isolation keeps the negative z-index inside the sigil
|
|
30
|
+
instead of dropping it behind the masthead. */
|
|
31
|
+
.axi-sigil {
|
|
32
|
+
position: relative; isolation: isolate;
|
|
33
|
+
width: 34px; height: 34px; flex: none;
|
|
34
|
+
display: grid; place-items: center;
|
|
35
|
+
color: var(--axi-accent-ink); font-weight: 900; font-size: 13px;
|
|
36
|
+
}
|
|
37
|
+
.axi-sigil::before {
|
|
38
|
+
content: ""; position: absolute; inset: 0; z-index: -1;
|
|
39
|
+
transform: rotate(45deg);
|
|
40
|
+
border-radius: 5px;
|
|
41
|
+
background: var(--axi-accent);
|
|
42
|
+
border: var(--axi-border-control) solid var(--axi-ink-line);
|
|
43
|
+
}
|
|
44
|
+
.axi-brand__name { font: 800 17px/1.15 var(--axi-sans); letter-spacing: -.02em; }
|
|
45
|
+
.axi-brand__name small {
|
|
46
|
+
display: block;
|
|
47
|
+
font: 700 10.5px/1.4 var(--axi-sans);
|
|
48
|
+
color: var(--axi-text-faint);
|
|
49
|
+
letter-spacing: .09em; text-transform: uppercase;
|
|
50
|
+
}
|
|
51
|
+
.axi-tabs { display: flex; gap: 8px; margin-left: auto; }
|
|
52
|
+
.axi-tabs a {
|
|
53
|
+
padding: 8px 16px;
|
|
54
|
+
font: 800 13.5px/1 var(--axi-sans);
|
|
55
|
+
color: var(--axi-text-dim); text-decoration: none;
|
|
56
|
+
border: var(--axi-border-control) solid transparent;
|
|
57
|
+
border-radius: 8px;
|
|
58
|
+
}
|
|
59
|
+
.axi-tabs a:hover { color: var(--axi-text); border-color: var(--axi-rule); }
|
|
60
|
+
/* The current tab is filled and blocked - the same treatment a pressed pill
|
|
61
|
+
gets, because it is the same idea: this one is on. */
|
|
62
|
+
.axi-tabs a[aria-current="page"] {
|
|
63
|
+
background: var(--axi-accent);
|
|
64
|
+
color: var(--axi-accent-ink);
|
|
65
|
+
border-color: var(--axi-ink-line);
|
|
66
|
+
box-shadow: var(--axi-offset-control) var(--axi-offset-control) 0 var(--axi-ink-line);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* ---------- notice ---------- */
|
|
70
|
+
.axi-notice {
|
|
71
|
+
display: flex; gap: 14px; padding: 15px 18px;
|
|
72
|
+
background: var(--axi-surface);
|
|
73
|
+
border: var(--axi-border-panel) solid var(--axi-ink-line);
|
|
74
|
+
border-radius: var(--axi-radius);
|
|
75
|
+
box-shadow: var(--axi-offset-panel) var(--axi-offset-panel) 0 var(--axi-ink-line);
|
|
76
|
+
}
|
|
77
|
+
.axi-notice__icon {
|
|
78
|
+
flex: none; width: 26px; height: 26px;
|
|
79
|
+
display: grid; place-items: center;
|
|
80
|
+
border-radius: var(--axi-radius-sm);
|
|
81
|
+
background: var(--axi-accent); color: var(--axi-accent-ink);
|
|
82
|
+
font-size: 14px; font-weight: 900;
|
|
83
|
+
border: var(--axi-border-control) solid var(--axi-ink-line);
|
|
84
|
+
}
|
|
85
|
+
.axi-notice p { margin: 0; font: var(--axi-t-small); color: var(--axi-text-dim); }
|
|
86
|
+
.axi-notice b { color: var(--axi-accent); font-weight: 800; }
|
|
87
|
+
|
|
88
|
+
/* ---------- toolbar ---------- */
|
|
89
|
+
.axi-toolbar {
|
|
90
|
+
display: flex; gap: 8px; flex-wrap: wrap; align-items: center;
|
|
91
|
+
padding: 14px;
|
|
92
|
+
background: var(--axi-surface);
|
|
93
|
+
border: var(--axi-border-panel) solid var(--axi-ink-line);
|
|
94
|
+
border-radius: var(--axi-radius);
|
|
95
|
+
box-shadow: var(--axi-offset-panel) var(--axi-offset-panel) 0 var(--axi-ink-line);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/* ---------- menu popover ---------- */
|
|
99
|
+
/* A disclosure, not a permanent row of controls. The trigger is an .axi-btn
|
|
100
|
+
with aria-expanded; this styles the panel it opens. */
|
|
101
|
+
.axi-menu { position: relative; }
|
|
102
|
+
.axi-menu__pop {
|
|
103
|
+
/* Above .axi-mast's z-index: 40. Both sit in the root stacking context, so
|
|
104
|
+
a toolbar scrolled under the sticky masthead would otherwise open its
|
|
105
|
+
menu behind it. */
|
|
106
|
+
position: absolute; top: calc(100% + 9px); left: 0; z-index: 41;
|
|
107
|
+
width: var(--axi-menu-width, 310px);
|
|
108
|
+
max-height: 340px; overflow-y: auto; padding: 8px;
|
|
109
|
+
background: var(--axi-surface-raised);
|
|
110
|
+
border: var(--axi-border-panel) solid var(--axi-ink-line);
|
|
111
|
+
border-radius: 9px;
|
|
112
|
+
box-shadow: var(--axi-offset-panel) var(--axi-offset-panel) 0 var(--axi-ink-line);
|
|
113
|
+
}
|
|
114
|
+
.axi-menu__pop[hidden] { display: none; }
|
|
115
|
+
.axi-menu__pop label {
|
|
116
|
+
display: flex; gap: 9px; align-items: flex-start;
|
|
117
|
+
padding: 7px 8px; border-radius: var(--axi-radius-sm);
|
|
118
|
+
font-size: 13px; font-weight: 600; color: var(--axi-text-dim); cursor: pointer;
|
|
119
|
+
}
|
|
120
|
+
.axi-menu__pop label:hover { background: var(--axi-ground); color: var(--axi-text); }
|
|
121
|
+
.axi-menu__pop input { margin: 3px 0 0; accent-color: var(--axi-accent); }
|
|
122
|
+
|
|
123
|
+
/* ---------- card ---------- */
|
|
124
|
+
.axi-card {
|
|
125
|
+
position: relative; overflow: hidden;
|
|
126
|
+
width: 100%; text-align: left;
|
|
127
|
+
display: flex; flex-direction: column; gap: 11px;
|
|
128
|
+
padding: 22px 16px 14px;
|
|
129
|
+
background: var(--axi-surface);
|
|
130
|
+
border: var(--axi-border-panel) solid var(--axi-ink-line);
|
|
131
|
+
border-radius: var(--axi-radius);
|
|
132
|
+
box-shadow: var(--axi-offset-panel) var(--axi-offset-panel) 0 var(--axi-ink-line);
|
|
133
|
+
font: inherit; color: inherit; text-decoration: none; cursor: pointer;
|
|
134
|
+
transition: transform .1s, box-shadow .1s;
|
|
135
|
+
}
|
|
136
|
+
.axi-card:hover {
|
|
137
|
+
transform: translate(-3px, -3px);
|
|
138
|
+
box-shadow: var(--axi-offset-panel-hover) var(--axi-offset-panel-hover) 0 var(--axi-ink-line);
|
|
139
|
+
}
|
|
140
|
+
/* The top strip is drawn from the card itself so a renderer does not have to
|
|
141
|
+
emit an extra element for it. It is opt-in: a card only gets a strip when
|
|
142
|
+
the consumer has a real value to encode in it, because a coloured strip that
|
|
143
|
+
means nothing is decoration impersonating data. */
|
|
144
|
+
.axi-card--strip::before {
|
|
145
|
+
content: ""; position: absolute; top: 0; left: 0; right: 0; height: 9px;
|
|
146
|
+
background: var(--axi-card-strip, var(--axi-accent));
|
|
147
|
+
border-bottom: var(--axi-border-panel) solid var(--axi-ink-line);
|
|
148
|
+
}
|
|
149
|
+
.axi-card__head { display: flex; gap: 12px; align-items: flex-start; }
|
|
150
|
+
.axi-card__glyph {
|
|
151
|
+
flex: none; width: 40px; height: 40px;
|
|
152
|
+
display: grid; place-items: center;
|
|
153
|
+
border-radius: 8px;
|
|
154
|
+
border: var(--axi-border-control) solid var(--axi-ink-line);
|
|
155
|
+
background: var(--axi-surface-raised);
|
|
156
|
+
color: var(--axi-text);
|
|
157
|
+
font: 900 15px/1 var(--axi-sans);
|
|
158
|
+
}
|
|
159
|
+
.axi-card__title { flex: 1; min-width: 0; }
|
|
160
|
+
.axi-card__title .axi-card__name {
|
|
161
|
+
display: block;
|
|
162
|
+
font: var(--axi-t-h3); letter-spacing: var(--axi-ls-h3);
|
|
163
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
164
|
+
}
|
|
165
|
+
.axi-card__title .axi-card__kind {
|
|
166
|
+
display: block; margin-top: 3px;
|
|
167
|
+
font: 700 11px/1.3 var(--axi-sans);
|
|
168
|
+
color: var(--axi-text-faint);
|
|
169
|
+
letter-spacing: .08em; text-transform: uppercase;
|
|
170
|
+
}
|
|
171
|
+
.axi-card p { margin: 0; font: var(--axi-t-small); color: var(--axi-text-dim); }
|
|
172
|
+
/* Pushed to the bottom so that in an equalised grid row every card's meta
|
|
173
|
+
strip lands on the same line and the rules read as continuous across the
|
|
174
|
+
grid. The cost is empty space on cards with less content. */
|
|
175
|
+
.axi-card__meta {
|
|
176
|
+
display: flex; gap: 14px; flex-wrap: wrap; align-items: center;
|
|
177
|
+
margin-top: auto; padding-top: 12px;
|
|
178
|
+
border-top: var(--axi-border-control) solid var(--axi-rule);
|
|
179
|
+
font: 700 12px/1 var(--axi-sans);
|
|
180
|
+
color: var(--axi-text-faint);
|
|
181
|
+
}
|
|
182
|
+
.axi-card__go { margin-left: auto; color: var(--axi-accent); font-weight: 900; text-transform: uppercase; letter-spacing: .05em; }
|
|
183
|
+
.axi-card:hover .axi-card__go { color: var(--axi-text); }
|
|
184
|
+
|
|
185
|
+
/* ---------- drawer ---------- */
|
|
186
|
+
.axi-scrim { position: fixed; inset: 0; z-index: 50; background: var(--axi-scrim); border: 0; }
|
|
187
|
+
.axi-drawer {
|
|
188
|
+
position: fixed; top: 0; right: 0; bottom: 0; z-index: 51;
|
|
189
|
+
width: min(var(--axi-drawer-width, 560px), 100vw);
|
|
190
|
+
display: flex; flex-direction: column;
|
|
191
|
+
background: var(--axi-surface);
|
|
192
|
+
border-left: var(--axi-border-panel) solid var(--axi-ink-line);
|
|
193
|
+
}
|
|
194
|
+
.axi-drawer[hidden], .axi-scrim[hidden] { display: none; }
|
|
195
|
+
.axi-drawer__head {
|
|
196
|
+
position: relative;
|
|
197
|
+
padding: 20px 24px 18px;
|
|
198
|
+
border-bottom: var(--axi-border-panel) solid var(--axi-ink-line);
|
|
199
|
+
}
|
|
200
|
+
.axi-drawer__head h2 {
|
|
201
|
+
margin: 2px 0 0; padding-right: 44px;
|
|
202
|
+
font: var(--axi-t-h2); letter-spacing: var(--axi-ls-h2);
|
|
203
|
+
}
|
|
204
|
+
.axi-drawer__close {
|
|
205
|
+
position: absolute; top: 16px; right: 18px;
|
|
206
|
+
width: 32px; height: 32px;
|
|
207
|
+
display: grid; place-items: center;
|
|
208
|
+
border-radius: 8px;
|
|
209
|
+
background: var(--axi-ground);
|
|
210
|
+
border: var(--axi-border-control) solid var(--axi-ink-line);
|
|
211
|
+
color: var(--axi-text-dim);
|
|
212
|
+
font-size: 15px; font-weight: 900; cursor: pointer;
|
|
213
|
+
transition: transform .1s, box-shadow .1s;
|
|
214
|
+
}
|
|
215
|
+
.axi-drawer__close:hover {
|
|
216
|
+
background: var(--axi-accent); color: var(--axi-accent-ink);
|
|
217
|
+
box-shadow: var(--axi-offset-control) var(--axi-offset-control) 0 var(--axi-ink-line);
|
|
218
|
+
transform: translate(-2px, -2px);
|
|
219
|
+
}
|
|
220
|
+
.axi-drawer__body { overflow-y: auto; padding: 18px 24px 30px; }
|
|
221
|
+
|
|
222
|
+
/* ---------- pull-quote ---------- */
|
|
223
|
+
/* A cited quotation. The accent rule down the left is the only place in the
|
|
224
|
+
language where colour marks a block rather than filling a chip - a quote is
|
|
225
|
+
someone else's words and wants to look borrowed. */
|
|
226
|
+
.axi-quote {
|
|
227
|
+
margin: 0;
|
|
228
|
+
padding: 11px 13px;
|
|
229
|
+
border-left: var(--axi-border-panel) solid var(--axi-accent);
|
|
230
|
+
background: var(--axi-ground);
|
|
231
|
+
border-radius: 0 var(--axi-radius-sm) var(--axi-radius-sm) 0;
|
|
232
|
+
}
|
|
233
|
+
.axi-quote p { margin: 0; font-size: 13px; font-style: italic; color: var(--axi-text-dim); line-height: 1.5; }
|
|
234
|
+
.axi-quote cite {
|
|
235
|
+
display: block; margin-top: 7px;
|
|
236
|
+
font-style: normal; font-size: 11.5px; font-weight: 700;
|
|
237
|
+
color: var(--axi-accent);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/* ---------- window ---------- */
|
|
241
|
+
/* Chrome for a frameless desktop app, where the app is not a page inside a
|
|
242
|
+
browser but the outermost thing on the screen. Rule 3 says a raised
|
|
243
|
+
element is outlined; a window is the most raised element there is, so it
|
|
244
|
+
takes the panel outline. It carries no block, because a block is an
|
|
245
|
+
element's shadow on the surface behind it and there is nothing behind a
|
|
246
|
+
window that this language is entitled to draw on.
|
|
247
|
+
The titlebar is filled with the ink line rather than a surface: it is the
|
|
248
|
+
outline widened into a strip, which is why the app's content reads as
|
|
249
|
+
sitting inside the outline instead of under a second toolbar. */
|
|
250
|
+
.axi-window {
|
|
251
|
+
height: 100vh;
|
|
252
|
+
display: flex; flex-direction: column; overflow: hidden;
|
|
253
|
+
background: var(--axi-ground);
|
|
254
|
+
border: var(--axi-border-panel) solid var(--axi-ink-line);
|
|
255
|
+
border-radius: var(--axi-radius);
|
|
256
|
+
}
|
|
257
|
+
.axi-titlebar {
|
|
258
|
+
flex: none;
|
|
259
|
+
display: flex; align-items: center; gap: 12px;
|
|
260
|
+
height: 38px; padding-left: 14px;
|
|
261
|
+
background: var(--axi-ink-line);
|
|
262
|
+
border-bottom: var(--axi-border-control) solid var(--axi-ink-line);
|
|
263
|
+
font: var(--axi-t-micro);
|
|
264
|
+
letter-spacing: var(--axi-ls-micro);
|
|
265
|
+
text-transform: uppercase;
|
|
266
|
+
color: var(--axi-text-faint);
|
|
267
|
+
/* The strip is the drag handle; the buttons opt back out in Electron. */
|
|
268
|
+
-webkit-app-region: drag;
|
|
269
|
+
}
|
|
270
|
+
.axi-titlebar__btns { margin-left: auto; display: flex; height: 100%; -webkit-app-region: no-drag; }
|
|
271
|
+
.axi-titlebar__btns button {
|
|
272
|
+
width: 44px;
|
|
273
|
+
border: 0;
|
|
274
|
+
background: transparent;
|
|
275
|
+
color: var(--axi-text-faint);
|
|
276
|
+
font: var(--axi-t-label);
|
|
277
|
+
cursor: pointer;
|
|
278
|
+
}
|
|
279
|
+
/* No lift here. A window button is chrome, not content: it has nothing to
|
|
280
|
+
lift off, and 44px of movement in the corner of every screen is exactly
|
|
281
|
+
the scattered motion rule 4 rations. It answers on the neutral ramp. */
|
|
282
|
+
.axi-titlebar__btns button:hover { background: var(--axi-surface); color: var(--axi-text); }
|
|
283
|
+
/* Close is the one destructive control in the chrome, and the one place a
|
|
284
|
+
status ink belongs in a titlebar. */
|
|
285
|
+
.axi-titlebar__btns button:last-child:hover { background: var(--axi-danger); color: var(--axi-ink-line); }
|
|
286
|
+
/* The scrolling region under the chrome. */
|
|
287
|
+
.axi-window__body { flex: 1; overflow-y: auto; }
|
|
288
|
+
|
|
289
|
+
@media (max-width: 640px) {
|
|
290
|
+
.axi-mast__in { min-height: 0; padding: 10px 16px; gap: 12px; }
|
|
291
|
+
.axi-tabs { margin-left: 0; width: 100%; overflow-x: auto; }
|
|
292
|
+
/* The offset blocks eat horizontal room a phone does not have, so the panel
|
|
293
|
+
step drops to the control step here rather than to an invented number.
|
|
294
|
+
The card's hover has to come down with it - left alone it kept deepening
|
|
295
|
+
to the panel hover block, so a card on a hover-emulating touch device
|
|
296
|
+
jumped further than it does on the desktop it was tuned for. */
|
|
297
|
+
.axi-notice, .axi-toolbar, .axi-card {
|
|
298
|
+
box-shadow: var(--axi-offset-control) var(--axi-offset-control) 0 var(--axi-ink-line);
|
|
299
|
+
}
|
|
300
|
+
.axi-card:hover {
|
|
301
|
+
box-shadow: var(--axi-offset-control-hover) var(--axi-offset-control-hover) 0 var(--axi-ink-line);
|
|
302
|
+
}
|
|
303
|
+
}
|