@nuucognition/ncm-ui 0.1.0-dev.1 → 0.1.1-beta.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.
Files changed (67) 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 +63 -12
  4. package/dist/blocks/aside/index.d.ts +2 -3
  5. package/dist/blocks/aside/index.js +63 -12
  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/code/index.d.ts +2 -3
  11. package/dist/blocks/code/index.js +2 -2
  12. package/dist/blocks/container/index.d.ts +2 -3
  13. package/dist/blocks/container/index.js +50 -5
  14. package/dist/blocks/custom-code/index.d.ts +2 -3
  15. package/dist/blocks/dataview/index.d.ts +2 -3
  16. package/dist/blocks/definition/index.d.ts +2 -3
  17. package/dist/blocks/definition/index.js +63 -12
  18. package/dist/blocks/embed/index.d.ts +2 -3
  19. package/dist/blocks/embedding/index.d.ts +2 -3
  20. package/dist/blocks/embedding/index.js +50 -5
  21. package/dist/blocks/figure/index.d.ts +2 -3
  22. package/dist/blocks/figure/index.js +50 -5
  23. package/dist/blocks/footnote/index.d.ts +2 -3
  24. package/dist/blocks/footnote/index.js +63 -12
  25. package/dist/blocks/frontmatter/index.d.ts +2 -3
  26. package/dist/blocks/header/index.d.ts +2 -3
  27. package/dist/blocks/header/index.js +50 -5
  28. package/dist/blocks/horizontal-rule/index.d.ts +2 -3
  29. package/dist/blocks/html/index.d.ts +3 -4
  30. package/dist/blocks/index.d.ts +1 -2
  31. package/dist/blocks/index.js +63 -12
  32. package/dist/blocks/latex/index.d.ts +2 -3
  33. package/dist/blocks/link/index.d.ts +2 -3
  34. package/dist/blocks/list/index.d.ts +2 -3
  35. package/dist/blocks/list/index.js +57 -7
  36. package/dist/blocks/math/index.d.ts +2 -3
  37. package/dist/blocks/mermaid/index.d.ts +2 -3
  38. package/dist/blocks/page-breaker/index.d.ts +2 -3
  39. package/dist/blocks/paragraph/index.d.ts +2 -3
  40. package/dist/blocks/paragraph/index.js +50 -5
  41. package/dist/blocks/spoiler/index.d.ts +2 -3
  42. package/dist/blocks/spoiler/index.js +63 -12
  43. package/dist/blocks/table/index.d.ts +2 -3
  44. package/dist/blocks/table/index.js +50 -5
  45. package/dist/blocks/task-list/index.d.ts +2 -3
  46. package/dist/blocks/task-list/index.js +52 -6
  47. package/dist/components/index.d.ts +34 -4
  48. package/dist/components/index.js +101 -14
  49. package/dist/components/ncm-block.d.ts +2 -3
  50. package/dist/components/ncm-block.js +63 -12
  51. package/dist/components/ncm-document.d.ts +4 -4
  52. package/dist/components/ncm-document.js +63 -12
  53. package/dist/context/index.d.ts +2 -3
  54. package/dist/hooks/index.d.ts +1 -2
  55. package/dist/hooks/index.js +63 -12
  56. package/dist/hooks/use-block-renderer.js +63 -12
  57. package/dist/hooks/use-ncm-config.d.ts +1 -2
  58. package/dist/index.d.ts +3 -4
  59. package/dist/index.js +102 -15
  60. package/dist/inline/index.d.ts +4 -4
  61. package/dist/inline/index.js +51 -6
  62. package/dist/lib/defaults.js +63 -12
  63. package/dist/{references-Bs5wJybr.d.ts → references-BYW5pnj_.d.ts} +4 -4
  64. package/dist/styles/nuu.css +304 -42
  65. package/dist/styles/paper.css +62 -1
  66. package/dist/{use-ncm-config-D0doMYTH.d.ts → use-ncm-config-CnntcShj.d.ts} +55 -4
  67. package/package.json +5 -4
@@ -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.25rem /
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,93 @@
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) — minimalist: sharp corners, 1px hairline border, a
109
+ * 2px colored left edge (the blockquote's own weight), tight padding. The
110
+ * type supplies only colors, through the `--ncm-callout-*` hooks; a host
111
+ * assigns them per `data-ncm-admonition-type` (Onyx does, from its palette
112
+ * families). Unset, every hook falls back to the neutral hairline card, so
113
+ * a host with no callout stylesheet — and any type a host does not cover —
114
+ * renders the same minimalist shell with no color.
115
+ *
116
+ * These rules override the component's own rounded-card utilities
117
+ * (`rounded-lg bg-card p-4`): this file is unlayered and Tailwind utilities
118
+ * are layered, so the unlayered declarations win regardless of specificity.
119
+ * The hooks are set by hosts at ANY specificity — this file deliberately
120
+ * never assigns them, only reads them with fallbacks.
121
+ */
122
+ [data-ncm-document].ncm-theme-nuu [data-ncm-admonition] {
123
+ border: 1px solid var(--ncm-callout-border, var(--border));
124
+ border-left: 2px solid var(--ncm-callout-edge, var(--border));
125
+ border-radius: 0;
126
+ padding: 0.625rem 0.875rem;
127
+ background-color: var(--ncm-callout-tint, transparent);
128
+ }
129
+
130
+ /*
131
+ * One gap for both title shapes. The static `<p>` title ships `mb-2` and the
132
+ * collapsible variant instead puts `mt-2` on the content that follows the
133
+ * `<summary>` — two different gaps for one anatomy. Zero the title's own
134
+ * margin and state the gap once on whatever follows the title; a collapsed
135
+ * `<details>` renders no content and so gets no stray bottom space.
136
+ */
137
+ [data-ncm-document].ncm-theme-nuu [data-ncm-admonition] > [data-ncm-admonition-title] {
138
+ margin-bottom: 0;
139
+ color: var(--ncm-callout-title, var(--foreground));
140
+ font-weight: 600;
141
+ }
142
+
143
+ [data-ncm-document].ncm-theme-nuu [data-ncm-admonition] > [data-ncm-admonition-title] + * {
144
+ margin-top: 0.375rem;
78
145
  }
79
146
 
80
147
  /* Blockquote */
@@ -82,7 +149,7 @@
82
149
  border-left: 2px solid var(--border);
83
150
  padding-left: 0.875rem;
84
151
  color: color-mix(in srgb, var(--foreground) 80%, transparent);
85
- line-height: 1.55;
152
+ line-height: 1.65;
86
153
  }
87
154
 
88
155
  [data-ncm-document].ncm-theme-nuu [data-ncm-type="blockquote"] p + p {
@@ -94,7 +161,7 @@
94
161
  [data-ncm-document].ncm-theme-nuu [data-ncm-type="list_unordered"] {
95
162
  padding-left: 1.25rem;
96
163
  color: var(--foreground);
97
- line-height: 1.55;
164
+ line-height: 1.65;
98
165
  }
99
166
 
100
167
  [data-ncm-document].ncm-theme-nuu [data-ncm-type="list_ordered"] {
@@ -136,6 +203,84 @@
136
203
  list-style-type: decimal;
137
204
  }
138
205
 
206
+ /*
207
+ * Task list — one spec with the editor's `ul[data-type="taskList"]` block in
208
+ * `@nuucognition/nuu-editor` (`src/nuu-editor/nuu-editor.scss`): flush-left
209
+ * rows 0.125rem apart, a 1em sharp-cornered checkbox drawn from theme tokens
210
+ * (border-token box, foreground fill when checked, background-coloured check),
211
+ * 0.5rem to the text, checked text dimmed to the muted tone with NO
212
+ * strike-through, nested lists indented 1.5rem so they start at the text
213
+ * column. If one side of that pair moves, move the other in the same change.
214
+ *
215
+ * The checkbox is drawn with `appearance: none` because the native control
216
+ * neither themes nor matches the editor's; the input stays a real (disabled)
217
+ * checkbox for the accessibility tree. The `> li` rule neutralizes the
218
+ * component's own Tailwind row gap so the 0.125rem list rhythm is the one
219
+ * answer.
220
+ */
221
+ [data-ncm-document].ncm-theme-nuu [data-ncm-task-list] {
222
+ padding-left: 0;
223
+ color: var(--foreground);
224
+ line-height: 1.65;
225
+ }
226
+
227
+ [data-ncm-document].ncm-theme-nuu [data-ncm-task-list] > li:not(:last-child) {
228
+ margin-bottom: 0;
229
+ }
230
+
231
+ [data-ncm-document].ncm-theme-nuu [data-ncm-task-list] li + li {
232
+ margin-top: 0.125rem;
233
+ }
234
+
235
+ [data-ncm-document].ncm-theme-nuu [data-ncm-task-list] [data-ncm-list-nested] {
236
+ margin-left: 1.5rem;
237
+ margin-top: 0.125rem;
238
+ }
239
+
240
+ /* Top-aligned like the editor (whose checkbox sits 0.375rem into the row):
241
+ on a wrapped item the box stays on the first line instead of centring
242
+ across the whole item. */
243
+ [data-ncm-document].ncm-theme-nuu [data-ncm-task-item] > span {
244
+ gap: 0.5rem;
245
+ align-items: flex-start;
246
+ }
247
+
248
+ [data-ncm-document].ncm-theme-nuu [data-ncm-task-item] input[type="checkbox"] {
249
+ appearance: none;
250
+ -webkit-appearance: none;
251
+ width: 1em;
252
+ height: 1em;
253
+ margin: 0.375rem 0 0;
254
+ border: 1px solid var(--border);
255
+ border-radius: 0;
256
+ background-color: transparent;
257
+ position: relative;
258
+ }
259
+
260
+ [data-ncm-document].ncm-theme-nuu [data-ncm-task-item] input[type="checkbox"]:checked {
261
+ background-color: var(--foreground);
262
+ border-color: var(--foreground);
263
+ }
264
+
265
+ [data-ncm-document].ncm-theme-nuu [data-ncm-task-item] input[type="checkbox"]:checked::before {
266
+ content: "";
267
+ position: absolute;
268
+ left: 50%;
269
+ top: 50%;
270
+ transform: translate(-50%, -50%);
271
+ width: 0.75em;
272
+ height: 0.75em;
273
+ background-color: var(--background);
274
+ -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")
275
+ center/contain no-repeat;
276
+ 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")
277
+ center/contain no-repeat;
278
+ }
279
+
280
+ [data-ncm-document].ncm-theme-nuu [data-ncm-task-item][data-ncm-checked="true"] > span > span {
281
+ color: var(--muted-foreground);
282
+ }
283
+
139
284
  /* Tables */
140
285
  [data-ncm-document].ncm-theme-nuu [data-ncm-type="table"] {
141
286
  width: 100%;
@@ -145,7 +290,7 @@
145
290
  [data-ncm-document].ncm-theme-nuu [data-ncm-type="table"] table {
146
291
  width: 100%;
147
292
  border-collapse: collapse;
148
- font-size: calc(0.75rem * var(--surf-reading-scale, 1));
293
+ font-size: 0.857em;
149
294
  }
150
295
 
151
296
  [data-ncm-document].ncm-theme-nuu [data-ncm-type="table"] th {
@@ -168,27 +313,50 @@
168
313
  background-color: color-mix(in srgb, var(--muted) 50%, transparent);
169
314
  }
170
315
 
316
+ /* An empty cell keeps its line box, so an entirely blank row renders at full
317
+ height instead of collapsing to its padding. */
318
+ [data-ncm-document].ncm-theme-nuu [data-ncm-type="table"] th:empty::after,
319
+ [data-ncm-document].ncm-theme-nuu [data-ncm-type="table"] td:empty::after {
320
+ content: "\00a0";
321
+ }
322
+
171
323
  /* Code (block) */
172
324
  [data-ncm-document].ncm-theme-nuu [data-ncm-type="code"] {
173
325
  position: relative;
174
326
  }
175
327
 
328
+ /* Minimalist, matching the editor's code block at rest: bare text — no fill,
329
+ no visible box. The border keeps its 1px width as transparent so the text
330
+ sits at exactly the editor's inset (its border reveals on hover/caret). The
331
+ editor's `pre` rule mirrors this — move both together. */
176
332
  [data-ncm-document].ncm-theme-nuu [data-ncm-type="code"] pre {
177
333
  overflow-x: auto;
178
- border: 1px solid var(--border);
179
- border-radius: var(--radius, 0.625rem);
334
+ border: 1px solid transparent;
335
+ border-radius: 0;
180
336
  padding: 0.75rem;
181
- background-color: var(--muted);
337
+ background-color: transparent;
182
338
  color: var(--foreground);
183
339
  font-family: var(--font-mono);
184
- font-size: calc(0.875rem * var(--surf-reading-scale, 1));
185
- line-height: 1.6;
340
+ font-size: 0.875em;
341
+ line-height: 1.55;
342
+ }
343
+
344
+ /* Parse warning (NcmBlockPreview) — the reader-mode display for a block
345
+ source that cannot be parsed as its block type. Minimalist: muted text,
346
+ hairline border, sharp corners. */
347
+ [data-ncm-parse-warning] {
348
+ border: 1px solid var(--border);
349
+ padding: 0.5rem 0.75rem;
350
+ color: var(--muted-foreground);
351
+ font-family: var(--font-sans);
352
+ font-size: 0.857em;
353
+ line-height: 1.5;
186
354
  }
187
355
 
188
356
  /* Inline code */
189
357
  [data-ncm-document].ncm-theme-nuu [data-ncm-token="code"] {
190
358
  font-family: var(--font-mono);
191
- font-size: calc(0.75rem * var(--surf-reading-scale, 1));
359
+ font-size: 0.857em;
192
360
  padding: 0.125rem 0.25rem;
193
361
  border-radius: 0.25rem;
194
362
  background-color: var(--muted);
@@ -212,17 +380,111 @@
212
380
  text-underline-offset: 4px;
213
381
  }
214
382
 
383
+ /*
384
+ * AMBIGUOUS IS A WARNING, NOT A FAILURE — and this rule used to say otherwise.
385
+ *
386
+ * It painted `--fire-text`/`--destructive`, i.e. RED, while the EDIT surface
387
+ * paints red for a MISSING link and amber for an ambiguous one. The same hue
388
+ * therefore carried two different facts depending on which mode the reader was
389
+ * in, which is worse than either surface being wrong on its own: a reader who
390
+ * learns what red means in one mode is actively misled by the other.
391
+ *
392
+ * The vocabulary this settles on, ruled in room `onyx-uiux` on 2026-07-31 and
393
+ * applied across both surfaces:
394
+ *
395
+ * missing pending — muted foreground, dashed. A link to a document that
396
+ * is not in this mesh is very often a PLAN, not a mistake; that is
397
+ * also the Obsidian benchmark, where an unresolved link is the
398
+ * link colour faded and never red.
399
+ * ambiguous warning — amber, dotted. Several targets match; the author has
400
+ * something to decide, but nothing has failed.
401
+ * red reserved for GENUINE FAILURE — in this package, an embed whose
402
+ * bytes did not arrive (see `renderImageError`).
403
+ *
404
+ * A DELIBERATE VISUAL CHANGE FOR EVERY EMBEDDER, not just for Onyx: an
405
+ * ambiguous reference stops being drawn as an error. Recorded in the README.
406
+ */
215
407
  [data-ncm-document].ncm-theme-nuu [data-ncm-token="wiki_link"][data-ncm-wikilink-status="ambiguous"],
216
408
  [data-ncm-document].ncm-theme-nuu [data-ncm-token="wiki_embed"][data-ncm-wikiembed-status="ambiguous"] {
217
- color: var(--fire-text, var(--destructive));
409
+ color: var(--sun-text, var(--warning, #b45309));
410
+ text-decoration: underline dotted;
411
+ text-underline-offset: 4px;
218
412
  }
219
413
 
220
- /* Horizontal rule */
414
+ /*
415
+ * Wikilink focus ring.
416
+ *
417
+ * `renderer.tsx` already emits a real <a> or <button> for an actionable
418
+ * wikilink, so read-mode links have ALWAYS been in the tab order — a keyboard
419
+ * user could reach them and had no way to see that they had. Measured on the
420
+ * Onyx read surface in both themes: outline-style none, box-shadow none, while
421
+ * the surrounding chrome buttons showed a ring. That is the whole of
422
+ * known-adopted issue 095#5, and it belongs here rather than in a host: an
423
+ * embedder gets no focus affordance for these elements from anywhere else.
424
+ *
425
+ * A NEW rule, touching no existing selector. It paints only when a host has
426
+ * made the link actionable AND the browser is in keyboard-focus mode, so no
427
+ * rendering that exists today changes.
428
+ *
429
+ * (Landed as the negotiated ncm-ui freeze exception for Task 507, approved in
430
+ * room `onyx-uiux` on 2026-07-31. The TSX half of the original proposal was
431
+ * withdrawn: the button is focusable by construction and needed nothing.)
432
+ */
433
+ [data-ncm-document].ncm-theme-nuu [data-ncm-token="wiki_link"]:focus-visible {
434
+ outline: 2px solid var(--ring, currentColor);
435
+ outline-offset: 2px;
436
+ border-radius: 0.1875rem;
437
+ }
438
+
439
+ /*
440
+ * Pointer cursor for the ACTIONABLE wikilink elements — the <a> a host's
441
+ * `resolveWikiLinkHref` produces and the <button> a bare `onWikiLinkClick`
442
+ * produces. The anchor has the pointer from the user agent already; the button
443
+ * does not — Tailwind v4's preflight leaves buttons on the default arrow, so in
444
+ * Onyx a clickable wikilink did not read as clickable. The element selectors
445
+ * exclude the inert <span> a host with neither config gets, which stays on the
446
+ * text cursor on purpose. (Task 558.)
447
+ */
448
+ [data-ncm-document].ncm-theme-nuu a[data-ncm-token="wiki_link"],
449
+ [data-ncm-document].ncm-theme-nuu button[data-ncm-token="wiki_link"] {
450
+ cursor: pointer;
451
+ }
452
+
453
+ /*
454
+ * The embed BUTTON — the element `EmbeddingsConfig.onEmbedClick` produces.
455
+ *
456
+ * Both rules below can only ever match an element that does not exist unless a
457
+ * host opted in, so neither can change a rendering that ships today.
458
+ *
459
+ * The reset first: a `<button>` in the middle of a sentence arrives wearing the
460
+ * user agent's chrome — its own background, border, padding and font — and an
461
+ * embedded document is a piece of the prose, not a form control. Onyx never saw
462
+ * this because Tailwind's preflight resets buttons app-wide; an embedder
463
+ * without one would have. Then the ring, the sibling of the wikilink rule
464
+ * directly above: the button is a tab stop by construction, so it needs to be
465
+ * able to say when it has been reached.
466
+ */
467
+ [data-ncm-document].ncm-theme-nuu button[data-ncm-token="wiki_embed"] {
468
+ appearance: none;
469
+ padding: 0;
470
+ border: 0;
471
+ background: none;
472
+ font: inherit;
473
+ cursor: pointer;
474
+ }
475
+
476
+ [data-ncm-document].ncm-theme-nuu [data-ncm-token="wiki_embed"]:focus-visible {
477
+ outline: 2px solid var(--ring, currentColor);
478
+ outline-offset: 2px;
479
+ border-radius: 0.1875rem;
480
+ }
481
+
482
+ /* Horizontal rule — 1em on both sides, matching the editor's hr rule. */
221
483
  [data-ncm-document].ncm-theme-nuu [data-ncm-type="horizontal_rule"] {
222
484
  border: 0;
223
485
  border-top: 1px solid var(--border);
224
- margin-top: 1.25rem;
225
- margin-bottom: 1.25rem;
486
+ margin-top: 1em;
487
+ margin-bottom: 1em;
226
488
  }
227
489
 
228
490
  /* Embeddings */
@@ -235,7 +497,7 @@
235
497
  [data-ncm-document].ncm-theme-nuu [data-ncm-type="embedding"] figcaption {
236
498
  margin-top: 0.5rem;
237
499
  text-align: left;
238
- font-size: 0.75rem;
500
+ font-size: 0.857em;
239
501
  color: var(--muted-foreground);
240
502
  }
241
503
 
@@ -253,7 +515,7 @@
253
515
  background-color: var(--muted);
254
516
  color: var(--foreground);
255
517
  font-family: var(--font-mono);
256
- font-size: 0.875rem;
518
+ font-size: 1em;
257
519
  }
258
520
 
259
521
  /* OpenDyslexic3 — newest, refined letterforms of the OpenDyslexic family
@@ -344,7 +606,7 @@
344
606
  background: var(--popover, var(--background, var(--muted)));
345
607
  color: var(--popover-foreground, var(--foreground));
346
608
  box-shadow: 0 4px 12px rgb(0 0 0 / 0.12);
347
- font-size: calc(0.8125rem * var(--surf-reading-scale, 1));
609
+ font-size: 0.929em;
348
610
  font-weight: 400;
349
611
  line-height: 1.5;
350
612
  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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nuucognition/ncm-ui",
3
- "version": "0.1.0-dev.1",
3
+ "version": "0.1.1-beta.0",
4
4
  "description": "React renderers for NUU Cognition Markdown (NCM) blocks — a component library that renders parsed NCM Blocks to accessible, themeable markup.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -18,7 +18,8 @@
18
18
  "*.css"
19
19
  ],
20
20
  "publishConfig": {
21
- "access": "public"
21
+ "access": "public",
22
+ "tag": "beta"
22
23
  },
23
24
  "main": "./dist/index.js",
24
25
  "module": "./dist/index.js",
@@ -29,12 +30,12 @@
29
30
  "LICENSE"
30
31
  ],
31
32
  "dependencies": {
32
- "@nuucognition/mesh-core": "^1.0.0",
33
+ "@nuucognition/mesh-core": "1.0.1-beta.1",
33
34
  "clsx": "^2.1.1",
34
35
  "katex": "^0.16.21",
35
36
  "shiki": "^1.29.2",
36
37
  "tailwind-merge": "^3.3.1",
37
- "@nuucognition/ncm": "0.1.0-dev.2"
38
+ "@nuucognition/ncm": "0.1.1-beta.0"
38
39
  },
39
40
  "devDependencies": {
40
41
  "@types/katex": "^0.16.7",