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/README.md +42 -163
- package/dist/index.d.ts +16 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -1
- package/dist/prose.css +448 -0
- package/dist/registry.json +343 -62
- package/dist/theme.css +77 -0
- package/dist/tokens.json +127 -1
- package/package.json +2 -1
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
|
+
}
|