@mystravil/hardpoint 0.2.0 → 0.2.1
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/CHANGELOG.md +42 -38
- package/LICENSE +25 -25
- package/README.md +67 -64
- package/dist/base.css +625 -625
- package/dist/components/AppShell.svelte +698 -698
- package/dist/components/Button.svelte +174 -174
- package/dist/components/Checkbox.svelte +235 -235
- package/dist/components/CheckboxGroup.svelte +207 -207
- package/dist/components/Combobox.svelte +901 -901
- package/dist/components/Dialog.svelte +359 -359
- package/dist/components/Disclosure.svelte +201 -201
- package/dist/components/EmptyState.svelte +88 -88
- package/dist/components/ErrorState.svelte +135 -135
- package/dist/components/ImageViewer.svelte +764 -764
- package/dist/components/KeyValueList.svelte +114 -114
- package/dist/components/ListDetail.svelte +218 -218
- package/dist/components/LoadRegion.svelte +219 -219
- package/dist/components/MediaFrame.svelte +110 -110
- package/dist/components/MediaImage.svelte +256 -256
- package/dist/components/Menu.svelte +258 -258
- package/dist/components/MenuCheckboxItem.svelte +119 -119
- package/dist/components/MenuGroup.svelte +50 -50
- package/dist/components/MenuItem.svelte +94 -94
- package/dist/components/MenuRadioGroup.svelte +62 -62
- package/dist/components/MenuRadioItem.svelte +110 -110
- package/dist/components/MenuSeparator.svelte +28 -28
- package/dist/components/Meter.svelte +283 -283
- package/dist/components/NativeSelect.svelte +135 -135
- package/dist/components/Notice.svelte +262 -262
- package/dist/components/Popover.svelte +222 -222
- package/dist/components/Progress.svelte +157 -157
- package/dist/components/RadioGroup.svelte +440 -440
- package/dist/components/Select.svelte +471 -471
- package/dist/components/Skeleton.svelte +149 -149
- package/dist/components/Spinner.svelte +118 -118
- package/dist/components/Stat.svelte +253 -253
- package/dist/components/Steps.svelte +189 -189
- package/dist/components/Switch.svelte +222 -222
- package/dist/components/Table.svelte +504 -504
- package/dist/components/Tabs.svelte +246 -246
- package/dist/components/Tag.svelte +40 -40
- package/dist/components/TextArea.svelte +272 -272
- package/dist/components/TextField.svelte +256 -256
- package/dist/components/Timeline.svelte +156 -156
- package/dist/components/Toaster.svelte +411 -411
- package/dist/components/Tooltip.svelte +200 -200
- package/dist/components/TooltipGroup.svelte +46 -46
- package/dist/components/internal/Scrim.svelte +52 -52
- package/dist/components/internal/Sheet.svelte +263 -263
- package/dist/fonts.css +71 -71
- package/dist/tokens.css +283 -283
- package/package.json +81 -76
package/dist/base.css
CHANGED
|
@@ -1,625 +1,625 @@
|
|
|
1
|
-
/* Hardpoint base. Framework-free like tokens.css: element defaults use
|
|
2
|
-
:where() so any app rule overrides them, and helpers carry an hp- prefix.
|
|
3
|
-
Import after tokens.css. */
|
|
4
|
-
|
|
5
|
-
/* ---- Document ---------------------------------------------------------- */
|
|
6
|
-
|
|
7
|
-
*,
|
|
8
|
-
*::before,
|
|
9
|
-
*::after {
|
|
10
|
-
box-sizing: border-box;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/* hidden="until-found" (Disclosure's closed content) stays out of this rule:
|
|
14
|
-
the browser hides it with content-visibility so find in page can search it. */
|
|
15
|
-
[hidden]:not([hidden='until-found' i]) {
|
|
16
|
-
display: none !important;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
html {
|
|
20
|
-
-webkit-text-size-adjust: 100%;
|
|
21
|
-
text-size-adjust: 100%;
|
|
22
|
-
scrollbar-color: var(--border) transparent;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
body {
|
|
26
|
-
margin: 0;
|
|
27
|
-
background: var(--canvas);
|
|
28
|
-
color: var(--text);
|
|
29
|
-
font-family: var(--sans);
|
|
30
|
-
font-size: var(--type-body);
|
|
31
|
-
font-weight: 400;
|
|
32
|
-
line-height: 1.5;
|
|
33
|
-
-webkit-font-smoothing: antialiased;
|
|
34
|
-
-moz-osx-font-smoothing: grayscale;
|
|
35
|
-
text-rendering: optimizeLegibility;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
:where(h1, h2, h3, h4, h5, h6, p, figure, blockquote, dl, dd) {
|
|
39
|
-
margin: 0;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
:where(h1, h2, h3, h4, h5, h6) {
|
|
43
|
-
font-size: inherit;
|
|
44
|
-
font-weight: 500;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/* Line breaks. Reading text never ends on a lone word; headings and title
|
|
48
|
-
roles balance their lines (this rule comes second, so a title set on a
|
|
49
|
-
<p> balances). The longhand leaves the wrap mode alone, so a nowrap or
|
|
50
|
-
truncating parent still holds; browsers without it wrap as before. */
|
|
51
|
-
:where(p, li, dd, blockquote, figcaption) {
|
|
52
|
-
text-wrap-style: pretty;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
:where(h1, h2, h3, h4, h5, h6, .hp-page-title, .hp-section-title, .hp-display) {
|
|
56
|
-
text-wrap-style: balance;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/* Reading measure: a paragraph stops at --measure, about 70 characters. */
|
|
60
|
-
:where(p) {
|
|
61
|
-
max-width: var(--measure);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/* Square corners everywhere: no cuts, no radii, also on native controls. */
|
|
65
|
-
:where(button, input, select, textarea) {
|
|
66
|
-
font: inherit;
|
|
67
|
-
letter-spacing: inherit;
|
|
68
|
-
color: inherit;
|
|
69
|
-
border-radius: 0;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
:where(button, a, input, select, textarea, summary, label) {
|
|
73
|
-
touch-action: manipulation;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
:where(img, svg, video, canvas) {
|
|
77
|
-
display: block;
|
|
78
|
-
max-width: 100%;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
:where(a) {
|
|
82
|
-
color: inherit;
|
|
83
|
-
text-decoration-thickness: 1px;
|
|
84
|
-
text-underline-offset: 0.2em;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
:where(code, kbd, samp, pre) {
|
|
88
|
-
font-family: var(--mono);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/* Inline code in running text moves to the next line whole, so a token
|
|
92
|
-
never splits at a hyphen or a space (--control-height, aria-label).
|
|
93
|
-
One wider than the line wraps inside its own box: at a hyphen or space
|
|
94
|
-
first, anywhere if it must, so it never overflows at 320px. Only direct
|
|
95
|
-
children of reading blocks: an atomic box in a truncating cell (a table
|
|
96
|
-
dd) would vanish whole into the ellipsis, and a link's underline does
|
|
97
|
-
not reach into one. */
|
|
98
|
-
:where(p, li, blockquote, figcaption) > :where(code, kbd, samp) {
|
|
99
|
-
display: inline-block;
|
|
100
|
-
overflow-wrap: anywhere;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
::selection {
|
|
104
|
-
background: var(--text);
|
|
105
|
-
color: var(--canvas);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/* ---- Focus ------------------------------------------------------------- */
|
|
109
|
-
|
|
110
|
-
/* A 2px titanium contour 4px outside the control, square like everything
|
|
111
|
-
else. Selection never looks like this: selection is an edge marker and a
|
|
112
|
-
tint. */
|
|
113
|
-
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
|
|
114
|
-
outline: 2px solid var(--focus);
|
|
115
|
-
outline-offset: 4px;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/* For rows and items clipped by a scroll container: draw the contour inside. */
|
|
119
|
-
.hp-focus-inset:focus-visible {
|
|
120
|
-
outline-offset: -2px;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/* ---- Type roles -------------------------------------------------------- */
|
|
124
|
-
|
|
125
|
-
/* Tracked caps for short fixed labels: buttons, tabs, tags, column headers,
|
|
126
|
-
field labels, key-value labels. Never for headings, list items, messages or
|
|
127
|
-
user data. */
|
|
128
|
-
.hp-label {
|
|
129
|
-
font-family: var(--sans);
|
|
130
|
-
font-size: var(--type-label);
|
|
131
|
-
font-weight: 500;
|
|
132
|
-
line-height: 16px;
|
|
133
|
-
letter-spacing: var(--label-tracking);
|
|
134
|
-
text-transform: var(--label-transform);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
.hp-caption {
|
|
138
|
-
font-size: var(--type-caption);
|
|
139
|
-
line-height: 18px;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/* Dense UI on fine pointers. Phones keep 16px body text. */
|
|
143
|
-
.hp-dense {
|
|
144
|
-
font-size: var(--type-body);
|
|
145
|
-
line-height: 24px;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
@media (pointer: fine) {
|
|
149
|
-
.hp-dense {
|
|
150
|
-
font-size: var(--type-dense);
|
|
151
|
-
line-height: 20px;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
.hp-body {
|
|
156
|
-
font-size: var(--type-body);
|
|
157
|
-
line-height: 24px;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
.hp-section-title {
|
|
161
|
-
font-size: var(--type-section);
|
|
162
|
-
font-weight: 500;
|
|
163
|
-
line-height: 28px;
|
|
164
|
-
letter-spacing: -0.01em;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
.hp-page-title {
|
|
168
|
-
font-size: var(--type-page);
|
|
169
|
-
font-weight: 500;
|
|
170
|
-
line-height: 1.25;
|
|
171
|
-
letter-spacing: -0.015em;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/* The view title in the shell header: a fixed string the app defines, so it
|
|
175
|
-
takes tracked caps (20px, 16px on compact). */
|
|
176
|
-
.hp-view-title {
|
|
177
|
-
font-family: var(--sans);
|
|
178
|
-
font-size: var(--type-view);
|
|
179
|
-
font-weight: 500;
|
|
180
|
-
line-height: 24px;
|
|
181
|
-
letter-spacing: var(--label-tracking);
|
|
182
|
-
text-transform: var(--label-transform);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/* One per view, and only for the subject: an identifier (add .hp-mono) or a
|
|
186
|
-
key figure. Names a user wrote stay at the page-title size. */
|
|
187
|
-
.hp-display {
|
|
188
|
-
font-size: var(--type-display);
|
|
189
|
-
font-weight: 500;
|
|
190
|
-
line-height: round(up, 1.1em, 1px);
|
|
191
|
-
letter-spacing: -0.02em;
|
|
192
|
-
font-variant-numeric: tabular-nums;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/* Identifiers, hashes, timestamps, code. */
|
|
196
|
-
.hp-mono {
|
|
197
|
-
font-family: var(--mono);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/* Numbers in Geist that line up and do not change width as they tick. */
|
|
201
|
-
.hp-num {
|
|
202
|
-
font-variant-numeric: tabular-nums;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/* Geist Pixel Square: the wordmark and at most one short fixed display line
|
|
206
|
-
per screen. Never labels, values, body text, page titles or user data.
|
|
207
|
-
Its module lands on whole pixels only if its top edge does too: do not
|
|
208
|
-
centre it in a box whose free space is odd. */
|
|
209
|
-
.hp-pixel {
|
|
210
|
-
font-family: var(--pixel);
|
|
211
|
-
font-size: var(--type-pixel);
|
|
212
|
-
font-weight: 400;
|
|
213
|
-
line-height: var(--type-pixel-leading);
|
|
214
|
-
letter-spacing: 0;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
.hp-pixel-lg {
|
|
218
|
-
font-family: var(--pixel);
|
|
219
|
-
font-size: var(--type-pixel-lg);
|
|
220
|
-
font-weight: 400;
|
|
221
|
-
line-height: var(--type-pixel-lg-leading);
|
|
222
|
-
letter-spacing: 0;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/* ---- Wells ------------------------------------------------------------- */
|
|
226
|
-
|
|
227
|
-
/* The input box: a black well with a 3:1 boundary, never a filled block,
|
|
228
|
-
because a filled select reads as a button (BRIEF, visual rule 2). Text
|
|
229
|
-
is 16px at every width, or iOS zooms on focus. Native selects, rich
|
|
230
|
-
select triggers and text fields share it. line-height: normal makes the
|
|
231
|
-
line box the glyph box, which the well centres: a text field already
|
|
232
|
-
centres its text that way, and with the inherited 24px line a select or
|
|
233
|
-
a trigger set its text 1px higher at every pixel ratio. */
|
|
234
|
-
.hp-well {
|
|
235
|
-
appearance: none;
|
|
236
|
-
box-sizing: border-box;
|
|
237
|
-
height: var(--control-height);
|
|
238
|
-
padding: 0 var(--space-3);
|
|
239
|
-
border: 1px solid var(--border-control);
|
|
240
|
-
border-radius: 0;
|
|
241
|
-
background-color: var(--canvas);
|
|
242
|
-
color: var(--text);
|
|
243
|
-
font-family: var(--sans);
|
|
244
|
-
font-size: var(--type-body);
|
|
245
|
-
line-height: normal;
|
|
246
|
-
text-overflow: ellipsis;
|
|
247
|
-
transition: background-color var(--dur-hover) var(--settle);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/* A chevron at the end of a select's well. Wrap the well in
|
|
251
|
-
<span class="hp-well-chevron">: the chevron is the wrapper's ::after,
|
|
252
|
-
because a <select> draws no pseudo-elements and a background image cannot
|
|
253
|
-
rotate. It turns to point up while the list is open, with the list's own
|
|
254
|
-
timing: --dur-pop opening, --dur-pop-exit closing. A rich select's trigger
|
|
255
|
-
says it is open with aria-expanded; a native select matches :open where
|
|
256
|
-
the browser supports it (Chromium 133+). The wrapper sets no width; the
|
|
257
|
-
component sizes it. */
|
|
258
|
-
.hp-well-chevron {
|
|
259
|
-
position: relative;
|
|
260
|
-
display: block;
|
|
261
|
-
}
|
|
262
|
-
.hp-well-chevron > .hp-well {
|
|
263
|
-
padding-right: var(--space-10);
|
|
264
|
-
}
|
|
265
|
-
.hp-well-chevron::after {
|
|
266
|
-
content: '';
|
|
267
|
-
position: absolute;
|
|
268
|
-
top: 50%;
|
|
269
|
-
/* The well's 1px border, plus the inset a background image had. */
|
|
270
|
-
right: calc(var(--space-3) + 1px);
|
|
271
|
-
width: 16px;
|
|
272
|
-
height: 16px;
|
|
273
|
-
margin-top: -8px;
|
|
274
|
-
background-color: var(--muted);
|
|
275
|
-
mask: var(--select-chevron) center / 16px 16px no-repeat;
|
|
276
|
-
pointer-events: none;
|
|
277
|
-
transition: transform var(--dur-pop-exit) var(--settle);
|
|
278
|
-
}
|
|
279
|
-
.hp-well-chevron:has(> [aria-expanded='true'])::after {
|
|
280
|
-
transform: rotate(180deg);
|
|
281
|
-
transition-duration: var(--dur-pop);
|
|
282
|
-
}
|
|
283
|
-
/* Its own rule: a browser without :open drops only this one. */
|
|
284
|
-
.hp-well-chevron:has(> :open)::after {
|
|
285
|
-
transform: rotate(180deg);
|
|
286
|
-
transition-duration: var(--dur-pop);
|
|
287
|
-
}
|
|
288
|
-
@media (prefers-reduced-motion: reduce) {
|
|
289
|
-
.hp-well-chevron::after {
|
|
290
|
-
transition: none;
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
@media (hover: hover) and (pointer: fine) {
|
|
294
|
-
.hp-well:hover:not(:disabled, [aria-disabled='true']) {
|
|
295
|
-
background-color: var(--surface-hover);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
/* Disabled: muted text and the quieter --border, as on a disabled outlined
|
|
299
|
-
button. Chromium also sets a disabled <select> to 70% opacity; cancelled,
|
|
300
|
-
so a native and a rich select look the same when disabled. */
|
|
301
|
-
.hp-well:disabled,
|
|
302
|
-
.hp-well[aria-disabled='true'] {
|
|
303
|
-
border-color: var(--border);
|
|
304
|
-
color: var(--muted);
|
|
305
|
-
cursor: not-allowed;
|
|
306
|
-
opacity: 1;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
/* ---- Expressive entrance ---------------------------------------------- */
|
|
310
|
-
|
|
311
|
-
/* Stagger (motion rule 7): a component that renders its first items in the
|
|
312
|
-
expressive tier adds .hp-enter and sets --i to the item's index. Items
|
|
313
|
-
from the sixth on share the sixth's delay. Only on first appearance:
|
|
314
|
-
never on items added later by filtering, sorting or paging. */
|
|
315
|
-
.hp-enter {
|
|
316
|
-
animation: hp-enter var(--dur-enter) var(--settle) both;
|
|
317
|
-
animation-delay: calc(min(var(--i, 0), 5) * var(--stagger-step));
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
@keyframes hp-enter {
|
|
321
|
-
from {
|
|
322
|
-
opacity: 0;
|
|
323
|
-
transform: translateY(4px);
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
@media (prefers-reduced-motion: reduce) {
|
|
328
|
-
.hp-enter {
|
|
329
|
-
animation: none;
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
/* ---- Prose ------------------------------------------------------------- */
|
|
334
|
-
|
|
335
|
-
/* Rendered markdown or sanitised HTML that an app cannot scope styles into:
|
|
336
|
-
put .hp-prose on its container. Every rule is :where(.hp-prose) …, so it
|
|
337
|
-
has no specificity and any app rule wins. Sizes in em follow the font
|
|
338
|
-
size the app sets on the container (16px on 24px lines by default); the
|
|
339
|
-
two heading sizes are the type tokens. Blocks do not stop at --measure:
|
|
340
|
-
the container's width decides, so give it a readable one.
|
|
341
|
-
|
|
342
|
-
Gaps: 1em between blocks, 0.5em between a heading and what it
|
|
343
|
-
introduces, 0.5em between blocks inside a list item and 0.25em between
|
|
344
|
-
items, so each gap between blocks is at least twice the gap inside one.
|
|
345
|
-
|
|
346
|
-
The ink is --text, with --muted for markers, quotes and the small
|
|
347
|
-
headings. Nothing here is a hue. */
|
|
348
|
-
|
|
349
|
-
:where(.hp-prose) {
|
|
350
|
-
/* In a flex or grid column the box may shrink below a long word. */
|
|
351
|
-
min-width: 0;
|
|
352
|
-
font-family: var(--sans);
|
|
353
|
-
font-size: var(--type-body);
|
|
354
|
-
line-height: 1.5;
|
|
355
|
-
/* A long address or word breaks inside the column; tables and code
|
|
356
|
-
blocks set their own rules below. */
|
|
357
|
-
overflow-wrap: anywhere;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
/* Block gaps: space above each block, none below, so the first and last
|
|
361
|
-
child need no special case and a margin never doubles. */
|
|
362
|
-
:where(.hp-prose) :where(p, ul, ol, pre, blockquote, table, h1, h2, h3, h4, h5, h6, .hp-prose-code, .hp-prose-scroll) {
|
|
363
|
-
margin-block: 1em 0;
|
|
364
|
-
margin-inline: 0;
|
|
365
|
-
}
|
|
366
|
-
:where(.hp-prose) :where(p) {
|
|
367
|
-
max-width: none;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
/* Headings as written, never caps. h1 and h2 take the page-title and
|
|
371
|
-
section-title roles; h3 to h6 stay at body size and step down in weight,
|
|
372
|
-
then in ink. More space above a heading than below it groups it with what
|
|
373
|
-
follows. */
|
|
374
|
-
:where(.hp-prose) :where(h1) {
|
|
375
|
-
margin-block-start: 1.5em;
|
|
376
|
-
font-size: var(--type-page);
|
|
377
|
-
font-weight: 500;
|
|
378
|
-
line-height: 1.25;
|
|
379
|
-
line-height: round(up, 1.25em, 4px);
|
|
380
|
-
letter-spacing: -0.015em;
|
|
381
|
-
}
|
|
382
|
-
:where(.hp-prose) :where(h2) {
|
|
383
|
-
margin-block-start: 1.6em;
|
|
384
|
-
font-size: var(--type-section);
|
|
385
|
-
font-weight: 500;
|
|
386
|
-
line-height: 1.25;
|
|
387
|
-
line-height: round(up, 1.25em, 4px);
|
|
388
|
-
letter-spacing: -0.01em;
|
|
389
|
-
}
|
|
390
|
-
:where(.hp-prose) :where(h3, h4, h5, h6) {
|
|
391
|
-
margin-block-start: 1.5em;
|
|
392
|
-
font-size: 1em;
|
|
393
|
-
font-weight: 500;
|
|
394
|
-
}
|
|
395
|
-
:where(.hp-prose) :where(h3) {
|
|
396
|
-
font-weight: 600;
|
|
397
|
-
}
|
|
398
|
-
:where(.hp-prose) :where(h5, h6) {
|
|
399
|
-
color: var(--muted);
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
:where(.hp-prose) :where(strong, b) {
|
|
403
|
-
font-weight: 600;
|
|
404
|
-
}
|
|
405
|
-
:where(.hp-prose) :where(del, s) {
|
|
406
|
-
text-decoration-line: line-through;
|
|
407
|
-
text-decoration-thickness: 1px;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
/* Lists: square markers on bulleted lists (square corners everywhere),
|
|
411
|
-
tabular figures on numbered ones, both in --muted. The markers hang
|
|
412
|
-
outside the text, so the text edge lines up with the paragraphs. A
|
|
413
|
-
start number comes from the list's own attribute. */
|
|
414
|
-
:where(.hp-prose) :where(ul, ol) {
|
|
415
|
-
padding-inline-start: 1.5em;
|
|
416
|
-
}
|
|
417
|
-
:where(.hp-prose) :where(ol) {
|
|
418
|
-
padding-inline-start: 2em;
|
|
419
|
-
list-style-type: decimal;
|
|
420
|
-
}
|
|
421
|
-
:where(.hp-prose) :where(ul) {
|
|
422
|
-
list-style-type: square;
|
|
423
|
-
}
|
|
424
|
-
:where(.hp-prose li)::marker {
|
|
425
|
-
color: var(--muted);
|
|
426
|
-
}
|
|
427
|
-
:where(.hp-prose ol > li)::marker {
|
|
428
|
-
font-variant-numeric: tabular-nums;
|
|
429
|
-
}
|
|
430
|
-
/* Tight inside a list: items 0.25em apart, a nested list 0.25em under its
|
|
431
|
-
item's text, other blocks in an item 0.5em apart. */
|
|
432
|
-
:where(.hp-prose li) + :where(li) {
|
|
433
|
-
margin-block-start: 0.25em;
|
|
434
|
-
}
|
|
435
|
-
:where(.hp-prose li > * + *) {
|
|
436
|
-
margin-block-start: 0.5em;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
/* Task lists. A checkbox in an item is drawn as Hardpoint's checkbox box
|
|
440
|
-
in its disabled form: a black box with the quiet --border edge and a
|
|
441
|
-
--muted mark, and it takes the marker's place. It is out of the flow,
|
|
442
|
-
so the item's text keeps the list's text edge. */
|
|
443
|
-
:where(.hp-prose li:has(> input[type='checkbox'], > p:first-child > input[type='checkbox'])) {
|
|
444
|
-
position: relative;
|
|
445
|
-
list-style: none;
|
|
446
|
-
}
|
|
447
|
-
:where(.hp-prose li input[type='checkbox']) {
|
|
448
|
-
appearance: none;
|
|
449
|
-
position: absolute;
|
|
450
|
-
inset-block-start: calc((1lh - 1.25em) / 2);
|
|
451
|
-
inset-inline-start: -1.5em;
|
|
452
|
-
display: grid;
|
|
453
|
-
place-items: center;
|
|
454
|
-
box-sizing: border-box;
|
|
455
|
-
width: 1.25em;
|
|
456
|
-
height: 1.25em;
|
|
457
|
-
margin: 0;
|
|
458
|
-
padding: 0;
|
|
459
|
-
border: 1px solid var(--border);
|
|
460
|
-
border-radius: 0;
|
|
461
|
-
background: var(--canvas);
|
|
462
|
-
color: var(--muted);
|
|
463
|
-
}
|
|
464
|
-
/* The mark is the box's 12px check as a clipped square, so it takes the
|
|
465
|
-
text colour and needs no image. */
|
|
466
|
-
:where(.hp-prose li input[type='checkbox']:checked)::before {
|
|
467
|
-
content: '';
|
|
468
|
-
width: 0.75em;
|
|
469
|
-
height: 0.75em;
|
|
470
|
-
background: currentColor;
|
|
471
|
-
clip-path: polygon(6.6% 60.1%, 37.5% 91%, 93.4% 35.1%, 81.6% 23.3%, 37.5% 67.4%, 18.4% 48.3%);
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
/* Quotes: a 3px edge in --border, since titanium marks selection, and the
|
|
475
|
-
text in --muted. */
|
|
476
|
-
:where(.hp-prose) :where(blockquote) {
|
|
477
|
-
padding-inline-start: 0.75em;
|
|
478
|
-
border-inline-start: 3px solid var(--border);
|
|
479
|
-
color: var(--muted);
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
:where(.hp-prose) :where(hr) {
|
|
483
|
-
height: 0;
|
|
484
|
-
margin-block: 1.5em;
|
|
485
|
-
margin-inline: 0;
|
|
486
|
-
border: 0;
|
|
487
|
-
border-block-start: 1px solid var(--border);
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
/* A markdown image sits in a paragraph, which has its own gap; one that
|
|
491
|
-
sits directly in the container gets the block gap too. */
|
|
492
|
-
:where(.hp-prose) :where(img) {
|
|
493
|
-
display: block;
|
|
494
|
-
max-width: 100%;
|
|
495
|
-
height: auto;
|
|
496
|
-
}
|
|
497
|
-
:where(.hp-prose) > :where(img) {
|
|
498
|
-
margin-block-start: 1em;
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
/* Code. Inline code is a little smaller than the text around it, because
|
|
502
|
-
Geist Mono looks larger at the same size. A block is Geist Mono on a 20px
|
|
503
|
-
rhythm, never wraps and scrolls sideways inside its own box. It has a
|
|
504
|
-
1px --border hairline instead of a fill: --surface-hover cannot be seen
|
|
505
|
-
on black, --surface-band is heavy under a long listing, and the line
|
|
506
|
-
reads in both themes and marks where the scrolling area ends. */
|
|
507
|
-
:where(.hp-prose) :where(code, kbd, samp) {
|
|
508
|
-
font-size: 0.875em;
|
|
509
|
-
}
|
|
510
|
-
:where(.hp-prose) :where(pre) {
|
|
511
|
-
padding: var(--space-3);
|
|
512
|
-
overflow-x: auto;
|
|
513
|
-
overscroll-behavior-x: contain;
|
|
514
|
-
border: 1px solid var(--border);
|
|
515
|
-
font-size: 0.875em;
|
|
516
|
-
line-height: calc(20 / 14);
|
|
517
|
-
white-space: pre;
|
|
518
|
-
overflow-wrap: normal;
|
|
519
|
-
tab-size: 4;
|
|
520
|
-
}
|
|
521
|
-
:where(.hp-prose pre) :where(code, kbd, samp) {
|
|
522
|
-
font-size: inherit;
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
/* A code block with a copy button: wrap the pre and the app's Button
|
|
526
|
-
(class hp-prose-copy) in .hp-prose-code. The wrapper draws the hairline and
|
|
527
|
-
the button sits at the top end, outside the text, so a renderer that
|
|
528
|
-
replaces the pre keeps it. The pre leaves a column for the button, so
|
|
529
|
-
text, which scrolls sideways under any overlay, never runs beneath it. */
|
|
530
|
-
:where(.hp-prose .hp-prose-code) {
|
|
531
|
-
position: relative;
|
|
532
|
-
border: 1px solid var(--border);
|
|
533
|
-
}
|
|
534
|
-
:where(.hp-prose .hp-prose-code > pre) {
|
|
535
|
-
margin: 0;
|
|
536
|
-
margin-inline-end: calc(var(--control-height) + var(--space-2));
|
|
537
|
-
border: 0;
|
|
538
|
-
}
|
|
539
|
-
:where(.hp-prose .hp-prose-copy) {
|
|
540
|
-
position: absolute;
|
|
541
|
-
inset-block-start: var(--space-1);
|
|
542
|
-
inset-inline-end: var(--space-1);
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
/* Tables. The header band is --surface-band with cells as written, never
|
|
546
|
-
caps. Cells are Geist, not Geist Mono: they hold words. Text that wraps
|
|
547
|
-
makes rows of several lines, and without a line between rows the eye
|
|
548
|
-
cannot tell where one ends, so every row has a 1px --border divider (the
|
|
549
|
-
table's ruled form), and no zebra. A table is as wide as its content or
|
|
550
|
-
its box, whichever is wider; wrap each in .hp-prose-scroll and a table
|
|
551
|
-
wider than the column scrolls sideways inside it. Alignment comes from
|
|
552
|
-
the cell's align attribute or its own text-align. */
|
|
553
|
-
:where(.hp-prose .hp-prose-scroll) {
|
|
554
|
-
max-width: 100%;
|
|
555
|
-
overflow-x: auto;
|
|
556
|
-
overscroll-behavior-x: contain;
|
|
557
|
-
}
|
|
558
|
-
:where(.hp-prose) :where(table) {
|
|
559
|
-
min-width: 100%;
|
|
560
|
-
border-collapse: collapse;
|
|
561
|
-
font-variant-numeric: tabular-nums;
|
|
562
|
-
}
|
|
563
|
-
:where(.hp-prose) :where(th, td) {
|
|
564
|
-
padding: var(--space-2) var(--space-3);
|
|
565
|
-
border-block-end: 1px solid var(--border);
|
|
566
|
-
overflow-wrap: normal;
|
|
567
|
-
text-align: start;
|
|
568
|
-
vertical-align: top;
|
|
569
|
-
}
|
|
570
|
-
:where(.hp-prose th) {
|
|
571
|
-
border-block-end: 0;
|
|
572
|
-
background: var(--surface-band);
|
|
573
|
-
font-weight: 500;
|
|
574
|
-
}
|
|
575
|
-
/* The cell's align attribute: the browser maps it to text-align at the
|
|
576
|
-
lowest priority, so the start alignment above would hide it. */
|
|
577
|
-
:where(.hp-prose :is(th, td)[align='left']) {
|
|
578
|
-
text-align: left;
|
|
579
|
-
}
|
|
580
|
-
:where(.hp-prose :is(th, td)[align='center']) {
|
|
581
|
-
text-align: center;
|
|
582
|
-
}
|
|
583
|
-
:where(.hp-prose :is(th, td)[align='right']) {
|
|
584
|
-
text-align: right;
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
@media (pointer: fine) {
|
|
588
|
-
:where(.hp-prose pre, .hp-prose .hp-prose-scroll) {
|
|
589
|
-
scrollbar-width: thin;
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
/* Gaps that depend on neighbours come last, so they win over the block gap:
|
|
594
|
-
a heading keeps 0.5em from what it introduces, and the first and last
|
|
595
|
-
child of any box touch its edges. */
|
|
596
|
-
:where(.hp-prose) :where(h1, h2, h3, h4, h5, h6) + * {
|
|
597
|
-
margin-block-start: 0.5em;
|
|
598
|
-
}
|
|
599
|
-
:where(.hp-prose :first-child) {
|
|
600
|
-
margin-block-start: 0;
|
|
601
|
-
}
|
|
602
|
-
:where(.hp-prose :last-child) {
|
|
603
|
-
margin-block-end: 0;
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
/* A nested list follows its item's text, which is not an element, so it is
|
|
607
|
-
that item's first child: it takes its 0.25em back after the rule above. */
|
|
608
|
-
:where(.hp-prose li > :is(ul, ol)) {
|
|
609
|
-
margin-block-start: 0.25em;
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
/* ---- Utilities --------------------------------------------------------- */
|
|
613
|
-
|
|
614
|
-
/* Hidden from sight, kept in the accessibility tree. */
|
|
615
|
-
.hp-visually-hidden {
|
|
616
|
-
position: absolute;
|
|
617
|
-
width: 1px;
|
|
618
|
-
height: 1px;
|
|
619
|
-
margin: -1px;
|
|
620
|
-
padding: 0;
|
|
621
|
-
overflow: hidden;
|
|
622
|
-
clip-path: inset(50%);
|
|
623
|
-
white-space: nowrap;
|
|
624
|
-
border: 0;
|
|
625
|
-
}
|
|
1
|
+
/* Hardpoint base. Framework-free like tokens.css: element defaults use
|
|
2
|
+
:where() so any app rule overrides them, and helpers carry an hp- prefix.
|
|
3
|
+
Import after tokens.css. */
|
|
4
|
+
|
|
5
|
+
/* ---- Document ---------------------------------------------------------- */
|
|
6
|
+
|
|
7
|
+
*,
|
|
8
|
+
*::before,
|
|
9
|
+
*::after {
|
|
10
|
+
box-sizing: border-box;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/* hidden="until-found" (Disclosure's closed content) stays out of this rule:
|
|
14
|
+
the browser hides it with content-visibility so find in page can search it. */
|
|
15
|
+
[hidden]:not([hidden='until-found' i]) {
|
|
16
|
+
display: none !important;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
html {
|
|
20
|
+
-webkit-text-size-adjust: 100%;
|
|
21
|
+
text-size-adjust: 100%;
|
|
22
|
+
scrollbar-color: var(--border) transparent;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
body {
|
|
26
|
+
margin: 0;
|
|
27
|
+
background: var(--canvas);
|
|
28
|
+
color: var(--text);
|
|
29
|
+
font-family: var(--sans);
|
|
30
|
+
font-size: var(--type-body);
|
|
31
|
+
font-weight: 400;
|
|
32
|
+
line-height: 1.5;
|
|
33
|
+
-webkit-font-smoothing: antialiased;
|
|
34
|
+
-moz-osx-font-smoothing: grayscale;
|
|
35
|
+
text-rendering: optimizeLegibility;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
:where(h1, h2, h3, h4, h5, h6, p, figure, blockquote, dl, dd) {
|
|
39
|
+
margin: 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
:where(h1, h2, h3, h4, h5, h6) {
|
|
43
|
+
font-size: inherit;
|
|
44
|
+
font-weight: 500;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* Line breaks. Reading text never ends on a lone word; headings and title
|
|
48
|
+
roles balance their lines (this rule comes second, so a title set on a
|
|
49
|
+
<p> balances). The longhand leaves the wrap mode alone, so a nowrap or
|
|
50
|
+
truncating parent still holds; browsers without it wrap as before. */
|
|
51
|
+
:where(p, li, dd, blockquote, figcaption) {
|
|
52
|
+
text-wrap-style: pretty;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
:where(h1, h2, h3, h4, h5, h6, .hp-page-title, .hp-section-title, .hp-display) {
|
|
56
|
+
text-wrap-style: balance;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* Reading measure: a paragraph stops at --measure, about 70 characters. */
|
|
60
|
+
:where(p) {
|
|
61
|
+
max-width: var(--measure);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* Square corners everywhere: no cuts, no radii, also on native controls. */
|
|
65
|
+
:where(button, input, select, textarea) {
|
|
66
|
+
font: inherit;
|
|
67
|
+
letter-spacing: inherit;
|
|
68
|
+
color: inherit;
|
|
69
|
+
border-radius: 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
:where(button, a, input, select, textarea, summary, label) {
|
|
73
|
+
touch-action: manipulation;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
:where(img, svg, video, canvas) {
|
|
77
|
+
display: block;
|
|
78
|
+
max-width: 100%;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
:where(a) {
|
|
82
|
+
color: inherit;
|
|
83
|
+
text-decoration-thickness: 1px;
|
|
84
|
+
text-underline-offset: 0.2em;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
:where(code, kbd, samp, pre) {
|
|
88
|
+
font-family: var(--mono);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* Inline code in running text moves to the next line whole, so a token
|
|
92
|
+
never splits at a hyphen or a space (--control-height, aria-label).
|
|
93
|
+
One wider than the line wraps inside its own box: at a hyphen or space
|
|
94
|
+
first, anywhere if it must, so it never overflows at 320px. Only direct
|
|
95
|
+
children of reading blocks: an atomic box in a truncating cell (a table
|
|
96
|
+
dd) would vanish whole into the ellipsis, and a link's underline does
|
|
97
|
+
not reach into one. */
|
|
98
|
+
:where(p, li, blockquote, figcaption) > :where(code, kbd, samp) {
|
|
99
|
+
display: inline-block;
|
|
100
|
+
overflow-wrap: anywhere;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
::selection {
|
|
104
|
+
background: var(--text);
|
|
105
|
+
color: var(--canvas);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/* ---- Focus ------------------------------------------------------------- */
|
|
109
|
+
|
|
110
|
+
/* A 2px titanium contour 4px outside the control, square like everything
|
|
111
|
+
else. Selection never looks like this: selection is an edge marker and a
|
|
112
|
+
tint. */
|
|
113
|
+
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
|
|
114
|
+
outline: 2px solid var(--focus);
|
|
115
|
+
outline-offset: 4px;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* For rows and items clipped by a scroll container: draw the contour inside. */
|
|
119
|
+
.hp-focus-inset:focus-visible {
|
|
120
|
+
outline-offset: -2px;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/* ---- Type roles -------------------------------------------------------- */
|
|
124
|
+
|
|
125
|
+
/* Tracked caps for short fixed labels: buttons, tabs, tags, column headers,
|
|
126
|
+
field labels, key-value labels. Never for headings, list items, messages or
|
|
127
|
+
user data. */
|
|
128
|
+
.hp-label {
|
|
129
|
+
font-family: var(--sans);
|
|
130
|
+
font-size: var(--type-label);
|
|
131
|
+
font-weight: 500;
|
|
132
|
+
line-height: 16px;
|
|
133
|
+
letter-spacing: var(--label-tracking);
|
|
134
|
+
text-transform: var(--label-transform);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.hp-caption {
|
|
138
|
+
font-size: var(--type-caption);
|
|
139
|
+
line-height: 18px;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/* Dense UI on fine pointers. Phones keep 16px body text. */
|
|
143
|
+
.hp-dense {
|
|
144
|
+
font-size: var(--type-body);
|
|
145
|
+
line-height: 24px;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
@media (pointer: fine) {
|
|
149
|
+
.hp-dense {
|
|
150
|
+
font-size: var(--type-dense);
|
|
151
|
+
line-height: 20px;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.hp-body {
|
|
156
|
+
font-size: var(--type-body);
|
|
157
|
+
line-height: 24px;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.hp-section-title {
|
|
161
|
+
font-size: var(--type-section);
|
|
162
|
+
font-weight: 500;
|
|
163
|
+
line-height: 28px;
|
|
164
|
+
letter-spacing: -0.01em;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.hp-page-title {
|
|
168
|
+
font-size: var(--type-page);
|
|
169
|
+
font-weight: 500;
|
|
170
|
+
line-height: 1.25;
|
|
171
|
+
letter-spacing: -0.015em;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/* The view title in the shell header: a fixed string the app defines, so it
|
|
175
|
+
takes tracked caps (20px, 16px on compact). */
|
|
176
|
+
.hp-view-title {
|
|
177
|
+
font-family: var(--sans);
|
|
178
|
+
font-size: var(--type-view);
|
|
179
|
+
font-weight: 500;
|
|
180
|
+
line-height: 24px;
|
|
181
|
+
letter-spacing: var(--label-tracking);
|
|
182
|
+
text-transform: var(--label-transform);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/* One per view, and only for the subject: an identifier (add .hp-mono) or a
|
|
186
|
+
key figure. Names a user wrote stay at the page-title size. */
|
|
187
|
+
.hp-display {
|
|
188
|
+
font-size: var(--type-display);
|
|
189
|
+
font-weight: 500;
|
|
190
|
+
line-height: round(up, 1.1em, 1px);
|
|
191
|
+
letter-spacing: -0.02em;
|
|
192
|
+
font-variant-numeric: tabular-nums;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/* Identifiers, hashes, timestamps, code. */
|
|
196
|
+
.hp-mono {
|
|
197
|
+
font-family: var(--mono);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/* Numbers in Geist that line up and do not change width as they tick. */
|
|
201
|
+
.hp-num {
|
|
202
|
+
font-variant-numeric: tabular-nums;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/* Geist Pixel Square: the wordmark and at most one short fixed display line
|
|
206
|
+
per screen. Never labels, values, body text, page titles or user data.
|
|
207
|
+
Its module lands on whole pixels only if its top edge does too: do not
|
|
208
|
+
centre it in a box whose free space is odd. */
|
|
209
|
+
.hp-pixel {
|
|
210
|
+
font-family: var(--pixel);
|
|
211
|
+
font-size: var(--type-pixel);
|
|
212
|
+
font-weight: 400;
|
|
213
|
+
line-height: var(--type-pixel-leading);
|
|
214
|
+
letter-spacing: 0;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.hp-pixel-lg {
|
|
218
|
+
font-family: var(--pixel);
|
|
219
|
+
font-size: var(--type-pixel-lg);
|
|
220
|
+
font-weight: 400;
|
|
221
|
+
line-height: var(--type-pixel-lg-leading);
|
|
222
|
+
letter-spacing: 0;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/* ---- Wells ------------------------------------------------------------- */
|
|
226
|
+
|
|
227
|
+
/* The input box: a black well with a 3:1 boundary, never a filled block,
|
|
228
|
+
because a filled select reads as a button (BRIEF, visual rule 2). Text
|
|
229
|
+
is 16px at every width, or iOS zooms on focus. Native selects, rich
|
|
230
|
+
select triggers and text fields share it. line-height: normal makes the
|
|
231
|
+
line box the glyph box, which the well centres: a text field already
|
|
232
|
+
centres its text that way, and with the inherited 24px line a select or
|
|
233
|
+
a trigger set its text 1px higher at every pixel ratio. */
|
|
234
|
+
.hp-well {
|
|
235
|
+
appearance: none;
|
|
236
|
+
box-sizing: border-box;
|
|
237
|
+
height: var(--control-height);
|
|
238
|
+
padding: 0 var(--space-3);
|
|
239
|
+
border: 1px solid var(--border-control);
|
|
240
|
+
border-radius: 0;
|
|
241
|
+
background-color: var(--canvas);
|
|
242
|
+
color: var(--text);
|
|
243
|
+
font-family: var(--sans);
|
|
244
|
+
font-size: var(--type-body);
|
|
245
|
+
line-height: normal;
|
|
246
|
+
text-overflow: ellipsis;
|
|
247
|
+
transition: background-color var(--dur-hover) var(--settle);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/* A chevron at the end of a select's well. Wrap the well in
|
|
251
|
+
<span class="hp-well-chevron">: the chevron is the wrapper's ::after,
|
|
252
|
+
because a <select> draws no pseudo-elements and a background image cannot
|
|
253
|
+
rotate. It turns to point up while the list is open, with the list's own
|
|
254
|
+
timing: --dur-pop opening, --dur-pop-exit closing. A rich select's trigger
|
|
255
|
+
says it is open with aria-expanded; a native select matches :open where
|
|
256
|
+
the browser supports it (Chromium 133+). The wrapper sets no width; the
|
|
257
|
+
component sizes it. */
|
|
258
|
+
.hp-well-chevron {
|
|
259
|
+
position: relative;
|
|
260
|
+
display: block;
|
|
261
|
+
}
|
|
262
|
+
.hp-well-chevron > .hp-well {
|
|
263
|
+
padding-right: var(--space-10);
|
|
264
|
+
}
|
|
265
|
+
.hp-well-chevron::after {
|
|
266
|
+
content: '';
|
|
267
|
+
position: absolute;
|
|
268
|
+
top: 50%;
|
|
269
|
+
/* The well's 1px border, plus the inset a background image had. */
|
|
270
|
+
right: calc(var(--space-3) + 1px);
|
|
271
|
+
width: 16px;
|
|
272
|
+
height: 16px;
|
|
273
|
+
margin-top: -8px;
|
|
274
|
+
background-color: var(--muted);
|
|
275
|
+
mask: var(--select-chevron) center / 16px 16px no-repeat;
|
|
276
|
+
pointer-events: none;
|
|
277
|
+
transition: transform var(--dur-pop-exit) var(--settle);
|
|
278
|
+
}
|
|
279
|
+
.hp-well-chevron:has(> [aria-expanded='true'])::after {
|
|
280
|
+
transform: rotate(180deg);
|
|
281
|
+
transition-duration: var(--dur-pop);
|
|
282
|
+
}
|
|
283
|
+
/* Its own rule: a browser without :open drops only this one. */
|
|
284
|
+
.hp-well-chevron:has(> :open)::after {
|
|
285
|
+
transform: rotate(180deg);
|
|
286
|
+
transition-duration: var(--dur-pop);
|
|
287
|
+
}
|
|
288
|
+
@media (prefers-reduced-motion: reduce) {
|
|
289
|
+
.hp-well-chevron::after {
|
|
290
|
+
transition: none;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
@media (hover: hover) and (pointer: fine) {
|
|
294
|
+
.hp-well:hover:not(:disabled, [aria-disabled='true']) {
|
|
295
|
+
background-color: var(--surface-hover);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
/* Disabled: muted text and the quieter --border, as on a disabled outlined
|
|
299
|
+
button. Chromium also sets a disabled <select> to 70% opacity; cancelled,
|
|
300
|
+
so a native and a rich select look the same when disabled. */
|
|
301
|
+
.hp-well:disabled,
|
|
302
|
+
.hp-well[aria-disabled='true'] {
|
|
303
|
+
border-color: var(--border);
|
|
304
|
+
color: var(--muted);
|
|
305
|
+
cursor: not-allowed;
|
|
306
|
+
opacity: 1;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/* ---- Expressive entrance ---------------------------------------------- */
|
|
310
|
+
|
|
311
|
+
/* Stagger (motion rule 7): a component that renders its first items in the
|
|
312
|
+
expressive tier adds .hp-enter and sets --i to the item's index. Items
|
|
313
|
+
from the sixth on share the sixth's delay. Only on first appearance:
|
|
314
|
+
never on items added later by filtering, sorting or paging. */
|
|
315
|
+
.hp-enter {
|
|
316
|
+
animation: hp-enter var(--dur-enter) var(--settle) both;
|
|
317
|
+
animation-delay: calc(min(var(--i, 0), 5) * var(--stagger-step));
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
@keyframes hp-enter {
|
|
321
|
+
from {
|
|
322
|
+
opacity: 0;
|
|
323
|
+
transform: translateY(4px);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
@media (prefers-reduced-motion: reduce) {
|
|
328
|
+
.hp-enter {
|
|
329
|
+
animation: none;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/* ---- Prose ------------------------------------------------------------- */
|
|
334
|
+
|
|
335
|
+
/* Rendered markdown or sanitised HTML that an app cannot scope styles into:
|
|
336
|
+
put .hp-prose on its container. Every rule is :where(.hp-prose) …, so it
|
|
337
|
+
has no specificity and any app rule wins. Sizes in em follow the font
|
|
338
|
+
size the app sets on the container (16px on 24px lines by default); the
|
|
339
|
+
two heading sizes are the type tokens. Blocks do not stop at --measure:
|
|
340
|
+
the container's width decides, so give it a readable one.
|
|
341
|
+
|
|
342
|
+
Gaps: 1em between blocks, 0.5em between a heading and what it
|
|
343
|
+
introduces, 0.5em between blocks inside a list item and 0.25em between
|
|
344
|
+
items, so each gap between blocks is at least twice the gap inside one.
|
|
345
|
+
|
|
346
|
+
The ink is --text, with --muted for markers, quotes and the small
|
|
347
|
+
headings. Nothing here is a hue. */
|
|
348
|
+
|
|
349
|
+
:where(.hp-prose) {
|
|
350
|
+
/* In a flex or grid column the box may shrink below a long word. */
|
|
351
|
+
min-width: 0;
|
|
352
|
+
font-family: var(--sans);
|
|
353
|
+
font-size: var(--type-body);
|
|
354
|
+
line-height: 1.5;
|
|
355
|
+
/* A long address or word breaks inside the column; tables and code
|
|
356
|
+
blocks set their own rules below. */
|
|
357
|
+
overflow-wrap: anywhere;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/* Block gaps: space above each block, none below, so the first and last
|
|
361
|
+
child need no special case and a margin never doubles. */
|
|
362
|
+
:where(.hp-prose) :where(p, ul, ol, pre, blockquote, table, h1, h2, h3, h4, h5, h6, .hp-prose-code, .hp-prose-scroll) {
|
|
363
|
+
margin-block: 1em 0;
|
|
364
|
+
margin-inline: 0;
|
|
365
|
+
}
|
|
366
|
+
:where(.hp-prose) :where(p) {
|
|
367
|
+
max-width: none;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/* Headings as written, never caps. h1 and h2 take the page-title and
|
|
371
|
+
section-title roles; h3 to h6 stay at body size and step down in weight,
|
|
372
|
+
then in ink. More space above a heading than below it groups it with what
|
|
373
|
+
follows. */
|
|
374
|
+
:where(.hp-prose) :where(h1) {
|
|
375
|
+
margin-block-start: 1.5em;
|
|
376
|
+
font-size: var(--type-page);
|
|
377
|
+
font-weight: 500;
|
|
378
|
+
line-height: 1.25;
|
|
379
|
+
line-height: round(up, 1.25em, 4px);
|
|
380
|
+
letter-spacing: -0.015em;
|
|
381
|
+
}
|
|
382
|
+
:where(.hp-prose) :where(h2) {
|
|
383
|
+
margin-block-start: 1.6em;
|
|
384
|
+
font-size: var(--type-section);
|
|
385
|
+
font-weight: 500;
|
|
386
|
+
line-height: 1.25;
|
|
387
|
+
line-height: round(up, 1.25em, 4px);
|
|
388
|
+
letter-spacing: -0.01em;
|
|
389
|
+
}
|
|
390
|
+
:where(.hp-prose) :where(h3, h4, h5, h6) {
|
|
391
|
+
margin-block-start: 1.5em;
|
|
392
|
+
font-size: 1em;
|
|
393
|
+
font-weight: 500;
|
|
394
|
+
}
|
|
395
|
+
:where(.hp-prose) :where(h3) {
|
|
396
|
+
font-weight: 600;
|
|
397
|
+
}
|
|
398
|
+
:where(.hp-prose) :where(h5, h6) {
|
|
399
|
+
color: var(--muted);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
:where(.hp-prose) :where(strong, b) {
|
|
403
|
+
font-weight: 600;
|
|
404
|
+
}
|
|
405
|
+
:where(.hp-prose) :where(del, s) {
|
|
406
|
+
text-decoration-line: line-through;
|
|
407
|
+
text-decoration-thickness: 1px;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/* Lists: square markers on bulleted lists (square corners everywhere),
|
|
411
|
+
tabular figures on numbered ones, both in --muted. The markers hang
|
|
412
|
+
outside the text, so the text edge lines up with the paragraphs. A
|
|
413
|
+
start number comes from the list's own attribute. */
|
|
414
|
+
:where(.hp-prose) :where(ul, ol) {
|
|
415
|
+
padding-inline-start: 1.5em;
|
|
416
|
+
}
|
|
417
|
+
:where(.hp-prose) :where(ol) {
|
|
418
|
+
padding-inline-start: 2em;
|
|
419
|
+
list-style-type: decimal;
|
|
420
|
+
}
|
|
421
|
+
:where(.hp-prose) :where(ul) {
|
|
422
|
+
list-style-type: square;
|
|
423
|
+
}
|
|
424
|
+
:where(.hp-prose li)::marker {
|
|
425
|
+
color: var(--muted);
|
|
426
|
+
}
|
|
427
|
+
:where(.hp-prose ol > li)::marker {
|
|
428
|
+
font-variant-numeric: tabular-nums;
|
|
429
|
+
}
|
|
430
|
+
/* Tight inside a list: items 0.25em apart, a nested list 0.25em under its
|
|
431
|
+
item's text, other blocks in an item 0.5em apart. */
|
|
432
|
+
:where(.hp-prose li) + :where(li) {
|
|
433
|
+
margin-block-start: 0.25em;
|
|
434
|
+
}
|
|
435
|
+
:where(.hp-prose li > * + *) {
|
|
436
|
+
margin-block-start: 0.5em;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/* Task lists. A checkbox in an item is drawn as Hardpoint's checkbox box
|
|
440
|
+
in its disabled form: a black box with the quiet --border edge and a
|
|
441
|
+
--muted mark, and it takes the marker's place. It is out of the flow,
|
|
442
|
+
so the item's text keeps the list's text edge. */
|
|
443
|
+
:where(.hp-prose li:has(> input[type='checkbox'], > p:first-child > input[type='checkbox'])) {
|
|
444
|
+
position: relative;
|
|
445
|
+
list-style: none;
|
|
446
|
+
}
|
|
447
|
+
:where(.hp-prose li input[type='checkbox']) {
|
|
448
|
+
appearance: none;
|
|
449
|
+
position: absolute;
|
|
450
|
+
inset-block-start: calc((1lh - 1.25em) / 2);
|
|
451
|
+
inset-inline-start: -1.5em;
|
|
452
|
+
display: grid;
|
|
453
|
+
place-items: center;
|
|
454
|
+
box-sizing: border-box;
|
|
455
|
+
width: 1.25em;
|
|
456
|
+
height: 1.25em;
|
|
457
|
+
margin: 0;
|
|
458
|
+
padding: 0;
|
|
459
|
+
border: 1px solid var(--border);
|
|
460
|
+
border-radius: 0;
|
|
461
|
+
background: var(--canvas);
|
|
462
|
+
color: var(--muted);
|
|
463
|
+
}
|
|
464
|
+
/* The mark is the box's 12px check as a clipped square, so it takes the
|
|
465
|
+
text colour and needs no image. */
|
|
466
|
+
:where(.hp-prose li input[type='checkbox']:checked)::before {
|
|
467
|
+
content: '';
|
|
468
|
+
width: 0.75em;
|
|
469
|
+
height: 0.75em;
|
|
470
|
+
background: currentColor;
|
|
471
|
+
clip-path: polygon(6.6% 60.1%, 37.5% 91%, 93.4% 35.1%, 81.6% 23.3%, 37.5% 67.4%, 18.4% 48.3%);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/* Quotes: a 3px edge in --border, since titanium marks selection, and the
|
|
475
|
+
text in --muted. */
|
|
476
|
+
:where(.hp-prose) :where(blockquote) {
|
|
477
|
+
padding-inline-start: 0.75em;
|
|
478
|
+
border-inline-start: 3px solid var(--border);
|
|
479
|
+
color: var(--muted);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
:where(.hp-prose) :where(hr) {
|
|
483
|
+
height: 0;
|
|
484
|
+
margin-block: 1.5em;
|
|
485
|
+
margin-inline: 0;
|
|
486
|
+
border: 0;
|
|
487
|
+
border-block-start: 1px solid var(--border);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/* A markdown image sits in a paragraph, which has its own gap; one that
|
|
491
|
+
sits directly in the container gets the block gap too. */
|
|
492
|
+
:where(.hp-prose) :where(img) {
|
|
493
|
+
display: block;
|
|
494
|
+
max-width: 100%;
|
|
495
|
+
height: auto;
|
|
496
|
+
}
|
|
497
|
+
:where(.hp-prose) > :where(img) {
|
|
498
|
+
margin-block-start: 1em;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/* Code. Inline code is a little smaller than the text around it, because
|
|
502
|
+
Geist Mono looks larger at the same size. A block is Geist Mono on a 20px
|
|
503
|
+
rhythm, never wraps and scrolls sideways inside its own box. It has a
|
|
504
|
+
1px --border hairline instead of a fill: --surface-hover cannot be seen
|
|
505
|
+
on black, --surface-band is heavy under a long listing, and the line
|
|
506
|
+
reads in both themes and marks where the scrolling area ends. */
|
|
507
|
+
:where(.hp-prose) :where(code, kbd, samp) {
|
|
508
|
+
font-size: 0.875em;
|
|
509
|
+
}
|
|
510
|
+
:where(.hp-prose) :where(pre) {
|
|
511
|
+
padding: var(--space-3);
|
|
512
|
+
overflow-x: auto;
|
|
513
|
+
overscroll-behavior-x: contain;
|
|
514
|
+
border: 1px solid var(--border);
|
|
515
|
+
font-size: 0.875em;
|
|
516
|
+
line-height: calc(20 / 14);
|
|
517
|
+
white-space: pre;
|
|
518
|
+
overflow-wrap: normal;
|
|
519
|
+
tab-size: 4;
|
|
520
|
+
}
|
|
521
|
+
:where(.hp-prose pre) :where(code, kbd, samp) {
|
|
522
|
+
font-size: inherit;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/* A code block with a copy button: wrap the pre and the app's Button
|
|
526
|
+
(class hp-prose-copy) in .hp-prose-code. The wrapper draws the hairline and
|
|
527
|
+
the button sits at the top end, outside the text, so a renderer that
|
|
528
|
+
replaces the pre keeps it. The pre leaves a column for the button, so
|
|
529
|
+
text, which scrolls sideways under any overlay, never runs beneath it. */
|
|
530
|
+
:where(.hp-prose .hp-prose-code) {
|
|
531
|
+
position: relative;
|
|
532
|
+
border: 1px solid var(--border);
|
|
533
|
+
}
|
|
534
|
+
:where(.hp-prose .hp-prose-code > pre) {
|
|
535
|
+
margin: 0;
|
|
536
|
+
margin-inline-end: calc(var(--control-height) + var(--space-2));
|
|
537
|
+
border: 0;
|
|
538
|
+
}
|
|
539
|
+
:where(.hp-prose .hp-prose-copy) {
|
|
540
|
+
position: absolute;
|
|
541
|
+
inset-block-start: var(--space-1);
|
|
542
|
+
inset-inline-end: var(--space-1);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/* Tables. The header band is --surface-band with cells as written, never
|
|
546
|
+
caps. Cells are Geist, not Geist Mono: they hold words. Text that wraps
|
|
547
|
+
makes rows of several lines, and without a line between rows the eye
|
|
548
|
+
cannot tell where one ends, so every row has a 1px --border divider (the
|
|
549
|
+
table's ruled form), and no zebra. A table is as wide as its content or
|
|
550
|
+
its box, whichever is wider; wrap each in .hp-prose-scroll and a table
|
|
551
|
+
wider than the column scrolls sideways inside it. Alignment comes from
|
|
552
|
+
the cell's align attribute or its own text-align. */
|
|
553
|
+
:where(.hp-prose .hp-prose-scroll) {
|
|
554
|
+
max-width: 100%;
|
|
555
|
+
overflow-x: auto;
|
|
556
|
+
overscroll-behavior-x: contain;
|
|
557
|
+
}
|
|
558
|
+
:where(.hp-prose) :where(table) {
|
|
559
|
+
min-width: 100%;
|
|
560
|
+
border-collapse: collapse;
|
|
561
|
+
font-variant-numeric: tabular-nums;
|
|
562
|
+
}
|
|
563
|
+
:where(.hp-prose) :where(th, td) {
|
|
564
|
+
padding: var(--space-2) var(--space-3);
|
|
565
|
+
border-block-end: 1px solid var(--border);
|
|
566
|
+
overflow-wrap: normal;
|
|
567
|
+
text-align: start;
|
|
568
|
+
vertical-align: top;
|
|
569
|
+
}
|
|
570
|
+
:where(.hp-prose th) {
|
|
571
|
+
border-block-end: 0;
|
|
572
|
+
background: var(--surface-band);
|
|
573
|
+
font-weight: 500;
|
|
574
|
+
}
|
|
575
|
+
/* The cell's align attribute: the browser maps it to text-align at the
|
|
576
|
+
lowest priority, so the start alignment above would hide it. */
|
|
577
|
+
:where(.hp-prose :is(th, td)[align='left']) {
|
|
578
|
+
text-align: left;
|
|
579
|
+
}
|
|
580
|
+
:where(.hp-prose :is(th, td)[align='center']) {
|
|
581
|
+
text-align: center;
|
|
582
|
+
}
|
|
583
|
+
:where(.hp-prose :is(th, td)[align='right']) {
|
|
584
|
+
text-align: right;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
@media (pointer: fine) {
|
|
588
|
+
:where(.hp-prose pre, .hp-prose .hp-prose-scroll) {
|
|
589
|
+
scrollbar-width: thin;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/* Gaps that depend on neighbours come last, so they win over the block gap:
|
|
594
|
+
a heading keeps 0.5em from what it introduces, and the first and last
|
|
595
|
+
child of any box touch its edges. */
|
|
596
|
+
:where(.hp-prose) :where(h1, h2, h3, h4, h5, h6) + * {
|
|
597
|
+
margin-block-start: 0.5em;
|
|
598
|
+
}
|
|
599
|
+
:where(.hp-prose :first-child) {
|
|
600
|
+
margin-block-start: 0;
|
|
601
|
+
}
|
|
602
|
+
:where(.hp-prose :last-child) {
|
|
603
|
+
margin-block-end: 0;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/* A nested list follows its item's text, which is not an element, so it is
|
|
607
|
+
that item's first child: it takes its 0.25em back after the rule above. */
|
|
608
|
+
:where(.hp-prose li > :is(ul, ol)) {
|
|
609
|
+
margin-block-start: 0.25em;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/* ---- Utilities --------------------------------------------------------- */
|
|
613
|
+
|
|
614
|
+
/* Hidden from sight, kept in the accessibility tree. */
|
|
615
|
+
.hp-visually-hidden {
|
|
616
|
+
position: absolute;
|
|
617
|
+
width: 1px;
|
|
618
|
+
height: 1px;
|
|
619
|
+
margin: -1px;
|
|
620
|
+
padding: 0;
|
|
621
|
+
overflow: hidden;
|
|
622
|
+
clip-path: inset(50%);
|
|
623
|
+
white-space: nowrap;
|
|
624
|
+
border: 0;
|
|
625
|
+
}
|