@wtfalch/design 0.11.0 → 0.12.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.
@@ -0,0 +1,88 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * What a rich-text value is, and the only shape `RichTextEditor` produces or
4
+ * `RichText` draws.
5
+ *
6
+ * **A restricted document, on purpose.** The editor is TipTap, which is
7
+ * ProseMirror, which will model tables, colours, fonts and arbitrary nesting
8
+ * given the chance. A system where every writer can reach all of that is one
9
+ * where two pages share nothing but a logo. What is left here is
10
+ * paragraphs, two heading levels, two kinds of list, bold, italic and links
11
+ * — the set a piece of prose actually needs.
12
+ *
13
+ * **The schema is the contract between three things**: what the toolbar can
14
+ * produce, what a consumer should store, and what the view can draw. A
15
+ * consumer validates with this on the way into its database, so a crafted
16
+ * request can no more insert a table than the editor can, and a node the
17
+ * view has no case for cannot arrive.
18
+ *
19
+ * Zod is an OPTIONAL peer dependency, and this file is the only one that
20
+ * reaches it — which is why it ships from `@wtfalch/design/rich-text` rather
21
+ * than the front door. A consumer that renders prose without validating it
22
+ * should not have to install a validator to load the package. The types and
23
+ * the pure helpers live in `./value`, which imports from here with `import
24
+ * type` and so compiles to nothing.
25
+ */
26
+ /** The marks a run of text may carry. `link` is the only one with a value, and its href is checked. */
27
+ export const richTextMarkSchema = z.discriminatedUnion('type', [
28
+ z.object({ type: z.literal('bold') }),
29
+ z.object({ type: z.literal('italic') }),
30
+ z.object({
31
+ type: z.literal('link'),
32
+ attrs: z.object({
33
+ /**
34
+ * http(s), mailto, or a path. `javascript:` and `data:` are the two
35
+ * that turn a link into script, and they are refused here rather than
36
+ * only in the editor: the editor is a convenience, this is the gate.
37
+ */
38
+ href: z
39
+ .string()
40
+ .trim()
41
+ .min(1)
42
+ .max(2048)
43
+ .refine((href) => /^(https?:\/\/|mailto:|\/)/i.test(href), 'A link must be http(s), mailto: or a path beginning with /'),
44
+ target: z.string().nullish(),
45
+ }),
46
+ }),
47
+ ]);
48
+ const textNode = z.object({
49
+ type: z.literal('text'),
50
+ text: z.string().max(10_000),
51
+ marks: z.array(richTextMarkSchema).max(8).optional(),
52
+ });
53
+ const inline = z.array(textNode).max(500).optional();
54
+ const paragraph = z.object({ type: z.literal('paragraph'), content: inline });
55
+ const heading = z.object({
56
+ type: z.literal('heading'),
57
+ /**
58
+ * Two levels, and not level 1: the page's own `<h1>` is its title, and a
59
+ * second one in the body is the most common way a document stops being
60
+ * navigable to somebody reading it with a screen reader.
61
+ */
62
+ attrs: z.object({ level: z.union([z.literal(2), z.literal(3)]) }),
63
+ content: inline,
64
+ });
65
+ const listItem = z.object({
66
+ type: z.literal('listItem'),
67
+ content: z.array(paragraph).max(20),
68
+ });
69
+ const bulletList = z.object({
70
+ type: z.literal('bulletList'),
71
+ content: z.array(listItem).max(200).optional(),
72
+ });
73
+ const orderedList = z.object({
74
+ type: z.literal('orderedList'),
75
+ attrs: z.object({ start: z.number().int().min(1).max(999) }).optional(),
76
+ content: z.array(listItem).max(200).optional(),
77
+ });
78
+ export const blockNodeSchema = z.discriminatedUnion('type', [
79
+ paragraph,
80
+ heading,
81
+ bulletList,
82
+ orderedList,
83
+ ]);
84
+ /** A whole value: what TipTap calls the document. */
85
+ export const richTextSchema = z.object({
86
+ type: z.literal('doc'),
87
+ content: z.array(blockNodeSchema).max(400).optional(),
88
+ });
@@ -0,0 +1,25 @@
1
+ import type { RichTextValue } from './schema.js';
2
+ /**
3
+ * The parts of a rich-text value that need no validator.
4
+ *
5
+ * Split from `./schema` so the package's front door stays free of zod. The
6
+ * schema is a runtime object and zod is an optional peer dependency, so a
7
+ * consumer that renders prose and never validates it — a site, a preview —
8
+ * would otherwise fail to load the package at all for want of a dependency
9
+ * it has no use for. The type import above is erased at build, so nothing
10
+ * here reaches zod.
11
+ */
12
+ /**
13
+ * A document with nothing in it — and one empty paragraph, not an empty
14
+ * array. ProseMirror's schema says a document is `block+`, so a `doc` with
15
+ * no children is not a document it can put a cursor in: the editor rendered
16
+ * an empty box with no paragraph, which meant no node to mark as empty and
17
+ * so no placeholder. The picture of that is why this reads the way it does.
18
+ * `isEmptyRichText` treats both forms as empty, and the schema accepts both,
19
+ * because a value that arrives from somewhere else may be either.
20
+ */
21
+ export declare const emptyRichText: RichTextValue;
22
+ /** Whether there is anything to draw, so a caller can skip an empty value rather than leave a gap. */
23
+ export declare function isEmptyRichText(value: RichTextValue): boolean;
24
+ /** The plain words, for a summary, a search index or a document's own `<title>`. */
25
+ export declare function richTextToPlain(value: RichTextValue): string;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The parts of a rich-text value that need no validator.
3
+ *
4
+ * Split from `./schema` so the package's front door stays free of zod. The
5
+ * schema is a runtime object and zod is an optional peer dependency, so a
6
+ * consumer that renders prose and never validates it — a site, a preview —
7
+ * would otherwise fail to load the package at all for want of a dependency
8
+ * it has no use for. The type import above is erased at build, so nothing
9
+ * here reaches zod.
10
+ */
11
+ /**
12
+ * A document with nothing in it — and one empty paragraph, not an empty
13
+ * array. ProseMirror's schema says a document is `block+`, so a `doc` with
14
+ * no children is not a document it can put a cursor in: the editor rendered
15
+ * an empty box with no paragraph, which meant no node to mark as empty and
16
+ * so no placeholder. The picture of that is why this reads the way it does.
17
+ * `isEmptyRichText` treats both forms as empty, and the schema accepts both,
18
+ * because a value that arrives from somewhere else may be either.
19
+ */
20
+ export const emptyRichText = { type: 'doc', content: [{ type: 'paragraph' }] };
21
+ /** Whether there is anything to draw, so a caller can skip an empty value rather than leave a gap. */
22
+ export function isEmptyRichText(value) {
23
+ return (value.content ?? []).every((node) => !('content' in node) || (node.content ?? []).length === 0);
24
+ }
25
+ /** The plain words, for a summary, a search index or a document's own `<title>`. */
26
+ export function richTextToPlain(value) {
27
+ const parts = [];
28
+ const walk = (nodes) => {
29
+ for (const node of nodes) {
30
+ if (!node || typeof node !== 'object')
31
+ continue;
32
+ const record = node;
33
+ if (typeof record.text === 'string')
34
+ parts.push(record.text);
35
+ if (Array.isArray(record.content))
36
+ walk(record.content);
37
+ }
38
+ };
39
+ walk(value.content ?? []);
40
+ return parts.join(' ').replace(/\s+/g, ' ').trim();
41
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The rich-text schema: `@wtfalch/design/rich-text`.
3
+ *
4
+ * What a consumer validates with on the way into its database, so that a
5
+ * crafted request can no more store a table than the toolbar can produce
6
+ * one. Separate from the front door because it is the only part of the
7
+ * package that needs zod, and separate from `./editor` because validating a
8
+ * value is a thing a server does and downloading an editor is not.
9
+ */
10
+ export { type BlockNode, type RichTextMark, type RichTextValue, blockNodeSchema, richTextMarkSchema, richTextSchema, } from './rich-text/schema.js';
11
+ export { emptyRichText, isEmptyRichText, richTextToPlain } from './rich-text/value.js';
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The rich-text schema: `@wtfalch/design/rich-text`.
3
+ *
4
+ * What a consumer validates with on the way into its database, so that a
5
+ * crafted request can no more store a table than the toolbar can produce
6
+ * one. Separate from the front door because it is the only part of the
7
+ * package that needs zod, and separate from `./editor` because validating a
8
+ * value is a thing a server does and downloading an editor is not.
9
+ */
10
+ export { blockNodeSchema, richTextMarkSchema, richTextSchema, } from './rich-text/schema.js';
11
+ export { emptyRichText, isEmptyRichText, richTextToPlain } from './rich-text/value.js';
@@ -37,6 +37,7 @@
37
37
  --text-2xs: var(--text-2xs);
38
38
  --text-md: var(--text-md);
39
39
  --font-weight-strong: var(--weight-strong);
40
+ --radius-DEFAULT: var(--radius);
40
41
  --radius-pill: var(--radius-pill);
41
42
  --color-text: var(--text);
42
43
  --color-muted: var(--muted);
@@ -173,6 +174,9 @@
173
174
  .min-h-0 {
174
175
  min-height: 0px;
175
176
  }
177
+ .min-h-\[calc\(var\(--space-15\)\*2\)\] {
178
+ min-height: calc(var(--space-15) * 2);
179
+ }
176
180
  .min-h-screen {
177
181
  min-height: 100vh;
178
182
  }
@@ -313,6 +317,9 @@
313
317
  .rounded {
314
318
  border-radius: 0.25rem;
315
319
  }
320
+ .rounded-\(--radius\) {
321
+ border-radius: var(--radius);
322
+ }
316
323
  .rounded-full {
317
324
  border-radius: calc(infinity * 1px);
318
325
  }
@@ -462,6 +469,9 @@
462
469
  .uppercase {
463
470
  text-transform: uppercase;
464
471
  }
472
+ .italic {
473
+ font-style: italic;
474
+ }
465
475
  .tabular-nums {
466
476
  --tw-numeric-spacing: tabular-nums;
467
477
  font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,);
@@ -508,9 +518,16 @@
508
518
  .surface-bg {
509
519
  background: var(--bg);
510
520
  }
521
+ .surface-control {
522
+ background: var(--control);
523
+ }
511
524
  .surface-panel {
512
525
  background: var(--panel);
513
526
  }
527
+ .focus-within\:shadow-\(--focus-ring\):focus-within {
528
+ --tw-shadow: var(--focus-ring);
529
+ box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
530
+ }
514
531
  .disabled\:cursor-default:disabled {
515
532
  cursor: default;
516
533
  }
@@ -4692,3 +4709,97 @@ body.splitting * { cursor: inherit !important; }
4692
4709
  .ident-remove:focus-visible { outline: none; box-shadow: var(--focus-ring); }
4693
4710
  }
4694
4711
 
4712
+ /* Prose and the surface it is written on, added in 0.8.0. Last, because
4713
+ `.prose` styles the elements the reset in `base.css` flattened and has to
4714
+ win over it, and because the editor's box sits over nothing. */
4715
+ /* ---- ./richtext.css ---- */
4716
+ @layer components {
4717
+ /* Prose: one class, and the elements it styles are ones nobody wrote.
4718
+ *
4719
+ * That is the whole reason a class exists here rather than utilities. Inside
4720
+ * the editor the paragraphs, headings and lists are ProseMirror's, built
4721
+ * from the document — there is no JSX to hang a utility on, so the choice is
4722
+ * descendant rules behind one class or an arbitrary variant per element,
4723
+ * which is the same selector spelled worse. `Markdown` has the same problem
4724
+ * for the same reason and answers it the same way, as `.md` in `base.css`.
4725
+ *
4726
+ * The editor and the view share the class on purpose. They have to look
4727
+ * identical and one set of rules is what guarantees it; two sets maintained
4728
+ * separately is precisely how a what-you-see-is-what-you-get editor stops
4729
+ * being one. Everything the components *do* render — the box, the toolbar,
4730
+ * the writing surface's own padding and focus ring — is utilities in the
4731
+ * component, where it belongs.
4732
+ *
4733
+ * `.rich-text` and not `.prose`: this package's classes ship unprefixed into
4734
+ * other people's stylesheets, and `.prose` is the one name in CSS most
4735
+ * likely to already be taken — Tailwind's typography plugin defines exactly
4736
+ * it. A consumer with that plugin and this package would have had two sets
4737
+ * of margins fighting, and it would have read as a theming bug.
4738
+ *
4739
+ * Every value here is a token. The vertical rhythm is `--space-*`, four
4740
+ * pixels a step and scaling with `--density`; the type steps are `--text-*`,
4741
+ * scaling with `--font-size`. A heading two sizes apart from its body at one
4742
+ * density is two sizes apart at every density.
4743
+ */
4744
+
4745
+ .rich-text > * {
4746
+ margin: 0;
4747
+ }
4748
+
4749
+ .rich-text > * + * {
4750
+ margin-top: var(--space-3);
4751
+ }
4752
+
4753
+ .rich-text h2 {
4754
+ font-size: var(--text-lg);
4755
+ font-weight: var(--weight-strong);
4756
+ letter-spacing: -0.01em;
4757
+ }
4758
+
4759
+ .rich-text h3 {
4760
+ font-size: var(--text-md);
4761
+ font-weight: var(--weight-strong);
4762
+ }
4763
+
4764
+ /* A heading after prose needs more room above it than below: the space
4765
+ belongs to the section it opens, not to the paragraph it follows. */
4766
+ .rich-text > * + :is(h2, h3) {
4767
+ margin-top: var(--space-6);
4768
+ }
4769
+
4770
+ .rich-text :is(ul, ol) {
4771
+ padding-left: var(--space-5);
4772
+ }
4773
+
4774
+ .rich-text li + li {
4775
+ margin-top: var(--space-1);
4776
+ }
4777
+
4778
+ .rich-text a {
4779
+ color: var(--accent);
4780
+ text-decoration: underline;
4781
+ text-underline-offset: 2px;
4782
+ }
4783
+
4784
+ .rich-text strong {
4785
+ font-weight: var(--weight-strong);
4786
+ }
4787
+
4788
+ /* An empty editor says what goes in it. The placeholder extension puts the
4789
+ words on the empty node as `data-placeholder` and marks it, so `attr()` is
4790
+ reading an attribute of the element it is drawn on — which is the part a
4791
+ hand-rolled rule got wrong twice: a rule on the paragraph cannot reach an
4792
+ attribute on the box, and ProseMirror's empty paragraph holds a trailing
4793
+ `<br>` so `:empty` never matches it either. `pointer-events: none` so a
4794
+ click on the words still lands in the text.
4795
+
4796
+ Harmless on the view, which never produces the class. */
4797
+ .rich-text p.is-editor-empty:first-child::before {
4798
+ content: attr(data-placeholder);
4799
+ float: left;
4800
+ height: 0;
4801
+ color: var(--muted);
4802
+ pointer-events: none;
4803
+ }
4804
+ }
4805
+
package/dist/tf.css CHANGED
@@ -307,6 +307,7 @@
307
307
  --text-2xs: var(--text-2xs);
308
308
  --text-md: var(--text-md);
309
309
  --font-weight-strong: var(--weight-strong);
310
+ --radius-DEFAULT: var(--radius);
310
311
  --radius-pill: var(--radius-pill);
311
312
  --color-text: var(--text);
312
313
  --color-muted: var(--muted);
@@ -443,6 +444,9 @@
443
444
  .min-h-0 {
444
445
  min-height: 0px;
445
446
  }
447
+ .min-h-\[calc\(var\(--space-15\)\*2\)\] {
448
+ min-height: calc(var(--space-15) * 2);
449
+ }
446
450
  .min-h-screen {
447
451
  min-height: 100vh;
448
452
  }
@@ -583,6 +587,9 @@
583
587
  .rounded {
584
588
  border-radius: 0.25rem;
585
589
  }
590
+ .rounded-\(--radius\) {
591
+ border-radius: var(--radius);
592
+ }
586
593
  .rounded-full {
587
594
  border-radius: calc(infinity * 1px);
588
595
  }
@@ -732,6 +739,9 @@
732
739
  .uppercase {
733
740
  text-transform: uppercase;
734
741
  }
742
+ .italic {
743
+ font-style: italic;
744
+ }
735
745
  .tabular-nums {
736
746
  --tw-numeric-spacing: tabular-nums;
737
747
  font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,);
@@ -778,9 +788,16 @@
778
788
  .surface-bg {
779
789
  background: var(--bg);
780
790
  }
791
+ .surface-control {
792
+ background: var(--control);
793
+ }
781
794
  .surface-panel {
782
795
  background: var(--panel);
783
796
  }
797
+ .focus-within\:shadow-\(--focus-ring\):focus-within {
798
+ --tw-shadow: var(--focus-ring);
799
+ box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
800
+ }
784
801
  .disabled\:cursor-default:disabled {
785
802
  cursor: default;
786
803
  }
@@ -4961,6 +4978,100 @@ body.splitting * { cursor: inherit !important; }
4961
4978
  .ident-remove:hover { background: var(--panel); color: var(--text); }
4962
4979
  .ident-remove:focus-visible { outline: none; box-shadow: var(--focus-ring); }
4963
4980
  }
4981
+
4982
+ /* Prose and the surface it is written on, added in 0.8.0. Last, because
4983
+ `.prose` styles the elements the reset in `base.css` flattened and has to
4984
+ win over it, and because the editor's box sits over nothing. */
4985
+ /* ---- ./richtext.css ---- */
4986
+ @layer components {
4987
+ /* Prose: one class, and the elements it styles are ones nobody wrote.
4988
+ *
4989
+ * That is the whole reason a class exists here rather than utilities. Inside
4990
+ * the editor the paragraphs, headings and lists are ProseMirror's, built
4991
+ * from the document — there is no JSX to hang a utility on, so the choice is
4992
+ * descendant rules behind one class or an arbitrary variant per element,
4993
+ * which is the same selector spelled worse. `Markdown` has the same problem
4994
+ * for the same reason and answers it the same way, as `.md` in `base.css`.
4995
+ *
4996
+ * The editor and the view share the class on purpose. They have to look
4997
+ * identical and one set of rules is what guarantees it; two sets maintained
4998
+ * separately is precisely how a what-you-see-is-what-you-get editor stops
4999
+ * being one. Everything the components *do* render — the box, the toolbar,
5000
+ * the writing surface's own padding and focus ring — is utilities in the
5001
+ * component, where it belongs.
5002
+ *
5003
+ * `.rich-text` and not `.prose`: this package's classes ship unprefixed into
5004
+ * other people's stylesheets, and `.prose` is the one name in CSS most
5005
+ * likely to already be taken — Tailwind's typography plugin defines exactly
5006
+ * it. A consumer with that plugin and this package would have had two sets
5007
+ * of margins fighting, and it would have read as a theming bug.
5008
+ *
5009
+ * Every value here is a token. The vertical rhythm is `--space-*`, four
5010
+ * pixels a step and scaling with `--density`; the type steps are `--text-*`,
5011
+ * scaling with `--font-size`. A heading two sizes apart from its body at one
5012
+ * density is two sizes apart at every density.
5013
+ */
5014
+
5015
+ .rich-text > * {
5016
+ margin: 0;
5017
+ }
5018
+
5019
+ .rich-text > * + * {
5020
+ margin-top: var(--space-3);
5021
+ }
5022
+
5023
+ .rich-text h2 {
5024
+ font-size: var(--text-lg);
5025
+ font-weight: var(--weight-strong);
5026
+ letter-spacing: -0.01em;
5027
+ }
5028
+
5029
+ .rich-text h3 {
5030
+ font-size: var(--text-md);
5031
+ font-weight: var(--weight-strong);
5032
+ }
5033
+
5034
+ /* A heading after prose needs more room above it than below: the space
5035
+ belongs to the section it opens, not to the paragraph it follows. */
5036
+ .rich-text > * + :is(h2, h3) {
5037
+ margin-top: var(--space-6);
5038
+ }
5039
+
5040
+ .rich-text :is(ul, ol) {
5041
+ padding-left: var(--space-5);
5042
+ }
5043
+
5044
+ .rich-text li + li {
5045
+ margin-top: var(--space-1);
5046
+ }
5047
+
5048
+ .rich-text a {
5049
+ color: var(--accent);
5050
+ text-decoration: underline;
5051
+ text-underline-offset: 2px;
5052
+ }
5053
+
5054
+ .rich-text strong {
5055
+ font-weight: var(--weight-strong);
5056
+ }
5057
+
5058
+ /* An empty editor says what goes in it. The placeholder extension puts the
5059
+ words on the empty node as `data-placeholder` and marks it, so `attr()` is
5060
+ reading an attribute of the element it is drawn on — which is the part a
5061
+ hand-rolled rule got wrong twice: a rule on the paragraph cannot reach an
5062
+ attribute on the box, and ProseMirror's empty paragraph holds a trailing
5063
+ `<br>` so `:empty` never matches it either. `pointer-events: none` so a
5064
+ click on the words still lands in the text.
5065
+
5066
+ Harmless on the view, which never produces the class. */
5067
+ .rich-text p.is-editor-empty:first-child::before {
5068
+ content: attr(data-placeholder);
5069
+ float: left;
5070
+ height: 0;
5071
+ color: var(--muted);
5072
+ pointer-events: none;
5073
+ }
5074
+ }
4964
5075
  /* ---- tf: the themes ---- */
4965
5076
  :root[data-theme='night']{--bg:#0f1115;--panel:#161a21;--panel-2:#1c222b;--border:#262d38;--text:#e6e9ef;--muted:#8b94a4;--accent:#5b9dff;--accent-dim:#2a4877;--app-bg:#0f1115;color-scheme:dark}
4966
5077
  :root[data-theme='paper']{--bg:#f6f7f9;--panel:#ffffff;--panel-2:#f0f2f5;--border:#e4e8ec;--border-strong:#8792a1;--text:#191d23;--muted:#5d6773;--accent:#0e7872;--accent-dim:#7fbdb8;--on-accent:#ffffff;--good:#1c7a4a;--warn:#8a6216;--bad:#b3312c;--info:#216bc9;--shadow-1:0 4px 14px rgba(16, 24, 40, 0.08);--shadow-2:0 8px 24px rgba(16, 24, 40, 0.10);--shadow-3:0 12px 32px rgba(16, 24, 40, 0.12);--scrim:rgba(16, 24, 40, 0.32);color-scheme:light}
package/dist/valet.css CHANGED
@@ -311,6 +311,7 @@
311
311
  --text-2xs: var(--text-2xs);
312
312
  --text-md: var(--text-md);
313
313
  --font-weight-strong: var(--weight-strong);
314
+ --radius-DEFAULT: var(--radius);
314
315
  --radius-pill: var(--radius-pill);
315
316
  --color-text: var(--text);
316
317
  --color-muted: var(--muted);
@@ -447,6 +448,9 @@
447
448
  .min-h-0 {
448
449
  min-height: 0px;
449
450
  }
451
+ .min-h-\[calc\(var\(--space-15\)\*2\)\] {
452
+ min-height: calc(var(--space-15) * 2);
453
+ }
450
454
  .min-h-screen {
451
455
  min-height: 100vh;
452
456
  }
@@ -587,6 +591,9 @@
587
591
  .rounded {
588
592
  border-radius: 0.25rem;
589
593
  }
594
+ .rounded-\(--radius\) {
595
+ border-radius: var(--radius);
596
+ }
590
597
  .rounded-full {
591
598
  border-radius: calc(infinity * 1px);
592
599
  }
@@ -736,6 +743,9 @@
736
743
  .uppercase {
737
744
  text-transform: uppercase;
738
745
  }
746
+ .italic {
747
+ font-style: italic;
748
+ }
739
749
  .tabular-nums {
740
750
  --tw-numeric-spacing: tabular-nums;
741
751
  font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,);
@@ -782,9 +792,16 @@
782
792
  .surface-bg {
783
793
  background: var(--bg);
784
794
  }
795
+ .surface-control {
796
+ background: var(--control);
797
+ }
785
798
  .surface-panel {
786
799
  background: var(--panel);
787
800
  }
801
+ .focus-within\:shadow-\(--focus-ring\):focus-within {
802
+ --tw-shadow: var(--focus-ring);
803
+ box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
804
+ }
788
805
  .disabled\:cursor-default:disabled {
789
806
  cursor: default;
790
807
  }
@@ -4965,6 +4982,100 @@ body.splitting * { cursor: inherit !important; }
4965
4982
  .ident-remove:hover { background: var(--panel); color: var(--text); }
4966
4983
  .ident-remove:focus-visible { outline: none; box-shadow: var(--focus-ring); }
4967
4984
  }
4985
+
4986
+ /* Prose and the surface it is written on, added in 0.8.0. Last, because
4987
+ `.prose` styles the elements the reset in `base.css` flattened and has to
4988
+ win over it, and because the editor's box sits over nothing. */
4989
+ /* ---- ./richtext.css ---- */
4990
+ @layer components {
4991
+ /* Prose: one class, and the elements it styles are ones nobody wrote.
4992
+ *
4993
+ * That is the whole reason a class exists here rather than utilities. Inside
4994
+ * the editor the paragraphs, headings and lists are ProseMirror's, built
4995
+ * from the document — there is no JSX to hang a utility on, so the choice is
4996
+ * descendant rules behind one class or an arbitrary variant per element,
4997
+ * which is the same selector spelled worse. `Markdown` has the same problem
4998
+ * for the same reason and answers it the same way, as `.md` in `base.css`.
4999
+ *
5000
+ * The editor and the view share the class on purpose. They have to look
5001
+ * identical and one set of rules is what guarantees it; two sets maintained
5002
+ * separately is precisely how a what-you-see-is-what-you-get editor stops
5003
+ * being one. Everything the components *do* render — the box, the toolbar,
5004
+ * the writing surface's own padding and focus ring — is utilities in the
5005
+ * component, where it belongs.
5006
+ *
5007
+ * `.rich-text` and not `.prose`: this package's classes ship unprefixed into
5008
+ * other people's stylesheets, and `.prose` is the one name in CSS most
5009
+ * likely to already be taken — Tailwind's typography plugin defines exactly
5010
+ * it. A consumer with that plugin and this package would have had two sets
5011
+ * of margins fighting, and it would have read as a theming bug.
5012
+ *
5013
+ * Every value here is a token. The vertical rhythm is `--space-*`, four
5014
+ * pixels a step and scaling with `--density`; the type steps are `--text-*`,
5015
+ * scaling with `--font-size`. A heading two sizes apart from its body at one
5016
+ * density is two sizes apart at every density.
5017
+ */
5018
+
5019
+ .rich-text > * {
5020
+ margin: 0;
5021
+ }
5022
+
5023
+ .rich-text > * + * {
5024
+ margin-top: var(--space-3);
5025
+ }
5026
+
5027
+ .rich-text h2 {
5028
+ font-size: var(--text-lg);
5029
+ font-weight: var(--weight-strong);
5030
+ letter-spacing: -0.01em;
5031
+ }
5032
+
5033
+ .rich-text h3 {
5034
+ font-size: var(--text-md);
5035
+ font-weight: var(--weight-strong);
5036
+ }
5037
+
5038
+ /* A heading after prose needs more room above it than below: the space
5039
+ belongs to the section it opens, not to the paragraph it follows. */
5040
+ .rich-text > * + :is(h2, h3) {
5041
+ margin-top: var(--space-6);
5042
+ }
5043
+
5044
+ .rich-text :is(ul, ol) {
5045
+ padding-left: var(--space-5);
5046
+ }
5047
+
5048
+ .rich-text li + li {
5049
+ margin-top: var(--space-1);
5050
+ }
5051
+
5052
+ .rich-text a {
5053
+ color: var(--accent);
5054
+ text-decoration: underline;
5055
+ text-underline-offset: 2px;
5056
+ }
5057
+
5058
+ .rich-text strong {
5059
+ font-weight: var(--weight-strong);
5060
+ }
5061
+
5062
+ /* An empty editor says what goes in it. The placeholder extension puts the
5063
+ words on the empty node as `data-placeholder` and marks it, so `attr()` is
5064
+ reading an attribute of the element it is drawn on — which is the part a
5065
+ hand-rolled rule got wrong twice: a rule on the paragraph cannot reach an
5066
+ attribute on the box, and ProseMirror's empty paragraph holds a trailing
5067
+ `<br>` so `:empty` never matches it either. `pointer-events: none` so a
5068
+ click on the words still lands in the text.
5069
+
5070
+ Harmless on the view, which never produces the class. */
5071
+ .rich-text p.is-editor-empty:first-child::before {
5072
+ content: attr(data-placeholder);
5073
+ float: left;
5074
+ height: 0;
5075
+ color: var(--muted);
5076
+ pointer-events: none;
5077
+ }
5078
+ }
4968
5079
  /* ---- valet: the themes ---- */
4969
5080
  :root[data-theme='valet']{--bg:#f4f5f8;--panel:#ffffff;--panel-2:#eceef3;--border:#dcdfe7;--border-strong:#7b8597;--text:#171a21;--muted:#5b6474;--accent:#4f46e5;--accent-dim:#a9a4f0;--on-accent:#ffffff;--good:#1b7f4b;--warn:#8a5f0a;--bad:#bf3a31;--info:#0e6f8e;--app-bg:#f4f5f8;--shadow-1:0 4px 14px rgba(23, 26, 33, 0.08);--shadow-2:0 8px 24px rgba(23, 26, 33, 0.10);--shadow-3:0 12px 32px rgba(23, 26, 33, 0.12);--scrim:rgba(23, 26, 33, 0.32);color-scheme:light}
4970
5081
  :root[data-theme='valet-night']{--bg:#0c0f14;--panel:#141820;--panel-2:#1b2029;--border:#262c37;--border-strong:#616b7d;--text:#e8eaf0;--muted:#9ba4b5;--accent:#8f88ff;--accent-dim:#3f3a8f;--on-accent:#0d0b2e;--good:#5fcb8f;--warn:#e2ae58;--bad:#f28b84;--info:#57c4e8;--app-bg:#0c0f14;--shadow-1:0 6px 20px rgba(0, 0, 0, 0.28);--shadow-2:0 8px 28px rgba(0, 0, 0, 0.34);--shadow-3:0 10px 34px rgba(0, 0, 0, 0.38);--scrim:rgba(0, 0, 0, 0.5);color-scheme:dark}