orbiq-neural-ui-kit 0.0.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.
@@ -0,0 +1,693 @@
1
+ import plugin from 'tailwindcss/plugin'
2
+ import {
3
+ generateColorPalette,
4
+ generateSemanticColors,
5
+ generateCSSVariables,
6
+ generateEffectVariables,
7
+ } from './colorPalette.js'
8
+ import radiusTokens from './generated/radius.json'
9
+ import typographyTokens from './generated/typography.json'
10
+ import effectsData from './generated/effects.json'
11
+
12
+ let colorPalette = generateColorPalette()
13
+ let semanticColors = generateSemanticColors()
14
+ let cssVariables = mergeVariableLayers(
15
+ generateCSSVariables(),
16
+ generateEffectVariables(),
17
+ generateRadiusVariables(),
18
+ )
19
+
20
+ // Emit `--radius-{key}` for every radius token (the numbered scale plus the
21
+ // kept `none` / `full` names — the deprecated size aliases were removed in
22
+ // 1.0.0 per ADR-0006, #998) so the values are inspectable as real CSS
23
+ // variables. `borderRadius` is rewired below to consume these vars, so
24
+ // `rounded-4` and `--radius-4` stay in sync.
25
+ function generateRadiusVariables() {
26
+ const vars = {}
27
+ for (const [key, value] of Object.entries(radiusTokens)) {
28
+ vars[`--radius-${key}`] = value
29
+ }
30
+ return { ':root': vars }
31
+ }
32
+
33
+ // Each value carries a trailing `/* {px} */` comment so editor tooling
34
+ // (Tailwind IntelliSense) surfaces the resolved px on hover, instead of
35
+ // the opaque `var(--radius-*)` reference. No `DEFAULT` key: the bare
36
+ // `rounded` utility no longer exists — use `rounded-4`.
37
+ function buildRadiusConfig() {
38
+ const out = {}
39
+ for (const [key, value] of Object.entries(radiusTokens)) {
40
+ out[key] = `var(--radius-${key}) /* ${value} */`
41
+ }
42
+ return out
43
+ }
44
+
45
+ // Merge two `{ selector: { var: value } }` objects into one, preserving any
46
+ // vars already declared under the same selector.
47
+ function mergeVariableLayers(...layers) {
48
+ const out = {}
49
+ for (const layer of layers) {
50
+ for (const [selector, vars] of Object.entries(layer)) {
51
+ out[selector] = { ...(out[selector] || {}), ...vars }
52
+ }
53
+ }
54
+ return out
55
+ }
56
+
57
+ // Weight variants are exposed as component classes — `text-<size>-<weight>` and
58
+ // the paragraph counterpart `text-p-<size>-<weight>` — one per Figma named
59
+ // style. Figma tracks each weight differently per size and CSS letter-spacing
60
+ // can't follow font-weight, so each (size, weight) must ship as a self-contained
61
+ // class. `regular` stays the bare `text-<size>` / `text-p-<size>` utility.
62
+ // (Component classes are JIT-purged, so only the ones used in content emit.)
63
+ // `black` was dropped in #998 — zero usage, and the Figma weights behind it
64
+ // were corrupt export data.
65
+ const WEIGHT_VARIANTS = ['medium', 'semibold', 'bold']
66
+
67
+ function buildFontSize() {
68
+ const out = {}
69
+ // Each size's regular variant already carries lineHeight, letterSpacing and
70
+ // fontWeight from the text-styles export (see figma-tokens-to-theme.js).
71
+ for (const [key, [size, meta]] of Object.entries(typographyTokens.fontSize)) {
72
+ out[key] = [size, { ...meta }]
73
+ }
74
+ // Paragraph variants (`text-p-<size>`): same size, the paragraph style's
75
+ // looser line-height and its own letter-spacing.
76
+ for (const [key, p] of Object.entries(typographyTokens.paragraph || {})) {
77
+ if (!out[key]) continue
78
+ const [size, meta] = out[key]
79
+ out[`p-${key}`] = [
80
+ size,
81
+ { ...meta, lineHeight: p.lineHeight, letterSpacing: p.letterSpacing },
82
+ ]
83
+ }
84
+ return out
85
+ }
86
+
87
+ // Focus ring utilities backed by `--focus-outline-*` CSS vars (theme-flipped
88
+ // in colorPalette.js#generateEffectVariables). Implemented as `outline`, not
89
+ // box-shadow, so rings never collide with shadow/ring utilities on the same
90
+ // element and survive forced-colors mode. The default ring is applied
91
+ // globally via `:focus-visible` (see globalStyles); these utilities are for
92
+ // themed overrides (`focus-visible:focus-ring-red`) and non-focus states
93
+ // (`data-[state=open]:focus-ring`). Registered via `addComponents` so
94
+ // Tailwind IntelliSense picks them up.
95
+ function buildFocusRingUtilities() {
96
+ const out = {}
97
+ for (const name of Object.keys(effectsData.focus.light)) {
98
+ const className = name === 'default' ? '.focus-ring' : `.focus-ring-${name}`
99
+ out[className] = {
100
+ outline: `var(--focus-outline-${name})`,
101
+ outlineOffset: '0px',
102
+ }
103
+ }
104
+ return out
105
+ }
106
+
107
+ function buildTextStyleUtilities() {
108
+ const out = {}
109
+ const t = typographyTokens
110
+ const groups = [
111
+ {
112
+ className: (s, w) => `.text-${s}-${w}`,
113
+ tracking: t.tracking?.text || {},
114
+ lineHeight: (s) => t.fontSize[s]?.[1].lineHeight,
115
+ },
116
+ {
117
+ className: (s, w) => `.text-p-${s}-${w}`,
118
+ tracking: t.tracking?.paragraph || {},
119
+ lineHeight: (s) => t.paragraph?.[s]?.lineHeight,
120
+ },
121
+ ]
122
+ for (const group of groups) {
123
+ for (const [size, byWeight] of Object.entries(group.tracking)) {
124
+ const entry = t.fontSize[size]
125
+ if (!entry) continue
126
+ const [fontSize] = entry
127
+ const lineHeight = group.lineHeight(size)
128
+ const transform = t.textTransform?.[size]
129
+ for (const weight of WEIGHT_VARIANTS) {
130
+ if (!(weight in byWeight)) continue
131
+ out[group.className(size, weight)] = {
132
+ fontSize,
133
+ lineHeight,
134
+ fontWeight: String(t.fontWeight[weight]),
135
+ letterSpacing: byWeight[weight],
136
+ ...(transform ? { textTransform: transform } : {}),
137
+ }
138
+ }
139
+ }
140
+ }
141
+ // `tiny` is an uppercase eyebrow style; the bare regular utility needs the
142
+ // text-transform too (Tailwind's fontSize tuple can't express it).
143
+ for (const [size, transform] of Object.entries(t.textTransform || {})) {
144
+ out[`.text-${size}`] = {
145
+ ...(out[`.text-${size}`] || {}),
146
+ textTransform: transform,
147
+ }
148
+ }
149
+ return out
150
+ }
151
+
152
+ let globalStyles = (theme) => ({
153
+ html: {
154
+ 'font-family': `InterVar, ${theme('fontFamily.sans')}`,
155
+ 'font-optical-sizing': 'auto',
156
+ },
157
+ 'html, body, button, p, span, div': {
158
+ fontVariationSettings: "'opsz' 24, 'cv11' 1",
159
+ WebkitFontSmoothing: 'antialiased',
160
+ MozOsxFontSmoothing: 'grayscale',
161
+ },
162
+ select: {
163
+ backgroundImage:
164
+ 'url(\'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="%237C7C7C" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" aria-hidden="true" viewBox="0 0 24 24" ><path d="m6 9 6 6 6-6" /></svg>\')',
165
+ backgroundSize: '1.13em',
166
+ backgroundPosition: 'right 0.44rem center',
167
+ },
168
+ // A bare `<p>` reads as body copy, so it defaults to a relaxed line-height
169
+ // instead of the tight value baked into the `text-<size>` utilities. Kept at
170
+ // element specificity (0,0,1) with `:where()` (which adds none) so any explicit
171
+ // `text-*` / `text-p-*` line-height wins, and scoped out of rich text so the
172
+ // `prose` / editor line-heights are untouched.
173
+ 'p:not(:where(.prose, .ProseMirror) *)': {
174
+ lineHeight: '1.5',
175
+ },
176
+ // Global keyboard focus indicator (espresso v2 focus/default token).
177
+ // Lives in the base layer so any utility on the element can override it:
178
+ // suppress with `focus-visible:outline-none`, retheme with
179
+ // `focus-visible:focus-ring-<color>`.
180
+ ':focus-visible': {
181
+ outline: 'var(--focus-outline-default)',
182
+ outlineOffset: '0px',
183
+ },
184
+ })
185
+
186
+ let componentStyles = {
187
+ '.form-input, .form-textarea, .form-select': {
188
+ '@apply h-7 rounded-4 border border-[--surface-gray-2] bg-surface-gray-2 py-1.5 pl-2 pr-2 text-base text-ink-gray-8 placeholder-ink-gray-4 transition-colors hover:border-outline-elevation-2 hover:bg-surface-gray-3 focus:border-outline-gray-4 focus:bg-surface-base focus:shadow-sm focus:ring-0':
189
+ {},
190
+ },
191
+ '.form-checkbox': {
192
+ '@apply rounded-5 bg-surface-gray-2 text-ink-blue-4 focus:ring-0': {},
193
+ }
194
+ }
195
+
196
+ let extraBaseStyles = {
197
+ "[data-theme='dark'] [type='checkbox']:checked": {
198
+ 'background-image': `url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='%230F0F0F' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e")`,
199
+ },
200
+ "[data-theme='dark'] [type='checkbox']:indeterminate": {
201
+ 'background-image': `url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3e%3cpath stroke='%230F0F0F' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3e%3c/svg%3e")`,
202
+ }
203
+ }
204
+
205
+ export default plugin(
206
+ function ({ addBase, addComponents, matchUtilities, theme }) {
207
+ addBase({ ...globalStyles(theme), ...cssVariables, ...extraBaseStyles })
208
+ addComponents(componentStyles)
209
+ addComponents(buildTextStyleUtilities())
210
+ addComponents(buildFocusRingUtilities())
211
+ // Authoring sugar for the list-family styling hooks (frappe-ui/list),
212
+ // on the spacing scale with variants and arbitrary values:
213
+ // `max-sm:list-gap-3` instead of `max-sm:[--list-gap:0.75rem]`.
214
+ matchUtilities(
215
+ {
216
+ 'list-gap': (value) => ({ '--list-gap': value }),
217
+ 'list-row-px': (value) => ({ '--list-row-padding-x': value }),
218
+ },
219
+ { values: theme('spacing') },
220
+ )
221
+ // Grid tracks are arbitrary-only (no meaningful scale):
222
+ // `max-md:list-cols-[minmax(0,1fr)_auto]` instead of
223
+ // `max-md:[--list-columns:minmax(0,1fr)_auto]`.
224
+ matchUtilities({
225
+ 'list-cols': (value) => ({ '--list-columns': value }),
226
+ })
227
+ },
228
+ {
229
+ theme: {
230
+ colors: colorPalette,
231
+ borderRadius: buildRadiusConfig(),
232
+ boxShadow: {
233
+ none: 'none',
234
+ sm: 'var(--elevation-sm)',
235
+ base: 'var(--elevation-base)',
236
+ DEFAULT: 'var(--elevation-base)',
237
+ md: 'var(--elevation-md)',
238
+ lg: 'var(--elevation-lg)',
239
+ xl: 'var(--elevation-xl)',
240
+ '2xl': 'var(--elevation-2xl)',
241
+ },
242
+ container: {
243
+ padding: {
244
+ xl: '5rem',
245
+ },
246
+ },
247
+ fontSize: buildFontSize(),
248
+ screens: {
249
+ sm: '640px',
250
+ md: '768px',
251
+ lg: '1024px',
252
+ xl: '1280px',
253
+ },
254
+ extend: {
255
+ textColor: {
256
+ ink: semanticColors.ink,
257
+ },
258
+ backgroundColor: {
259
+ surface: semanticColors.surface,
260
+ 'surface-alpha': semanticColors['surface-alpha'],
261
+ },
262
+ gradientColorStops: {
263
+ surface: semanticColors.surface,
264
+ 'surface-alpha': semanticColors['surface-alpha'],
265
+ ink: semanticColors.ink,
266
+ outline: semanticColors.outline,
267
+ 'outline-alpha': semanticColors['outline-alpha'],
268
+ },
269
+ fill: {
270
+ ink: semanticColors.ink,
271
+ surface: semanticColors.surface,
272
+ },
273
+ stroke: {
274
+ ink: semanticColors.ink,
275
+ },
276
+ placeholderColor: {
277
+ ink: semanticColors.ink,
278
+ },
279
+ borderColor: () => ({
280
+ DEFAULT: 'var(--outline-gray-1)',
281
+ outline: semanticColors.outline,
282
+ 'outline-alpha': semanticColors['outline-alpha'],
283
+ }),
284
+ ringColor: {
285
+ outline: semanticColors.outline,
286
+ 'outline-alpha': semanticColors['outline-alpha'],
287
+ },
288
+ divideColor: {
289
+ outline: semanticColors.outline,
290
+ 'outline-alpha': semanticColors['outline-alpha'],
291
+ },
292
+ spacing: {
293
+ 4.5: '1.125rem',
294
+ 5.5: '1.375rem',
295
+ 6.5: '1.625rem',
296
+ 7.5: '1.875rem',
297
+ 8.5: '2.125rem',
298
+ 9.5: '2.375rem',
299
+ 10.5: '2.625rem',
300
+ 11.5: '2.875rem',
301
+ 12.5: '3.125rem',
302
+ 13: '3.25rem',
303
+ 13.5: '3.375rem',
304
+ 14.5: '3.625rem',
305
+ 15: '3.75rem',
306
+ 15.5: '3.875rem',
307
+ },
308
+ width: {
309
+ 3.5: '0.875rem',
310
+ 112: '28rem',
311
+ wizard: '650px',
312
+ },
313
+ height: {
314
+ 3.5: '0.875rem',
315
+ },
316
+ minWidth: {
317
+ 40: '10rem',
318
+ 50: '18rem',
319
+ },
320
+ maxHeight: {
321
+ 52: '13rem',
322
+ },
323
+ typography: (theme) => ({
324
+ DEFAULT: {
325
+ css: {
326
+ '--tw-prose-body': 'var(--ink-gray-8)',
327
+ '--tw-prose-headings': 'var(--ink-gray-9)',
328
+ '--tw-prose-lead': 'var(--ink-gray-5)',
329
+ '--tw-prose-links': 'var(--ink-gray-9)',
330
+ '--tw-prose-bold': 'var(--ink-gray-9)',
331
+ '--tw-prose-counters': 'var(--ink-gray-4)',
332
+ '--tw-prose-bullets': 'var(--ink-gray-2)',
333
+ '--tw-prose-hr': 'var(--ink-gray-1)',
334
+ '--tw-prose-quotes': 'var(--ink-gray-8)',
335
+ '--tw-prose-quote-borders': 'var(--ink-gray-1)',
336
+ '--tw-prose-captions': 'var(--ink-gray-4)',
337
+ '--tw-prose-kbd': 'var(--ink-gray-9)',
338
+ '--tw-prose-code': 'var(--ink-gray-9)',
339
+ '--tw-prose-pre-code': 'var(--ink-gray-1)',
340
+ '--tw-prose-pre-bg': 'var(--ink-gray-8)',
341
+ '--tw-prose-th-borders': 'var(--ink-gray-2)',
342
+ '--tw-prose-td-borders': 'var(--ink-gray-1)',
343
+ h1: {
344
+ fontWeight: 600,
345
+ },
346
+ h2: {
347
+ fontWeight: 600,
348
+ },
349
+ h3: {
350
+ fontWeight: 600,
351
+ },
352
+ h4: {
353
+ fontWeight: 600,
354
+ },
355
+ h5: {
356
+ fontWeight: 600,
357
+ },
358
+ 'h1 strong': {
359
+ fontWeight: 600,
360
+ },
361
+ 'h2 strong': {
362
+ fontWeight: 600,
363
+ },
364
+ 'h3 strong': {
365
+ fontWeight: 600,
366
+ },
367
+ 'h4 strong': {
368
+ fontWeight: 600,
369
+ },
370
+ 'h5 strong': {
371
+ fontWeight: 600,
372
+ },
373
+ 'img[data-align=right]': {
374
+ marginLeft: 'auto',
375
+ marginRight: '0',
376
+ },
377
+ 'img[data-align=center]': {
378
+ marginLeft: 'auto',
379
+ marginRight: 'auto',
380
+ },
381
+ },
382
+ },
383
+ sm: {
384
+ css: {
385
+ fontSize: '14px',
386
+ fontWeight: 420,
387
+ lineHeight: 1.5,
388
+ letterSpacing: '0.02em',
389
+ h1: {
390
+ fontSize: em(20, 14),
391
+ },
392
+ h2: {
393
+ fontSize: em(18, 14),
394
+ },
395
+ h3: {
396
+ fontSize: em(16, 14),
397
+ },
398
+ h4: {
399
+ fontSize: em(14, 14),
400
+ },
401
+ h5: {
402
+ fontSize: em(13, 14),
403
+ },
404
+ p: {
405
+ marginTop: '0.5rem',
406
+ marginBottom: '0.5rem',
407
+ },
408
+ 'ul > li': {
409
+ margin: '0.5rem 0',
410
+ '> p': {
411
+ margin: '0.5rem 0',
412
+ },
413
+ '> p:first-child:last-child': {
414
+ margin: '0.5rem 0',
415
+ },
416
+ '> p:first-child': {
417
+ marginTop: '0.5rem',
418
+ },
419
+ '> p:last-child': {
420
+ marginBottom: '0.5rem',
421
+ },
422
+ },
423
+ 'ol > li': {
424
+ margin: '0.5rem 0',
425
+ '> p': {
426
+ margin: '0.5rem 0',
427
+ },
428
+ '> p:first-child:last-child': {
429
+ margin: '0.5rem 0',
430
+ },
431
+ '> p:first-child': {
432
+ marginTop: '0.5rem',
433
+ },
434
+ '> p:last-child': {
435
+ marginBottom: '0.5rem',
436
+ },
437
+ },
438
+ },
439
+ },
440
+ // prose-v3: zero paragraph margins, user controls spacing with Enter
441
+ // all spacing on 8px grid: 4, 8, 16, 24, 32px
442
+ // empty <p> = 14px × 1.7 line-height ≈ 23.8px (the user's spacing unit)
443
+ //
444
+ // Base font-size is customizable via `--prose-font-size` (default 14px).
445
+ // Every child size is `em`-relative to this base, so overriding the
446
+ // variable rescales the whole editor proportionally — headings, lists,
447
+ // code — while line-height (unitless) and em letter-spacing scale too.
448
+ // Set it with an arbitrary-property utility or inline style, e.g.
449
+ // <EditorContent class="[--prose-font-size:1rem]" />
450
+ v3: {
451
+ css: [
452
+ {
453
+ fontSize: 'var(--prose-font-size, 15px)',
454
+ fontWeight: 420,
455
+ lineHeight: '1.7',
456
+ letterSpacing: '0.02em',
457
+
458
+ // prose-v3 color tokens — calmer, softer than defaults
459
+ '--tw-prose-body': 'var(--ink-gray-7)',
460
+ '--tw-prose-bold': 'var(--ink-gray-8)',
461
+ '--tw-prose-quotes': 'var(--ink-gray-7)',
462
+ '--tw-prose-quote-borders': 'var(--ink-gray-3)',
463
+ '--tw-prose-kbd': 'var(--ink-gray-8)',
464
+ '--tw-prose-code': 'var(--ink-gray-8)',
465
+
466
+ // links: subtle bottom border, darkens on hover
467
+ a: {
468
+ textDecoration: 'none',
469
+ borderBottom: '1px solid var(--ink-gray-4)',
470
+ transition: 'border-color 0.08s ease',
471
+ },
472
+ 'a:hover': {
473
+ borderBottom: '1px solid var(--ink-gray-6)',
474
+ },
475
+
476
+ // named text color + bold: the color lives on a `textStyle`
477
+ // span that wraps (or is wrapped by) `<strong>`. Typography's
478
+ // `strong { color: var(--tw-prose-bold) }` otherwise overrides
479
+ // the inherited named color, painting bold text gray. Mirror the
480
+ // base preset's `a strong { color: inherit }` so bold keeps the
481
+ // span's color in both nesting orders.
482
+ ':where(span[style*="--prose-color-"]) strong': {
483
+ color: 'inherit',
484
+ },
485
+ 'strong:where([style*="--prose-color-"])': {
486
+ color: 'inherit',
487
+ },
488
+
489
+ // inline code: subtle pill — strip Tailwind's added quotes
490
+ 'code::before': { content: 'none' },
491
+ 'code::after': { content: 'none' },
492
+ code: {
493
+ backgroundColor: 'var(--surface-gray-2)',
494
+ borderRadius: '4px',
495
+ paddingTop: '1px',
496
+ paddingBottom: '1px',
497
+ paddingInlineStart: '5px',
498
+ paddingInlineEnd: '5px',
499
+ fontWeight: 420,
500
+ fontSize: em(12, 14),
501
+ },
502
+ // code inside pre should not get the pill styles
503
+ 'pre code': {
504
+ backgroundColor: 'transparent',
505
+ borderRadius: '0',
506
+ padding: '0',
507
+ fontWeight: 'inherit',
508
+ fontSize: 'inherit',
509
+ },
510
+
511
+ // blockquote: left border, receded color, no italic, no quote marks
512
+ blockquote: {
513
+ 'border-inline-start-width': '2px',
514
+ borderInlineStartColor: 'var(--ink-gray-3)',
515
+ borderInlineStartStyle: 'solid',
516
+ marginTop: '16px',
517
+ marginBottom: '16px',
518
+ paddingInlineStart: '1em',
519
+ fontStyle: 'normal',
520
+ color: 'var(--ink-gray-6)',
521
+ quotes: 'none',
522
+ },
523
+ 'blockquote p:first-of-type::before': { content: 'none' },
524
+ 'blockquote p:last-of-type::after': { content: 'none' },
525
+ 'blockquote p': {
526
+ marginTop: '0',
527
+ marginBottom: '0',
528
+ },
529
+
530
+ // paragraphs: zero margin — user controls spacing with empty paragraphs
531
+ p: {
532
+ marginTop: '0',
533
+ marginBottom: '0',
534
+ },
535
+
536
+ // headings: marginTop creates section break (32/24px),
537
+ // marginBottom keeps heading close to its content (8px, proximity)
538
+ // h1/h2: full weight + darkest; h3-h5: softer weight + stepped-back color
539
+ h1: {
540
+ fontSize: em(20, 14),
541
+ marginTop: '32px',
542
+ marginBottom: '8px',
543
+ lineHeight: '1.3',
544
+ },
545
+ h2: {
546
+ fontSize: em(18, 14),
547
+ marginTop: '32px',
548
+ marginBottom: '8px',
549
+ lineHeight: '1.35',
550
+ },
551
+ h3: {
552
+ fontSize: em(16, 14),
553
+ marginTop: '24px',
554
+ marginBottom: '8px',
555
+ lineHeight: '1.4',
556
+ },
557
+ h4: {
558
+ fontSize: em(14, 14),
559
+ marginTop: '24px',
560
+ marginBottom: '8px',
561
+ lineHeight: '1.45',
562
+ },
563
+ h5: {
564
+ fontSize: em(13, 14),
565
+ marginTop: '24px',
566
+ marginBottom: '8px',
567
+ lineHeight: '1.45',
568
+ },
569
+
570
+ // element after heading gets no extra top margin
571
+ 'h1 + *': { marginTop: '0' },
572
+ 'h2 + *': { marginTop: '0' },
573
+ 'h3 + *': { marginTop: '0' },
574
+ 'h4 + *': { marginTop: '0' },
575
+ 'h5 + *': { marginTop: '0' },
576
+
577
+ // lists: small outer margin (4px), tight internal spacing
578
+ ul: {
579
+ marginTop: '4px',
580
+ marginBottom: '4px',
581
+ paddingInlineStart: '1.5em',
582
+ },
583
+ ol: {
584
+ marginTop: '4px',
585
+ marginBottom: '4px',
586
+ paddingInlineStart: '1.7em',
587
+ },
588
+ li: {
589
+ marginTop: '4px',
590
+ marginBottom: '4px',
591
+ },
592
+ 'li p': {
593
+ marginTop: '4px',
594
+ marginBottom: '4px',
595
+ },
596
+ 'ul ul, ul ol, ol ul, ol ol': {
597
+ marginTop: '4px',
598
+ marginBottom: '4px',
599
+ },
600
+
601
+ // code blocks: breathing room (16px)
602
+ pre: {
603
+ fontSize: em(12, 14),
604
+ lineHeight: '1.6',
605
+ marginTop: '16px',
606
+ marginBottom: '16px',
607
+ borderRadius: '0.375rem',
608
+ paddingTop: '0.75em',
609
+ paddingInlineEnd: '1em',
610
+ paddingBottom: '0.75em',
611
+ paddingInlineStart: '1em',
612
+ },
613
+
614
+ // tables: breathing room (16px)
615
+ table: {
616
+ fontSize: em(12, 14),
617
+ lineHeight: '1.5',
618
+ marginTop: '16px',
619
+ marginBottom: '16px',
620
+ },
621
+
622
+ // images, video, figures: breathing room (16px)
623
+ img: {
624
+ marginTop: '16px',
625
+ marginBottom: '16px',
626
+ },
627
+ picture: {
628
+ marginTop: '16px',
629
+ marginBottom: '16px',
630
+ },
631
+ 'picture > img': {
632
+ marginTop: '0',
633
+ marginBottom: '0',
634
+ },
635
+ video: {
636
+ marginTop: '16px',
637
+ marginBottom: '16px',
638
+ },
639
+ figure: {
640
+ marginTop: '16px',
641
+ marginBottom: '16px',
642
+ },
643
+ 'figure > *': {
644
+ marginTop: '0',
645
+ marginBottom: '0',
646
+ },
647
+
648
+ // hr: short centered line, not edge-to-edge
649
+ hr: {
650
+ marginTop: '24px',
651
+ marginBottom: '24px',
652
+ marginLeft: 'auto',
653
+ marginRight: 'auto',
654
+ width: '20%',
655
+ },
656
+ 'hr + *': {
657
+ marginTop: '0',
658
+ },
659
+ },
660
+ {
661
+ // first/last child: no extra margin
662
+ '> :first-child': {
663
+ marginTop: '0',
664
+ },
665
+ '> :last-child': {
666
+ marginBottom: '0',
667
+ },
668
+ },
669
+ ],
670
+ },
671
+ // prose-p-spacing: restores paragraph margins for content authored before prose-v3.
672
+ // Apply alongside prose-v3 for pre-migration content:
673
+ // new content: "prose prose-v3"
674
+ // old content: "prose prose-v3 prose-p-spacing"
675
+ // Must be defined after v3 in this config so its rules come later in
676
+ // the generated CSS and override v3's zero paragraph margins.
677
+ 'p-spacing': {
678
+ css: {
679
+ p: {
680
+ marginTop: '0.5rem',
681
+ marginBottom: '0.5rem',
682
+ },
683
+ },
684
+ },
685
+ }),
686
+ },
687
+ },
688
+ },
689
+ )
690
+
691
+ function em(pixels, base = 16) {
692
+ return `${pixels / base}em`
693
+ }