dowel-ui 0.24.0 → 0.26.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/dist/prose.css ADDED
@@ -0,0 +1,448 @@
1
+ /*
2
+ * dowel prose - the shape of text a product did not write by hand.
3
+ *
4
+ * Every other rule in this system is applied by a component, because a
5
+ * component owns the element it draws. Rendered markdown is the case where
6
+ * that is impossible: the product hands the DOM a string of HTML - from
7
+ * `marked`, from a CMS, from a model's reply - and there is no React element
8
+ * to hang a class on. The tags arrive already made. Only a descendant selector
9
+ * reaches them.
10
+ *
11
+ * kilna proved the shape before this file existed, as thirty `[&_h1]:mt-4`
12
+ * arbitrary variants inside one `className` string. It works and it is a
13
+ * paragraph of unreadable text that no second product can share, which is the
14
+ * whole argument for moving it here.
15
+ *
16
+ * Scoped to `.prose`, so nothing leaks: a stylesheet that styled `h2`
17
+ * globally would reach into every component that happens to render one.
18
+ *
19
+ * WHY NOT A TYPOGRAPHY PLUGIN. `@tailwindcss/typography` answers the same
20
+ * question and brings its own answer to a different one - its own type scale,
21
+ * its own greys, its own idea of measure. Installing it next to this theme
22
+ * means two vocabularies describing the same text, and the one that wins is
23
+ * whichever loaded last. Every value below is a token from `theme.css`; there
24
+ * is not a single colour or size written down here.
25
+ *
26
+ * WHAT THIS IS NOT FOR. Interface text - a label, a row, a button - is styled
27
+ * by the component that draws it. `.prose` is for a reading column: a note, a
28
+ * description, an article, a chat reply. Wrapping a form in it is how a screen
29
+ * ends up with two competing ideas of what `text-sm` means.
30
+ *
31
+ * Import after the theme:
32
+ *
33
+ * @import 'tailwindcss';
34
+ * @import './dowel/theme.css';
35
+ * @import './dowel/prose.css';
36
+ */
37
+
38
+ .prose {
39
+ /*
40
+ * The reading size is `--text-base`, not the `--text-sm` the interface runs
41
+ * at, and the two are different on purpose. Chrome is scanned - a label is
42
+ * recognised rather than read - and it packs tighter the less of it there
43
+ * is. Prose is read word by word, and 12px of continuous text is where a
44
+ * reader starts leaning in. The products already knew this and did it by
45
+ * hand: kilna's rendered markdown sets `text-sm` (12px) against an interface
46
+ * of 11px, the same one-step lift.
47
+ */
48
+ font-size: var(--text-base);
49
+
50
+ /*
51
+ * Line height is set here rather than inherited from the size token, which
52
+ * carries 20px for 14px text - right for a label, tight for a paragraph.
53
+ * 1.65 is the ratio the eye returns to the start of the next line with; it
54
+ * is a ratio rather than a length so a product that scales the size up for a
55
+ * reading view keeps the proportion.
56
+ */
57
+ line-height: 1.65;
58
+ color: var(--text);
59
+
60
+ /*
61
+ * A measure, not a width. Prose is unreadable across a wide window - the eye
62
+ * loses the line it is returning from - and `ch` is the unit that says so in
63
+ * the terms the limit is actually about: characters, at whatever size the
64
+ * text is drawn. 68 is inside the 45-75 the typographic literature agrees
65
+ * on, at the wide end because these are technical texts with code and long
66
+ * identifiers in them.
67
+ *
68
+ * A product that has its own column - a chat bubble, a card - overrides
69
+ * `max-width` and loses nothing else.
70
+ */
71
+ max-width: 68ch;
72
+
73
+ /*
74
+ * The shell of a desktop app usually turns selection off, so that dragging
75
+ * inside a window moves the window. Text someone came to READ has to hand it
76
+ * back, or the one thing a reader wants to do with a paragraph - take a
77
+ * sentence out of it - is the thing the app forbids.
78
+ */
79
+ user-select: text;
80
+ -webkit-user-select: text;
81
+ }
82
+
83
+ /*
84
+ * Vertical rhythm.
85
+ *
86
+ * Margins collapse between siblings, so stating both a top and a bottom on
87
+ * every block would double the gap at some joins and not others depending on
88
+ * which value was larger. Instead: one bottom margin on everything, and the
89
+ * top margin belongs to the headings alone, which are the only elements that
90
+ * need more space above them than below - a heading belongs to what follows
91
+ * it, and sitting equidistant between two paragraphs it appears to belong to
92
+ * neither.
93
+ */
94
+ .prose > * {
95
+ margin-block: 0 0.75em;
96
+ }
97
+
98
+ /* The first and last child never push the container open: a rendered note
99
+ * inside a bordered card would otherwise have a gap at the top that the card's
100
+ * own padding did not put there. */
101
+ .prose > :first-child {
102
+ margin-block-start: 0;
103
+ }
104
+ .prose > :last-child {
105
+ margin-block-end: 0;
106
+ }
107
+
108
+ /*
109
+ * Headings.
110
+ *
111
+ * The scale is the theme's, stepped down from a document's h1 - and it starts
112
+ * lower than a web page's would, because prose here is almost always a section
113
+ * INSIDE a screen that already has a title. An h1 drawn at 21px next to a page
114
+ * heading of 18px makes the note look like the more important thing on screen.
115
+ *
116
+ * `text-wrap: balance` on headings only: it is expensive on long text and
117
+ * makes a two-line heading break in the middle rather than leaving one word
118
+ * alone on the second line.
119
+ */
120
+ .prose :is(h1, h2, h3, h4, h5, h6) {
121
+ margin-block: 1.6em 0.5em;
122
+ font-weight: var(--font-weight-semibold);
123
+ line-height: 1.3;
124
+ text-wrap: balance;
125
+ /* A heading that ends up at the top of a scrolled container should not be
126
+ * flush against its edge. */
127
+ scroll-margin-block-start: 1rem;
128
+ }
129
+
130
+ .prose h1 {
131
+ font-size: var(--text-xl);
132
+ letter-spacing: var(--tracking-tight);
133
+ }
134
+ .prose h2 {
135
+ font-size: var(--text-lg);
136
+ letter-spacing: var(--tracking-tight);
137
+ }
138
+ .prose h3 {
139
+ font-size: var(--text-base);
140
+ }
141
+
142
+ /*
143
+ * h4 and below stop growing and start differentiating by other means: a
144
+ * document nested six levels deep has run out of sizes long before it runs out
145
+ * of levels, and inventing two more steps inside four pixels is the noise the
146
+ * type scale exists to remove. They are the body size, set apart by weight and
147
+ * by colour.
148
+ */
149
+ .prose :is(h4, h5, h6) {
150
+ font-size: var(--text-base);
151
+ color: var(--dim);
152
+ }
153
+
154
+ /* A heading directly after another has nothing between them to separate. */
155
+ .prose :is(h1, h2, h3, h4, h5, h6) + :is(h1, h2, h3, h4, h5, h6) {
156
+ margin-block-start: 0.8em;
157
+ }
158
+
159
+ /*
160
+ * Lists.
161
+ *
162
+ * Tailwind's preflight strips the markers, which is right for the interface -
163
+ * a menu is a `ul` and must not have bullets - and wrong here, where a list is
164
+ * a list. They are put back, and `outside` so the marker hangs in the indent
165
+ * and the text of a wrapped item lines up with itself rather than with the
166
+ * bullet.
167
+ */
168
+ .prose :is(ul, ol) {
169
+ padding-inline-start: 1.5em;
170
+ }
171
+ .prose ul {
172
+ list-style: disc;
173
+ }
174
+ .prose ol {
175
+ list-style: decimal;
176
+ }
177
+ /*
178
+ * `--dim`, not `--faint`, and that was measured rather than chosen.
179
+ *
180
+ * A bullet looks like furniture, so the faintest token is the instinct. But
181
+ * the marker is what says "this is a list" - drop it and an ordered list loses
182
+ * its numbers, which are content. On the stand it came out at 3.17:1 against
183
+ * the surface, below the 4.5:1 that anything carrying meaning has to clear,
184
+ * and it read as a smudge at the size a bullet actually is. `--dim` puts it at
185
+ * 6.2:1 and still sits back from the text.
186
+ */
187
+ .prose li::marker {
188
+ color: var(--dim);
189
+ }
190
+ .prose li {
191
+ margin-block: 0.25em;
192
+ }
193
+
194
+ /* A nested list belongs to the item above it, not to the gap after it. */
195
+ .prose li > :is(ul, ol) {
196
+ margin-block: 0.25em;
197
+ }
198
+
199
+ /* A task list from markdown: the checkbox replaces the marker, so the bullet
200
+ * beside it would be a second one saying the same thing. */
201
+ .prose li:has(> input[type='checkbox']:first-child) {
202
+ list-style: none;
203
+ margin-inline-start: -1.25em;
204
+ }
205
+ .prose li > input[type='checkbox'] {
206
+ margin-inline-end: 0.4em;
207
+ accent-color: var(--accent);
208
+ }
209
+
210
+ /*
211
+ * Inline code.
212
+ *
213
+ * `0.9em` rather than a token: a monospaced face at the same nominal size as
214
+ * the text around it looks larger, because its lowercase letters are taller
215
+ * relative to the em. The correction is proportional to whatever size the
216
+ * surrounding text happens to be, which a fixed token could not follow.
217
+ */
218
+ .prose code {
219
+ font-family: var(--font-mono);
220
+ font-size: 0.9em;
221
+ background-color: var(--soft);
222
+ border-radius: var(--radius-xs);
223
+ padding: 0.15em 0.35em;
224
+ /* A long identifier in the middle of a sentence must be allowed to break,
225
+ * or it pushes the whole column wider than its measure. */
226
+ overflow-wrap: anywhere;
227
+ }
228
+
229
+ /*
230
+ * A code block is not a big inline code. The padding, background and radius
231
+ * belong to the `pre`; the `code` inside it gives them all up, or the block
232
+ * gets a second inset panel drawn inside itself - which is exactly what
233
+ * happens when a typography plugin's inline rule is left to apply here.
234
+ *
235
+ * `overflow-x: auto` rather than wrapping: a wrapped line of code is a line
236
+ * that lies about where it ends, and indentation is how code is read.
237
+ */
238
+ .prose pre {
239
+ font-family: var(--font-mono);
240
+ font-size: var(--text-sm);
241
+ line-height: 1.55;
242
+ background-color: var(--soft);
243
+ border: 1px solid var(--line);
244
+ border-radius: var(--radius-md);
245
+ padding: 0.75rem 0.85rem;
246
+ overflow-x: auto;
247
+ /* `tab-size: 2` is the line's own indent; the browser default of 8 turns a
248
+ * tab-indented file into a horizontal scroll for nothing. */
249
+ tab-size: 2;
250
+ }
251
+
252
+ .prose pre code {
253
+ background-color: transparent;
254
+ border-radius: 0;
255
+ padding: 0;
256
+ font-size: inherit;
257
+ /* Inside a scrolling block, breaking a long line would defeat the scroll. */
258
+ overflow-wrap: normal;
259
+ }
260
+
261
+ /*
262
+ * Links.
263
+ *
264
+ * `--accent-2` rather than `--accent`: on the dark theme the accent is the
265
+ * product's own colour at full strength, which against body text reads as a
266
+ * button that failed to draw. The partner shade is the one the products use
267
+ * for a link, and it is also the one with room to darken on hover.
268
+ *
269
+ * Underlined, always. Colour alone is not a link - a reader who does not see
270
+ * the hue gets no signal at all - and this is the one place in the system
271
+ * where the rule "meaning never rests on colour" has a standard answer.
272
+ */
273
+ .prose a {
274
+ color: var(--accent-2);
275
+ text-decoration: underline;
276
+ /* The underline drops below the descenders instead of striking through
277
+ * them, which is the difference between a link and a crossed-out word. */
278
+ text-underline-offset: 0.2em;
279
+ text-decoration-thickness: from-font;
280
+ overflow-wrap: anywhere;
281
+ }
282
+
283
+ .prose a:hover {
284
+ color: var(--accent);
285
+ }
286
+
287
+ /*
288
+ * A quotation.
289
+ *
290
+ * A rule on the left and dimmed text, rather than italics: a blockquote is
291
+ * frequently a paragraph or more, and a long passage in italic is slower to
292
+ * read for everyone and materially harder for some dyslexic readers.
293
+ */
294
+ .prose blockquote {
295
+ border-inline-start: 2px solid var(--line-2);
296
+ padding-inline-start: 0.9em;
297
+ color: var(--dim);
298
+ }
299
+
300
+ /*
301
+ * A table inside prose.
302
+ *
303
+ * `display: block` with its own scroll, because the one thing a table must not
304
+ * do in a reading column is set the column's width: a note with a six-column
305
+ * table in it would push every paragraph around it out to the table's width.
306
+ * The cost is stated - a block-level table no longer participates in the
307
+ * column's own layout - and it is the right trade for text.
308
+ */
309
+ .prose table {
310
+ display: block;
311
+ max-width: 100%;
312
+ overflow-x: auto;
313
+ border-collapse: collapse;
314
+ font-size: var(--text-sm);
315
+ }
316
+
317
+ .prose :is(th, td) {
318
+ border: 1px solid var(--line);
319
+ padding: 0.3em 0.55em;
320
+ text-align: start;
321
+ vertical-align: top;
322
+ }
323
+
324
+ .prose th {
325
+ background-color: var(--soft);
326
+ font-weight: var(--font-weight-semibold);
327
+ }
328
+
329
+ /*
330
+ * A horizontal rule is a section break, so the space around it is the point;
331
+ * a hairline with a paragraph's gap either side reads as a mistake.
332
+ */
333
+ .prose hr {
334
+ border: 0;
335
+ border-block-start: 1px solid var(--line);
336
+ margin-block: 2em;
337
+ }
338
+
339
+ .prose :is(strong, b) {
340
+ font-weight: var(--font-weight-semibold);
341
+ color: var(--text);
342
+ }
343
+
344
+ .prose :is(em, i) {
345
+ font-style: italic;
346
+ }
347
+
348
+ .prose :is(s, del) {
349
+ color: var(--dim);
350
+ }
351
+
352
+ .prose mark {
353
+ background-color: var(--accent-soft);
354
+ color: inherit;
355
+ border-radius: var(--radius-xs);
356
+ padding: 0.05em 0.2em;
357
+ }
358
+
359
+ /*
360
+ * An image in prose is never wider than the column, and keeps its ratio when
361
+ * it is constrained. `display: block` because an inline image sits on the text
362
+ * baseline and leaves a strip of descender space under it that looks like a
363
+ * broken margin.
364
+ */
365
+ .prose img {
366
+ display: block;
367
+ max-width: 100%;
368
+ height: auto;
369
+ border-radius: var(--radius-sm);
370
+ }
371
+
372
+ /*
373
+ * `kbd` is drawn as a key rather than as code: a reader who sees `Ctrl` in the
374
+ * same grey box as a variable name has to work out which it is. This matches
375
+ * the Kbd primitive, so a key looks the same whether a component drew it or
376
+ * markdown did.
377
+ */
378
+ .prose kbd {
379
+ font-family: var(--font-mono);
380
+ font-size: 0.85em;
381
+ background-color: var(--raise);
382
+ border: 1px solid var(--line-2);
383
+ border-block-end-width: 2px;
384
+ border-radius: var(--radius-xs);
385
+ padding: 0.1em 0.35em;
386
+ color: var(--dim);
387
+ }
388
+
389
+ /*
390
+ * A definition list, which markdown does not produce but a CMS does.
391
+ */
392
+ .prose dt {
393
+ font-weight: var(--font-weight-semibold);
394
+ margin-block-start: 0.75em;
395
+ }
396
+ .prose dd {
397
+ margin-inline-start: 1.5em;
398
+ color: var(--dim);
399
+ }
400
+
401
+ /*
402
+ * A footnote reference and the notes at the bottom - what `remark-gfm`
403
+ * produces. Smaller and dimmer, because a footnote that reads at the weight of
404
+ * the text interrupts the sentence carrying it.
405
+ */
406
+ .prose sup a {
407
+ text-decoration: none;
408
+ font-size: 0.8em;
409
+ }
410
+ .prose .footnotes {
411
+ font-size: var(--text-sm);
412
+ color: var(--dim);
413
+ border-block-start: 1px solid var(--line);
414
+ margin-block-start: 2em;
415
+ padding-block-start: 0.75em;
416
+ }
417
+
418
+ /*
419
+ * Tight: the same prose in a place that has no room for a reading column - a
420
+ * chat bubble, a table cell, a hover card. The rhythm compresses and the
421
+ * measure is given up to the container, because in a bubble the container IS
422
+ * the measure. Nothing else changes: the same tags, the same tokens.
423
+ */
424
+ .prose-tight {
425
+ font-size: var(--text-sm);
426
+ line-height: 1.55;
427
+ max-width: none;
428
+ }
429
+
430
+ .prose-tight > * {
431
+ margin-block: 0 0.5em;
432
+ }
433
+
434
+ .prose-tight :is(h1, h2, h3, h4, h5, h6) {
435
+ margin-block: 1em 0.35em;
436
+ }
437
+
438
+ .prose-tight :is(h1, h2) {
439
+ font-size: var(--text-base);
440
+ }
441
+
442
+ .prose-tight h3 {
443
+ font-size: var(--text-sm);
444
+ }
445
+
446
+ .prose-tight hr {
447
+ margin-block: 1.2em;
448
+ }