@nuucognition/ncm-ui 0.1.0-dev.1 → 0.1.1-beta.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.
Files changed (74) hide show
  1. package/README.md +90 -0
  2. package/dist/blocks/admonition/index.d.ts +2 -3
  3. package/dist/blocks/admonition/index.js +79 -23
  4. package/dist/blocks/aside/index.d.ts +2 -3
  5. package/dist/blocks/aside/index.js +79 -23
  6. package/dist/blocks/attribution/index.d.ts +2 -3
  7. package/dist/blocks/blockquote/index.d.ts +5 -4
  8. package/dist/blocks/blockquote/index.js +52 -7
  9. package/dist/blocks/chart/index.d.ts +2 -3
  10. package/dist/blocks/chart/index.js +2 -1
  11. package/dist/blocks/code/index.d.ts +2 -3
  12. package/dist/blocks/code/index.js +2 -2
  13. package/dist/blocks/container/index.d.ts +2 -3
  14. package/dist/blocks/container/index.js +50 -5
  15. package/dist/blocks/custom-code/index.d.ts +2 -3
  16. package/dist/blocks/custom-code/index.js +1 -1
  17. package/dist/blocks/dataview/index.d.ts +2 -3
  18. package/dist/blocks/dataview/index.js +2 -1
  19. package/dist/blocks/definition/index.d.ts +2 -3
  20. package/dist/blocks/definition/index.js +79 -23
  21. package/dist/blocks/embed/index.d.ts +2 -3
  22. package/dist/blocks/embedding/index.d.ts +2 -3
  23. package/dist/blocks/embedding/index.js +50 -5
  24. package/dist/blocks/figure/index.d.ts +2 -3
  25. package/dist/blocks/figure/index.js +50 -5
  26. package/dist/blocks/footnote/index.d.ts +2 -3
  27. package/dist/blocks/footnote/index.js +79 -23
  28. package/dist/blocks/frontmatter/index.d.ts +2 -3
  29. package/dist/blocks/header/index.d.ts +2 -3
  30. package/dist/blocks/header/index.js +50 -5
  31. package/dist/blocks/horizontal-rule/index.d.ts +2 -3
  32. package/dist/blocks/html/index.d.ts +3 -4
  33. package/dist/blocks/html/index.js +2 -1
  34. package/dist/blocks/index.d.ts +1 -2
  35. package/dist/blocks/index.js +79 -23
  36. package/dist/blocks/latex/index.d.ts +2 -3
  37. package/dist/blocks/latex/index.js +2 -2
  38. package/dist/blocks/link/index.d.ts +2 -3
  39. package/dist/blocks/list/index.d.ts +2 -3
  40. package/dist/blocks/list/index.js +57 -7
  41. package/dist/blocks/math/index.d.ts +2 -3
  42. package/dist/blocks/math/index.js +3 -2
  43. package/dist/blocks/mermaid/index.d.ts +2 -3
  44. package/dist/blocks/mermaid/index.js +2 -1
  45. package/dist/blocks/page-breaker/index.d.ts +2 -3
  46. package/dist/blocks/paragraph/index.d.ts +2 -3
  47. package/dist/blocks/paragraph/index.js +50 -5
  48. package/dist/blocks/spoiler/index.d.ts +2 -3
  49. package/dist/blocks/spoiler/index.js +79 -23
  50. package/dist/blocks/table/index.d.ts +2 -3
  51. package/dist/blocks/table/index.js +51 -6
  52. package/dist/blocks/task-list/index.d.ts +2 -3
  53. package/dist/blocks/task-list/index.js +52 -6
  54. package/dist/components/index.d.ts +34 -4
  55. package/dist/components/index.js +117 -25
  56. package/dist/components/ncm-block.d.ts +2 -3
  57. package/dist/components/ncm-block.js +79 -23
  58. package/dist/components/ncm-document.d.ts +4 -4
  59. package/dist/components/ncm-document.js +79 -23
  60. package/dist/context/index.d.ts +2 -3
  61. package/dist/hooks/index.d.ts +1 -2
  62. package/dist/hooks/index.js +79 -23
  63. package/dist/hooks/use-block-renderer.js +79 -23
  64. package/dist/hooks/use-ncm-config.d.ts +1 -2
  65. package/dist/index.d.ts +3 -4
  66. package/dist/index.js +118 -26
  67. package/dist/inline/index.d.ts +4 -4
  68. package/dist/inline/index.js +51 -6
  69. package/dist/lib/defaults.js +79 -23
  70. package/dist/{references-Bs5wJybr.d.ts → references-BYW5pnj_.d.ts} +4 -4
  71. package/dist/styles/nuu.css +329 -44
  72. package/dist/styles/paper.css +62 -1
  73. package/dist/{use-ncm-config-D0doMYTH.d.ts → use-ncm-config-CnntcShj.d.ts} +55 -4
  74. package/package.json +5 -5
@@ -8,10 +8,25 @@
8
8
  * Selectors target the data attributes emitted by ncm-ui block components.
9
9
  */
10
10
 
11
+ /*
12
+ * Read mode and edit mode are ONE document at two moments, so they render at
13
+ * one size. The base and the heading ladder below are copied from the editor's
14
+ * own scale (`@nuucognition/nuu-editor`, `src/nuu-editor/nuu-editor.scss`):
15
+ * 1rem body at 1.65, headings 1.75 / 1.56 / 1.39 / 1.24 / 1.1em. Switching
16
+ * view must not resize a single glyph. Since the flow-block rhythm pass, the
17
+ * MARGINS are part of the same contract: the editor states this file's block
18
+ * gaps (1rem between siblings, per-level rem heading margins, the 1.5rem /
19
+ * 0.125rem list geometry) per element on its side. If one side of that pair
20
+ * moves, move the other in the same change.
21
+ *
22
+ * `--surf-reading-scale` is the reader's own text-size setting and stays on
23
+ * the base alone; everything under it is expressed in `em`, so one multiply
24
+ * carries the whole document.
25
+ */
11
26
  [data-ncm-document].ncm-theme-nuu {
12
27
  font-family: var(--font-sans);
13
- font-size: calc(0.875rem * var(--surf-reading-scale, 1));
14
- line-height: 1.55;
28
+ font-size: calc(1rem * var(--surf-reading-scale, 1));
29
+ line-height: 1.65;
15
30
  color: var(--foreground);
16
31
  }
17
32
 
@@ -19,7 +34,7 @@
19
34
  margin-top: 1rem;
20
35
  }
21
36
 
22
- /* Headings — Geist Sans, tracking-tight, normal weight on h1–h3 */
37
+ /* Headings — the editor ladder: one 1.12 ratio, h5 a clear step above body */
23
38
  [data-ncm-document].ncm-theme-nuu [data-ncm-type="header"] {
24
39
  font-family: var(--font-heading);
25
40
  color: var(--foreground);
@@ -28,10 +43,10 @@
28
43
  }
29
44
 
30
45
  [data-ncm-document].ncm-theme-nuu [data-ncm-type="header"][data-ncm-level="1"] {
31
- font-size: calc(1.875rem * var(--surf-reading-scale, 1));
32
- line-height: 2.25rem;
33
- letter-spacing: -0.025em;
34
- font-weight: 400;
46
+ font-size: 1.75em;
47
+ line-height: 1.3;
48
+ letter-spacing: -0.01em;
49
+ font-weight: 700;
35
50
  margin-top: 1.5rem;
36
51
  }
37
52
 
@@ -40,41 +55,105 @@
40
55
  }
41
56
 
42
57
  [data-ncm-document].ncm-theme-nuu [data-ncm-type="header"][data-ncm-level="2"] {
43
- font-size: calc(1.5rem * var(--surf-reading-scale, 1));
44
- line-height: 2rem;
45
- letter-spacing: -0.025em;
46
- font-weight: 400;
58
+ font-size: 1.56em;
59
+ line-height: 1.3;
60
+ letter-spacing: -0.01em;
61
+ font-weight: 660;
47
62
  margin-top: 1.25rem;
48
63
  }
49
64
 
50
65
  [data-ncm-document].ncm-theme-nuu [data-ncm-type="header"][data-ncm-level="3"] {
51
- font-size: calc(1.25rem * var(--surf-reading-scale, 1));
52
- line-height: 1.75rem;
53
- letter-spacing: -0.025em;
54
- font-weight: 400;
66
+ font-size: 1.39em;
67
+ line-height: 1.3;
68
+ font-weight: 620;
55
69
  margin-top: 1rem;
56
70
  }
57
71
 
72
+ /* h4/h5 open at 1.25rem, above the 1rem gap that follows them: a heading must
73
+ sit closer to the section it introduces than to the text it ends. At the
74
+ old 0.75rem the binding was inverted. */
58
75
  [data-ncm-document].ncm-theme-nuu [data-ncm-type="header"][data-ncm-level="4"] {
59
- font-size: calc(0.875rem * var(--surf-reading-scale, 1));
60
- line-height: 1.25rem;
61
- font-weight: 500;
62
- margin-top: 0.75rem;
76
+ font-size: 1.24em;
77
+ line-height: 1.3;
78
+ font-weight: 600;
79
+ margin-top: 1.25rem;
63
80
  }
64
81
 
65
82
  [data-ncm-document].ncm-theme-nuu [data-ncm-type="header"][data-ncm-level="5"] {
66
- font-size: calc(0.75rem * var(--surf-reading-scale, 1));
67
- line-height: 1rem;
68
- font-weight: 500;
69
- letter-spacing: 0.05em;
70
- text-transform: uppercase;
71
- margin-top: 0.75rem;
83
+ font-size: 1.1em;
84
+ line-height: 1.3;
85
+ font-weight: 600;
86
+ margin-top: 1.25rem;
72
87
  }
73
88
 
74
89
  /* Paragraph */
75
90
  [data-ncm-document].ncm-theme-nuu [data-ncm-type="paragraph"] {
76
91
  color: var(--foreground);
77
- line-height: 1.55;
92
+ line-height: 1.65;
93
+ }
94
+
95
+ /*
96
+ * Callout content keeps its line breaks. NCM parses a callout's quoted lines
97
+ * into paragraph content WITH the newlines preserved ("line one\nline two"),
98
+ * and default HTML whitespace collapsing was rendering them as one
99
+ * space-joined line — an author who wrote three lines in a callout saw one.
100
+ * Scoped to admonitions: everywhere else soft-wrapped source keeps its
101
+ * CommonMark space-join.
102
+ */
103
+ [data-ncm-document].ncm-theme-nuu [data-ncm-admonition] [data-ncm-type="paragraph"] {
104
+ white-space: pre-wrap;
105
+ }
106
+
107
+ /*
108
+ * Callout (admonition) — a quiet card: rounded `md` corners (the NUU chrome
109
+ * step, stated as radius × 0.8 so a host's `--radius` carries through), 1px
110
+ * hairline border, a 2px colored left edge (the blockquote's own weight),
111
+ * tight padding. Rounded since (Task) 581, superseding the sharp-corner line
112
+ * of the minimalist pass — everything else from that pass stands. The type
113
+ * supplies only colors, through the `--ncm-callout-*` hooks; a host assigns
114
+ * them per `data-ncm-admonition-type` (Onyx does, from its palette
115
+ * families). Unset, every hook falls back to the neutral hairline card, so
116
+ * a host with no callout stylesheet — and any type a host does not cover —
117
+ * renders the same quiet shell with no color.
118
+ *
119
+ * These rules override the component's own rounded-card utilities
120
+ * (`rounded-lg bg-card p-4`): this file is unlayered and Tailwind utilities
121
+ * are layered, so the unlayered declarations win regardless of specificity.
122
+ * The hooks are set by hosts at ANY specificity — this file deliberately
123
+ * never assigns them, only reads them with fallbacks.
124
+ */
125
+ [data-ncm-document].ncm-theme-nuu [data-ncm-admonition] {
126
+ border: 1px solid var(--ncm-callout-border, var(--border));
127
+ border-left: 2px solid var(--ncm-callout-edge, var(--border));
128
+ border-radius: calc(var(--radius, 0.625rem) * 0.8);
129
+ padding: 0.625rem 0.875rem;
130
+ background-color: var(--ncm-callout-tint, transparent);
131
+ /*
132
+ * An unbroken word must stay inside the card. Hosts lay the title out as a
133
+ * flex row (glyph + text), and a flex item's min-content size is the whole
134
+ * unbreakable word — the text ran past the card edge. `anywhere`, not
135
+ * `break-word`: only `anywhere` lets the break point count toward the
136
+ * min-content size, which is what the flex layout measures. Inherited, so
137
+ * the title and every content paragraph get it. ((Task) 581)
138
+ */
139
+ overflow-wrap: anywhere;
140
+ }
141
+
142
+ /*
143
+ * One gap for both title shapes. The static `<p>` title ships `mb-2` and the
144
+ * collapsible variant instead puts `mt-2` on the content that follows the
145
+ * `<summary>` — two different gaps for one anatomy. Zero the title's own
146
+ * margin and state the gap once on whatever follows the title; a collapsed
147
+ * `<details>` renders no content and so gets no stray bottom space.
148
+ */
149
+ [data-ncm-document].ncm-theme-nuu [data-ncm-admonition] > [data-ncm-admonition-title] {
150
+ margin-bottom: 0;
151
+ color: var(--ncm-callout-title, var(--foreground));
152
+ font-weight: 600;
153
+ }
154
+
155
+ [data-ncm-document].ncm-theme-nuu [data-ncm-admonition] > [data-ncm-admonition-title] + * {
156
+ margin-top: 0.375rem;
78
157
  }
79
158
 
80
159
  /* Blockquote */
@@ -82,7 +161,7 @@
82
161
  border-left: 2px solid var(--border);
83
162
  padding-left: 0.875rem;
84
163
  color: color-mix(in srgb, var(--foreground) 80%, transparent);
85
- line-height: 1.55;
164
+ line-height: 1.65;
86
165
  }
87
166
 
88
167
  [data-ncm-document].ncm-theme-nuu [data-ncm-type="blockquote"] p + p {
@@ -90,11 +169,16 @@
90
169
  }
91
170
 
92
171
  /* Lists */
172
+ /*
173
+ * 1.5rem is the ONE marker-slot width all three list kinds share — disc,
174
+ * decimal, and the task checkbox (1em box + 0.5rem gap) — so the text column
175
+ * is identical across them at every depth.
176
+ */
93
177
  [data-ncm-document].ncm-theme-nuu [data-ncm-type="list_ordered"],
94
178
  [data-ncm-document].ncm-theme-nuu [data-ncm-type="list_unordered"] {
95
- padding-left: 1.25rem;
179
+ padding-left: 1.5rem;
96
180
  color: var(--foreground);
97
- line-height: 1.55;
181
+ line-height: 1.65;
98
182
  }
99
183
 
100
184
  [data-ncm-document].ncm-theme-nuu [data-ncm-type="list_ordered"] {
@@ -115,7 +199,7 @@
115
199
  [data-ncm-document].ncm-theme-nuu [data-ncm-type="list_unordered"] ol,
116
200
  [data-ncm-document].ncm-theme-nuu [data-ncm-type="list_unordered"] ul {
117
201
  margin-top: 0.125rem;
118
- padding-left: 1.25rem;
202
+ padding-left: 1.5rem;
119
203
  }
120
204
 
121
205
  [data-ncm-document].ncm-theme-nuu [data-ncm-type="list_ordered"] li::marker,
@@ -136,6 +220,90 @@
136
220
  list-style-type: decimal;
137
221
  }
138
222
 
223
+ /*
224
+ * Task list — one spec with the editor's `ul[data-type="taskList"]` block in
225
+ * `@nuucognition/nuu-editor` (`src/nuu-editor/nuu-editor.scss`): flush-left
226
+ * rows 0.125rem apart, a 1em checkbox rounded 0.25rem (the nuu-ui Checkbox
227
+ * radius) drawn from theme tokens (border-token box, foreground fill when
228
+ * checked, background-coloured check), 0.5rem to the text — 1em + 0.5rem =
229
+ * the shared 1.5rem marker slot that lines task text up with disc and
230
+ * decimal text. Checked text dims to the muted tone with NO strike-through.
231
+ * A nested task list carries NO indent of its own: the checkbox slot IS the
232
+ * indent, the same 1.5rem step the other list kinds get from padding. If one
233
+ * side of that pair moves, move the other in the same change.
234
+ *
235
+ * The checkbox is drawn with `appearance: none` because the native control
236
+ * neither themes nor matches the editor's; the input stays a real (disabled)
237
+ * checkbox for the accessibility tree. The `> li` rule neutralizes the
238
+ * component's own Tailwind row gap so the 0.125rem list rhythm is the one
239
+ * answer.
240
+ */
241
+ [data-ncm-document].ncm-theme-nuu [data-ncm-task-list] {
242
+ padding-left: 0;
243
+ color: var(--foreground);
244
+ line-height: 1.65;
245
+ }
246
+
247
+ [data-ncm-document].ncm-theme-nuu [data-ncm-task-list] > li:not(:last-child) {
248
+ margin-bottom: 0;
249
+ }
250
+
251
+ [data-ncm-document].ncm-theme-nuu [data-ncm-task-list] li + li {
252
+ margin-top: 0.125rem;
253
+ }
254
+
255
+ [data-ncm-document].ncm-theme-nuu [data-ncm-task-list] [data-ncm-list-nested] {
256
+ /* No margin: the nested row's own checkbox slot (1em + 0.5rem) is the
257
+ 1.5rem indent, the same step disc and decimal get from padding. */
258
+ margin-left: 0;
259
+ margin-top: 0.125rem;
260
+ }
261
+
262
+ /* Top-aligned like the editor (whose checkbox sits 0.375rem into the row):
263
+ on a wrapped item the box stays on the first line instead of centring
264
+ across the whole item. */
265
+ [data-ncm-document].ncm-theme-nuu [data-ncm-task-item] > span {
266
+ gap: 0.5rem;
267
+ align-items: flex-start;
268
+ }
269
+
270
+ [data-ncm-document].ncm-theme-nuu [data-ncm-task-item] input[type="checkbox"] {
271
+ appearance: none;
272
+ -webkit-appearance: none;
273
+ width: 1em;
274
+ height: 1em;
275
+ margin: 0.375rem 0 0;
276
+ border: 1px solid var(--border);
277
+ /* nuu-ui's Checkbox radius (rounded-[4px]). */
278
+ border-radius: 0.25rem;
279
+ background-color: transparent;
280
+ position: relative;
281
+ }
282
+
283
+ [data-ncm-document].ncm-theme-nuu [data-ncm-task-item] input[type="checkbox"]:checked {
284
+ background-color: var(--foreground);
285
+ border-color: var(--foreground);
286
+ }
287
+
288
+ [data-ncm-document].ncm-theme-nuu [data-ncm-task-item] input[type="checkbox"]:checked::before {
289
+ content: "";
290
+ position: absolute;
291
+ left: 50%;
292
+ top: 50%;
293
+ transform: translate(-50%, -50%);
294
+ width: 0.75em;
295
+ height: 0.75em;
296
+ background-color: var(--background);
297
+ -webkit-mask: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22currentColor%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M21.4142%204.58579C22.1953%205.36683%2022.1953%206.63317%2021.4142%207.41421L10.4142%2018.4142C9.63317%2019.1953%208.36684%2019.1953%207.58579%2018.4142L2.58579%2013.4142C1.80474%2012.6332%201.80474%2011.3668%202.58579%2010.5858C3.36683%209.80474%204.63317%209.80474%205.41421%2010.5858L9%2014.1716L18.5858%204.58579C19.3668%203.80474%2020.6332%203.80474%2021.4142%204.58579Z%22%20fill%3D%22currentColor%22%2F%3E%3C%2Fsvg%3E")
298
+ center/contain no-repeat;
299
+ mask: url("data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22currentColor%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M21.4142%204.58579C22.1953%205.36683%2022.1953%206.63317%2021.4142%207.41421L10.4142%2018.4142C9.63317%2019.1953%208.36684%2019.1953%207.58579%2018.4142L2.58579%2013.4142C1.80474%2012.6332%201.80474%2011.3668%202.58579%2010.5858C3.36683%209.80474%204.63317%209.80474%205.41421%2010.5858L9%2014.1716L18.5858%204.58579C19.3668%203.80474%2020.6332%203.80474%2021.4142%204.58579Z%22%20fill%3D%22currentColor%22%2F%3E%3C%2Fsvg%3E")
300
+ center/contain no-repeat;
301
+ }
302
+
303
+ [data-ncm-document].ncm-theme-nuu [data-ncm-task-item][data-ncm-checked="true"] > span > span {
304
+ color: var(--muted-foreground);
305
+ }
306
+
139
307
  /* Tables */
140
308
  [data-ncm-document].ncm-theme-nuu [data-ncm-type="table"] {
141
309
  width: 100%;
@@ -145,7 +313,7 @@
145
313
  [data-ncm-document].ncm-theme-nuu [data-ncm-type="table"] table {
146
314
  width: 100%;
147
315
  border-collapse: collapse;
148
- font-size: calc(0.75rem * var(--surf-reading-scale, 1));
316
+ font-size: 0.857em;
149
317
  }
150
318
 
151
319
  [data-ncm-document].ncm-theme-nuu [data-ncm-type="table"] th {
@@ -168,27 +336,50 @@
168
336
  background-color: color-mix(in srgb, var(--muted) 50%, transparent);
169
337
  }
170
338
 
339
+ /* An empty cell keeps its line box, so an entirely blank row renders at full
340
+ height instead of collapsing to its padding. */
341
+ [data-ncm-document].ncm-theme-nuu [data-ncm-type="table"] th:empty::after,
342
+ [data-ncm-document].ncm-theme-nuu [data-ncm-type="table"] td:empty::after {
343
+ content: "\00a0";
344
+ }
345
+
171
346
  /* Code (block) */
172
347
  [data-ncm-document].ncm-theme-nuu [data-ncm-type="code"] {
173
348
  position: relative;
174
349
  }
175
350
 
351
+ /* Minimalist, matching the editor's code block at rest: bare text — no fill,
352
+ no visible box. The border keeps its 1px width as transparent so the text
353
+ sits at exactly the editor's inset (its border reveals on hover/caret). The
354
+ editor's `pre` rule mirrors this — move both together. */
176
355
  [data-ncm-document].ncm-theme-nuu [data-ncm-type="code"] pre {
177
356
  overflow-x: auto;
178
- border: 1px solid var(--border);
179
- border-radius: var(--radius, 0.625rem);
357
+ border: 1px solid transparent;
358
+ border-radius: 0;
180
359
  padding: 0.75rem;
181
- background-color: var(--muted);
360
+ background-color: transparent;
182
361
  color: var(--foreground);
183
362
  font-family: var(--font-mono);
184
- font-size: calc(0.875rem * var(--surf-reading-scale, 1));
185
- line-height: 1.6;
363
+ font-size: 0.875em;
364
+ line-height: 1.55;
365
+ }
366
+
367
+ /* Parse warning (NcmBlockPreview) — the reader-mode display for a block
368
+ source that cannot be parsed as its block type. Minimalist: muted text,
369
+ hairline border, sharp corners. */
370
+ [data-ncm-parse-warning] {
371
+ border: 1px solid var(--border);
372
+ padding: 0.5rem 0.75rem;
373
+ color: var(--muted-foreground);
374
+ font-family: var(--font-sans);
375
+ font-size: 0.857em;
376
+ line-height: 1.5;
186
377
  }
187
378
 
188
379
  /* Inline code */
189
380
  [data-ncm-document].ncm-theme-nuu [data-ncm-token="code"] {
190
381
  font-family: var(--font-mono);
191
- font-size: calc(0.75rem * var(--surf-reading-scale, 1));
382
+ font-size: 0.857em;
192
383
  padding: 0.125rem 0.25rem;
193
384
  border-radius: 0.25rem;
194
385
  background-color: var(--muted);
@@ -212,17 +403,111 @@
212
403
  text-underline-offset: 4px;
213
404
  }
214
405
 
406
+ /*
407
+ * AMBIGUOUS IS A WARNING, NOT A FAILURE — and this rule used to say otherwise.
408
+ *
409
+ * It painted `--fire-text`/`--destructive`, i.e. RED, while the EDIT surface
410
+ * paints red for a MISSING link and amber for an ambiguous one. The same hue
411
+ * therefore carried two different facts depending on which mode the reader was
412
+ * in, which is worse than either surface being wrong on its own: a reader who
413
+ * learns what red means in one mode is actively misled by the other.
414
+ *
415
+ * The vocabulary this settles on, ruled in room `onyx-uiux` on 2026-07-31 and
416
+ * applied across both surfaces:
417
+ *
418
+ * missing pending — muted foreground, dashed. A link to a document that
419
+ * is not in this mesh is very often a PLAN, not a mistake; that is
420
+ * also the Obsidian benchmark, where an unresolved link is the
421
+ * link colour faded and never red.
422
+ * ambiguous warning — amber, dotted. Several targets match; the author has
423
+ * something to decide, but nothing has failed.
424
+ * red reserved for GENUINE FAILURE — in this package, an embed whose
425
+ * bytes did not arrive (see `renderImageError`).
426
+ *
427
+ * A DELIBERATE VISUAL CHANGE FOR EVERY EMBEDDER, not just for Onyx: an
428
+ * ambiguous reference stops being drawn as an error. Recorded in the README.
429
+ */
215
430
  [data-ncm-document].ncm-theme-nuu [data-ncm-token="wiki_link"][data-ncm-wikilink-status="ambiguous"],
216
431
  [data-ncm-document].ncm-theme-nuu [data-ncm-token="wiki_embed"][data-ncm-wikiembed-status="ambiguous"] {
217
- color: var(--fire-text, var(--destructive));
432
+ color: var(--sun-text, var(--warning, #b45309));
433
+ text-decoration: underline dotted;
434
+ text-underline-offset: 4px;
435
+ }
436
+
437
+ /*
438
+ * Wikilink focus ring.
439
+ *
440
+ * `renderer.tsx` already emits a real <a> or <button> for an actionable
441
+ * wikilink, so read-mode links have ALWAYS been in the tab order — a keyboard
442
+ * user could reach them and had no way to see that they had. Measured on the
443
+ * Onyx read surface in both themes: outline-style none, box-shadow none, while
444
+ * the surrounding chrome buttons showed a ring. That is the whole of
445
+ * known-adopted issue 095#5, and it belongs here rather than in a host: an
446
+ * embedder gets no focus affordance for these elements from anywhere else.
447
+ *
448
+ * A NEW rule, touching no existing selector. It paints only when a host has
449
+ * made the link actionable AND the browser is in keyboard-focus mode, so no
450
+ * rendering that exists today changes.
451
+ *
452
+ * (Landed as the negotiated ncm-ui freeze exception for Task 507, approved in
453
+ * room `onyx-uiux` on 2026-07-31. The TSX half of the original proposal was
454
+ * withdrawn: the button is focusable by construction and needed nothing.)
455
+ */
456
+ [data-ncm-document].ncm-theme-nuu [data-ncm-token="wiki_link"]:focus-visible {
457
+ outline: 2px solid var(--ring, currentColor);
458
+ outline-offset: 2px;
459
+ border-radius: 0.1875rem;
460
+ }
461
+
462
+ /*
463
+ * Pointer cursor for the ACTIONABLE wikilink elements — the <a> a host's
464
+ * `resolveWikiLinkHref` produces and the <button> a bare `onWikiLinkClick`
465
+ * produces. The anchor has the pointer from the user agent already; the button
466
+ * does not — Tailwind v4's preflight leaves buttons on the default arrow, so in
467
+ * Onyx a clickable wikilink did not read as clickable. The element selectors
468
+ * exclude the inert <span> a host with neither config gets, which stays on the
469
+ * text cursor on purpose. (Task 558.)
470
+ */
471
+ [data-ncm-document].ncm-theme-nuu a[data-ncm-token="wiki_link"],
472
+ [data-ncm-document].ncm-theme-nuu button[data-ncm-token="wiki_link"] {
473
+ cursor: pointer;
218
474
  }
219
475
 
220
- /* Horizontal rule */
476
+ /*
477
+ * The embed BUTTON — the element `EmbeddingsConfig.onEmbedClick` produces.
478
+ *
479
+ * Both rules below can only ever match an element that does not exist unless a
480
+ * host opted in, so neither can change a rendering that ships today.
481
+ *
482
+ * The reset first: a `<button>` in the middle of a sentence arrives wearing the
483
+ * user agent's chrome — its own background, border, padding and font — and an
484
+ * embedded document is a piece of the prose, not a form control. Onyx never saw
485
+ * this because Tailwind's preflight resets buttons app-wide; an embedder
486
+ * without one would have. Then the ring, the sibling of the wikilink rule
487
+ * directly above: the button is a tab stop by construction, so it needs to be
488
+ * able to say when it has been reached.
489
+ */
490
+ [data-ncm-document].ncm-theme-nuu button[data-ncm-token="wiki_embed"] {
491
+ appearance: none;
492
+ padding: 0;
493
+ border: 0;
494
+ background: none;
495
+ font: inherit;
496
+ cursor: pointer;
497
+ }
498
+
499
+ [data-ncm-document].ncm-theme-nuu [data-ncm-token="wiki_embed"]:focus-visible {
500
+ outline: 2px solid var(--ring, currentColor);
501
+ outline-offset: 2px;
502
+ border-radius: 0.1875rem;
503
+ }
504
+
505
+ /* Horizontal rule — 1em on both sides, matching the editor's hr rule. */
221
506
  [data-ncm-document].ncm-theme-nuu [data-ncm-type="horizontal_rule"] {
222
507
  border: 0;
223
508
  border-top: 1px solid var(--border);
224
- margin-top: 1.25rem;
225
- margin-bottom: 1.25rem;
509
+ margin-top: 1em;
510
+ margin-bottom: 1em;
226
511
  }
227
512
 
228
513
  /* Embeddings */
@@ -235,7 +520,7 @@
235
520
  [data-ncm-document].ncm-theme-nuu [data-ncm-type="embedding"] figcaption {
236
521
  margin-top: 0.5rem;
237
522
  text-align: left;
238
- font-size: 0.75rem;
523
+ font-size: 0.857em;
239
524
  color: var(--muted-foreground);
240
525
  }
241
526
 
@@ -253,7 +538,7 @@
253
538
  background-color: var(--muted);
254
539
  color: var(--foreground);
255
540
  font-family: var(--font-mono);
256
- font-size: 0.875rem;
541
+ font-size: 1em;
257
542
  }
258
543
 
259
544
  /* OpenDyslexic3 — newest, refined letterforms of the OpenDyslexic family
@@ -344,7 +629,7 @@
344
629
  background: var(--popover, var(--background, var(--muted)));
345
630
  color: var(--popover-foreground, var(--foreground));
346
631
  box-shadow: 0 4px 12px rgb(0 0 0 / 0.12);
347
- font-size: calc(0.8125rem * var(--surf-reading-scale, 1));
632
+ font-size: 0.929em;
348
633
  font-weight: 400;
349
634
  line-height: 1.5;
350
635
  white-space: normal;
@@ -172,6 +172,13 @@
172
172
  color: var(--muted-foreground);
173
173
  }
174
174
 
175
+ /* An empty cell keeps its line box, so an entirely blank row renders at full
176
+ height instead of collapsing to its padding. */
177
+ [data-ncm-document].ncm-theme-paper [data-ncm-type="table"] th:empty::after,
178
+ [data-ncm-document].ncm-theme-paper [data-ncm-type="table"] td:empty::after {
179
+ content: "\00a0";
180
+ }
181
+
175
182
  /* Code (block) */
176
183
  [data-ncm-document].ncm-theme-paper [data-ncm-type="code"] {
177
184
  position: relative;
@@ -218,9 +225,63 @@
218
225
  text-underline-offset: 3px;
219
226
  }
220
227
 
228
+ /*
229
+ * Ambiguous is a warning, not a failure — the paper sibling of the rule in
230
+ * `nuu.css`, where the whole vocabulary argument is written down. Same change,
231
+ * same reason, kept in step so the two themes cannot disagree about what a
232
+ * colour means. Paper's dotted underline is 1px-thin and offset 3px like every
233
+ * other decoration in this sheet.
234
+ */
221
235
  [data-ncm-document].ncm-theme-paper [data-ncm-token="wiki_link"][data-ncm-wikilink-status="ambiguous"],
222
236
  [data-ncm-document].ncm-theme-paper [data-ncm-token="wiki_embed"][data-ncm-wikiembed-status="ambiguous"] {
223
- color: var(--fire-text, var(--destructive));
237
+ color: var(--sun-text, var(--warning, #b45309));
238
+ text-decoration: underline dotted;
239
+ text-decoration-thickness: 1px;
240
+ text-underline-offset: 3px;
241
+ }
242
+
243
+ /*
244
+ * Wikilink focus ring — the paper sibling of the rule in `nuu.css`; see that
245
+ * file for the full reasoning. Kept in step deliberately: a focus affordance
246
+ * that exists in one theme and not the other is a keyboard user's experience
247
+ * changing with a setting that is supposed to be about typography.
248
+ *
249
+ * `--ring` with a `currentColor` fallback, so a paper host that defines no ring
250
+ * token still gets a visible ring in the link's own ink rather than nothing.
251
+ */
252
+ [data-ncm-document].ncm-theme-paper [data-ncm-token="wiki_link"]:focus-visible {
253
+ outline: 2px solid var(--ring, currentColor);
254
+ outline-offset: 2px;
255
+ border-radius: 0.1875rem;
256
+ }
257
+
258
+ /*
259
+ * Pointer cursor for the actionable wikilink elements — the paper sibling of
260
+ * the rule in `nuu.css`; that file carries the reasoning. (Task 558.)
261
+ */
262
+ [data-ncm-document].ncm-theme-paper a[data-ncm-token="wiki_link"],
263
+ [data-ncm-document].ncm-theme-paper button[data-ncm-token="wiki_link"] {
264
+ cursor: pointer;
265
+ }
266
+
267
+ /*
268
+ * The embed button — paper siblings of the two rules in `nuu.css`; that file
269
+ * carries the reasoning. Both can only match an element a host opted into, so
270
+ * neither changes anything that renders today.
271
+ */
272
+ [data-ncm-document].ncm-theme-paper button[data-ncm-token="wiki_embed"] {
273
+ appearance: none;
274
+ padding: 0;
275
+ border: 0;
276
+ background: none;
277
+ font: inherit;
278
+ cursor: pointer;
279
+ }
280
+
281
+ [data-ncm-document].ncm-theme-paper [data-ncm-token="wiki_embed"]:focus-visible {
282
+ outline: 2px solid var(--ring, currentColor);
283
+ outline-offset: 2px;
284
+ border-radius: 0.1875rem;
224
285
  }
225
286
 
226
287
  /* Horizontal rule */
@@ -1,6 +1,6 @@
1
- import * as react_jsx_runtime from 'react/jsx-runtime';
1
+ import * as react from 'react';
2
2
  import { ComponentType, ReactNode } from 'react';
3
- import { MeshResolveLinkResult, MeshHydrationContext } from '@nuucognition/mesh-core';
3
+ import { MeshResolveEmbedResult, MeshResolveLinkResult, MeshHydrationContext } from '@nuucognition/mesh-core';
4
4
  import { EmbedType } from '@nuucognition/ncm';
5
5
  import { c as EmbeddingRendererRegistry, b as BlockRendererRegistry } from './renderers-C5f41FCy.js';
6
6
 
@@ -23,6 +23,28 @@ interface InlineConfig {
23
23
  */
24
24
  referencePopover?: boolean;
25
25
  }
26
+ /**
27
+ * What a host draws in place of an image whose bytes never arrived.
28
+ *
29
+ * The renderer cannot answer this itself. Whether an `<img src>` actually
30
+ * loads is only learned by trying, and what to SAY about the failure is the
31
+ * host's vocabulary — a mesh embedder can offer a retry against its own blob
32
+ * cache, a static-site embedder has nothing to retry and should say so.
33
+ */
34
+ interface ImageErrorProps {
35
+ /** The embed target exactly as the author wrote it (`logo.png`, a URL). */
36
+ target: string;
37
+ /** The URL that was handed to `<img src>` and did not load. */
38
+ src: string;
39
+ /** The alt text from the source, if the author wrote one. */
40
+ alt?: string;
41
+ /**
42
+ * Re-attempt the load. The `<img>` is re-keyed, so this is a real second
43
+ * fetch rather than the browser's cached failure. A host that owns the URL
44
+ * (a blob cache) should refresh it first and then call this.
45
+ */
46
+ retry: () => void;
47
+ }
26
48
  /**
27
49
  * Configuration for embedding rendering
28
50
  */
@@ -32,6 +54,34 @@ interface EmbeddingsConfig {
32
54
  imageClassName?: string;
33
55
  videoClassName?: string;
34
56
  audioClassName?: string;
57
+ /**
58
+ * Render a failed image instead of leaving the browser's broken-image box.
59
+ *
60
+ * OPT-IN, and strictly so: with no renderer supplied the `<img>` carries no
61
+ * `onError` handler at all, so the emitted DOM is identical to what every
62
+ * consumer gets today — not merely visually identical. Supplying one is what
63
+ * turns an unloadable image into a state with a name and a way out.
64
+ *
65
+ * Applies to both spellings of an inline image — `![alt](url)` and an
66
+ * `![[target]]` that resolves to one.
67
+ */
68
+ renderImageError?: ComponentType<ImageErrorProps>;
69
+ /**
70
+ * Handle a click on a non-media embed — an embedded DOCUMENT, or any target
71
+ * whose type has no player.
72
+ *
73
+ * WITHOUT THIS the renderer emits `<a href={resolveUrl(...)}>`, which is only
74
+ * honest when the target really has a URL. A document in a title-addressed
75
+ * mesh does not: the host navigates in its own state, so the href ends up
76
+ * being the raw target, a relative URL that leads nowhere and copies as
77
+ * nonsense from the context menu.
78
+ *
79
+ * Supplying this renders the same content as a `<button>` instead, which
80
+ * claims nothing about URLs and is a real control — exactly the shape
81
+ * {@link InlineConfig.onWikiLinkClick} already produces for a wikilink with
82
+ * no href. Absent, nothing changes.
83
+ */
84
+ onEmbedClick?: (target: string, result: MeshResolveEmbedResult | null, event: React.MouseEvent) => void;
35
85
  }
36
86
  /**
37
87
  * Code rendering configuration
@@ -43,6 +93,7 @@ interface CodeConfig {
43
93
  className?: string;
44
94
  }>;
45
95
  lineNumbers?: boolean;
96
+ /** Opt-in since the minimalist block pass: the button renders only on `true`. */
46
97
  copyButton?: boolean;
47
98
  }
48
99
  /**
@@ -109,6 +160,6 @@ interface NcmProviderProps {
109
160
  * </NcmProvider>
110
161
  * ```
111
162
  */
112
- declare function NcmProvider({ config, children }: NcmProviderProps): react_jsx_runtime.JSX.Element;
163
+ declare function NcmProvider({ config, children }: NcmProviderProps): react.JSX.Element;
113
164
 
114
- export { type CodeConfig as C, type EmbeddingsConfig as E, type InlineConfig as I, type NcmConfig as N, NcmProvider as a, type NcmProviderProps as b, useNcmConfig as u };
165
+ export { type CodeConfig as C, type EmbeddingsConfig as E, type ImageErrorProps as I, type NcmConfig as N, type InlineConfig as a, NcmProvider as b, type NcmProviderProps as c, useNcmConfig as u };