@cueplusplus/ui 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +49 -0
- package/README.md +1 -1
- package/dist/elements/markdown.d.ts +29 -9
- package/dist/elements/markdown.js +102 -13
- package/dist/forms/_chassis.d.ts +4 -1
- package/dist/forms/_chassis.js +4 -1
- package/dist/styles.css +18 -1
- package/dist/theming/contrast.d.ts +2 -2
- package/dist/theming/contrast.js +2 -2
- package/dist/theming/create-theme.js +15 -6
- package/dist/theming/index.d.ts +2 -2
- package/dist/theming/index.js +2 -2
- package/dist/theming/serialize.js +2 -1
- package/manifest/components/markdown-text.json +16 -2
- package/manifest/manifest.json +3 -3
- package/manifest/tokens.json +2 -1
- package/package.json +14 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,54 @@
|
|
|
1
1
|
# @cueplusplus/ui
|
|
2
2
|
|
|
3
|
+
## 0.23.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- dae9dec: **A field's edge can reach 3:1: `data-field-edge="contrast"`** (cue-ui-60). Every shipped theme's
|
|
8
|
+
`--cue-border` measured 1.07:1 to 2.02:1 against `surface-1`, under WCAG 1.4.11's 3:1 for the line
|
|
9
|
+
that marks out where to type. The border stays as it is, because it is a box edge and a quiet one on
|
|
10
|
+
purpose, and a field's edge becomes a role of its own.
|
|
11
|
+
|
|
12
|
+
- **`--cue-field-edge`** is each block's border moved toward its ink until it clears 3:1 on its
|
|
13
|
+
`surface-1`, written per block as a literal. Every theme package, `theme-base`'s own palette and
|
|
14
|
+
`createTheme()` write it, and the contrast report holds it to 3:1 as the required pair
|
|
15
|
+
`field-edge/surface-1`.
|
|
16
|
+
- **Opt in with `data-field-edge="contrast"`** on any element, `<html>` included. Every recessed
|
|
17
|
+
control inside then draws its resting edge in `--cue-field-edge`, disabled ones too, and over a
|
|
18
|
+
paint pack's stroke. Hover, focus and invalid edges keep their own colours. Outside the attribute,
|
|
19
|
+
nothing changes.
|
|
20
|
+
- **`edgeToContrast(edge, ground, toward, minimum = 3)`** is the arithmetic, exported from
|
|
21
|
+
`@cueplusplus/theme-base/contrast` and `@cueplusplus/ui/theming`.
|
|
22
|
+
- `@cueplusplus/tokens` publishes `field-edge` as a derived name, with `var(--cue-border)` as its
|
|
23
|
+
template. A stylesheet built before this release falls back to the plain border.
|
|
24
|
+
|
|
25
|
+
**For a theme package of your own:** `theme-tools` now writes one more line per block. Run
|
|
26
|
+
`generate` once after upgrading; until then `check` reports the stylesheet out of date.
|
|
27
|
+
|
|
28
|
+
- 39884b3: **`MarkdownText` renders GitHub tables** (cue-ui-58). An agent's table used to arrive as one
|
|
29
|
+
paragraph of pipes: the parser read CommonMark only, and the sanitizer allowed no table elements.
|
|
30
|
+
Tables are now the one part of GFM the parser reads. Each column is aligned as its delimiter row
|
|
31
|
+
says, header cells sit on `surface-2`, and the table scrolls sideways inside a named,
|
|
32
|
+
keyboard-reachable region rather than widening the turn. Task lists, footnotes and autolink literals
|
|
33
|
+
stay out, and raw HTML is still refused: a cell's markup is text, and alignment is the only style a
|
|
34
|
+
cell carries.
|
|
35
|
+
|
|
36
|
+
**`remarkPlugins`** adds parser plugins after the table syntax. It is a seam for the parser, not
|
|
37
|
+
for the page: whatever a plugin puts in the tree passes the same sanitizer, so it can change what
|
|
38
|
+
the markdown means but cannot widen what renders.
|
|
39
|
+
|
|
40
|
+
### Migrating
|
|
41
|
+
- **`@cueplusplus/ui/elements/markdown` has two more optional peers**, the table half of GFM:
|
|
42
|
+
`pnpm add micromark-extension-gfm-table mdast-util-gfm-table`. Like `react-markdown` and
|
|
43
|
+
`rehype-sanitize`, they are needed only where this subpath is imported. A build that imports it
|
|
44
|
+
without them fails to resolve the module, and names it.
|
|
45
|
+
|
|
46
|
+
### Patch Changes
|
|
47
|
+
|
|
48
|
+
- Updated dependencies [dae9dec]
|
|
49
|
+
- @cueplusplus/tokens@0.23.0
|
|
50
|
+
- @cueplusplus/theme-base@1.4.0
|
|
51
|
+
|
|
3
52
|
## 0.22.0
|
|
4
53
|
|
|
5
54
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -105,7 +105,7 @@ their own, so importing a button never pulls a charting library into your bundle
|
|
|
105
105
|
| `@cueplusplus/ui/chat` | the multi-agent transcript: messages, composer, HITL questions, agent colour |
|
|
106
106
|
| `@cueplusplus/ui/agent-runtime` | the streaming layer under Chat; needs `@assistant-ui/react` |
|
|
107
107
|
| `@cueplusplus/ui/elements` | the agent's own surface, vendored from assistant-ui and repainted onto the tokens; needs `lucide-react` and `heat-graph` (+ `./elements.css`) |
|
|
108
|
-
| `@cueplusplus/ui/elements/markdown` | `MarkdownText`; needs `react-markdown`, `rehype-sanitize` |
|
|
108
|
+
| `@cueplusplus/ui/elements/markdown` | `MarkdownText`; needs `react-markdown`, `rehype-sanitize`, `micromark-extension-gfm-table`, `mdast-util-gfm-table` |
|
|
109
109
|
| `@cueplusplus/ui/elements/generative` | the generative-UI renderer and its spec (+ `./elements/generative.css`) |
|
|
110
110
|
| `@cueplusplus/ui/elements/replay` | the scripted session-replay frame |
|
|
111
111
|
| `@cueplusplus/ui/dmx` | 512 bytes at wire rate: the bar, the strip, the patch bar, the channel grid |
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
+
import Markdown from "react-markdown";
|
|
2
3
|
//#region src/elements/markdown.d.ts
|
|
3
4
|
/** A fenced block, handed to a highlighter that was opted into. */
|
|
4
5
|
interface MarkdownCodeBlock {
|
|
@@ -50,6 +51,16 @@ interface MarkdownTextProps extends Omit<React.ComponentPropsWithoutRef<"div">,
|
|
|
50
51
|
* behaviour they inherited.
|
|
51
52
|
*/
|
|
52
53
|
renderCodeBlock?: (block: MarkdownCodeBlock) => React.ReactNode;
|
|
54
|
+
/**
|
|
55
|
+
* More remark plugins, run after the component's own table syntax.
|
|
56
|
+
*
|
|
57
|
+
* A seam for the parser, not for the page: whatever a plugin adds to the
|
|
58
|
+
* tree still passes the same sanitizer, so a plugin can change what the
|
|
59
|
+
* markdown *means* — a mention syntax, a glossary link — but cannot widen
|
|
60
|
+
* what renders. An element outside the subset is dropped, and an attribute
|
|
61
|
+
* the schema does not name never arrives.
|
|
62
|
+
*/
|
|
63
|
+
remarkPlugins?: React.ComponentProps<typeof Markdown>["remarkPlugins"];
|
|
53
64
|
}
|
|
54
65
|
/**
|
|
55
66
|
* Model output, rendered.
|
|
@@ -84,9 +95,11 @@ interface MarkdownTextProps extends Omit<React.ComponentPropsWithoutRef<"div">,
|
|
|
84
95
|
* - **Attributes are enumerated, not filtered.** An element that is not in the
|
|
85
96
|
* schema renders nothing; an element that is renders only the attributes
|
|
86
97
|
* named beside it — `href`/`title` on a link, `src`/`alt`/`title` on an
|
|
87
|
-
* image, `language-…` on a `code
|
|
98
|
+
* image, `language-…` on a `code`, and `align` on a table cell, held to
|
|
99
|
+
* `left`, `center` or `right`. No element in the subset accepts `id`,
|
|
88
100
|
* `style`, `srcset`, `formaction` or any `on*` handler, so none of them has
|
|
89
|
-
* an attribute to arrive on
|
|
101
|
+
* an attribute to arrive on; the one inline style anything carries is a
|
|
102
|
+
* cell's `text-align`, which the renderer writes from that `align`.
|
|
90
103
|
* - **URLs are allow-listed twice.** `http`, `https` and `mailto` on both
|
|
91
104
|
* `href` and `src`; a relative URL stays relative; everything else — a
|
|
92
105
|
* `javascript:` link, a `data:text/html` image, `vbscript:`, `blob:`,
|
|
@@ -98,10 +111,14 @@ interface MarkdownTextProps extends Omit<React.ComponentPropsWithoutRef<"div">,
|
|
|
98
111
|
*
|
|
99
112
|
* **The subset.** Paragraphs, ATX and setext headings, bullet and ordered
|
|
100
113
|
* lists, block quotes, thematic breaks, fenced and indented code, inline code,
|
|
101
|
-
* emphasis, strong, strikethrough, links, images,
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
114
|
+
* emphasis, strong, strikethrough, links, images, hard breaks, and GitHub
|
|
115
|
+
* tables. Tables are the one part of GFM the parser reads, each column aligned
|
|
116
|
+
* as its delimiter row says and wrapped in a named region that scrolls
|
|
117
|
+
* sideways; task lists, footnotes and autolink literals stay out — an explicit
|
|
118
|
+
* `<https://…>` autolink still works. Fenced blocks are plain text unless
|
|
119
|
+
* {@link MarkdownTextProps.renderCodeBlock} is given, and
|
|
120
|
+
* {@link MarkdownTextProps.remarkPlugins} adds parser plugins without widening
|
|
121
|
+
* the subset.
|
|
105
122
|
*
|
|
106
123
|
* **Streaming.** Pass `streaming` while the turn is arriving and the token the
|
|
107
124
|
* model is halfway through typing is hidden rather than rendered as
|
|
@@ -110,10 +127,11 @@ interface MarkdownTextProps extends Omit<React.ComponentPropsWithoutRef<"div">,
|
|
|
110
127
|
* words appear as they arrive; only their formatting waits for the closing
|
|
111
128
|
* token.
|
|
112
129
|
*
|
|
113
|
-
* `react-markdown
|
|
114
|
-
*
|
|
130
|
+
* `react-markdown`, `rehype-sanitize` and GFM's table pair
|
|
131
|
+
* (`micromark-extension-gfm-table`, `mdast-util-gfm-table`) are optional peers
|
|
132
|
+
* of this subpath alone, which is why this component is not re-exported from
|
|
115
133
|
* `@cueplusplus/ui/elements`: a console that renders a transcript without
|
|
116
|
-
* markdown should install
|
|
134
|
+
* markdown should install none of them.
|
|
117
135
|
*
|
|
118
136
|
* @example
|
|
119
137
|
* <Message agent="researcher">
|
|
@@ -125,6 +143,8 @@ interface MarkdownTextProps extends Omit<React.ComponentPropsWithoutRef<"div">,
|
|
|
125
143
|
* <MarkdownText renderCodeBlock={({ language, code }) => <Highlight lang={language} code={code} />}>
|
|
126
144
|
* {answer}
|
|
127
145
|
* </MarkdownText>
|
|
146
|
+
* @example
|
|
147
|
+
* <MarkdownText remarkPlugins={[remarkMentions]}>{turn.text}</MarkdownText>
|
|
128
148
|
*/
|
|
129
149
|
declare const MarkdownText: React.ForwardRefExoticComponent<MarkdownTextProps & React.RefAttributes<HTMLDivElement>>;
|
|
130
150
|
//#endregion
|
|
@@ -4,6 +4,8 @@ import { FOCUS_RING } from "../system/_focus-ring.js";
|
|
|
4
4
|
import { codeScroll, codeSurface, field, mono } from "./surfaces.js";
|
|
5
5
|
import * as React from "react";
|
|
6
6
|
import { jsx } from "react/jsx-runtime";
|
|
7
|
+
import { gfmTableFromMarkdown } from "mdast-util-gfm-table";
|
|
8
|
+
import { gfmTable } from "micromark-extension-gfm-table";
|
|
7
9
|
import Markdown from "react-markdown";
|
|
8
10
|
import rehypeSanitize from "rehype-sanitize";
|
|
9
11
|
//#region src/elements/markdown.tsx
|
|
@@ -19,9 +21,13 @@ import rehypeSanitize from "rehype-sanitize";
|
|
|
19
21
|
* `MarkdownText`'s JSDoc quotes it, and a test asserts nothing outside it
|
|
20
22
|
* survives.
|
|
21
23
|
*
|
|
24
|
+
* The six table elements are here because GFM's table syntax is the one part
|
|
25
|
+
* of GFM the parser reads (see {@link remarkTables}): a table is inert, and it
|
|
26
|
+
* is how an agent presents a comparison.
|
|
27
|
+
*
|
|
22
28
|
* Deliberately absent, and each for a reason: `input` (GFM task lists would
|
|
23
|
-
* put a control inside a transcript), `
|
|
24
|
-
*
|
|
29
|
+
* put a control inside a transcript), `sup`/`section` (footnotes, which a
|
|
30
|
+
* reply does not need), `svg`/`math` (an untrusted
|
|
25
31
|
* vector is a script host in several browsers' history), `details`/`summary`
|
|
26
32
|
* (disclosure is a component, not a token stream), and every sectioning or
|
|
27
33
|
* embedding tag — `iframe`, `object`, `embed`, `script`, `style`, `form`.
|
|
@@ -46,6 +52,12 @@ const ALLOWED_TAGS = [
|
|
|
46
52
|
"p",
|
|
47
53
|
"pre",
|
|
48
54
|
"strong",
|
|
55
|
+
"table",
|
|
56
|
+
"tbody",
|
|
57
|
+
"td",
|
|
58
|
+
"th",
|
|
59
|
+
"thead",
|
|
60
|
+
"tr",
|
|
49
61
|
"ul"
|
|
50
62
|
];
|
|
51
63
|
/**
|
|
@@ -72,6 +84,9 @@ const ALLOWED_PROTOCOLS = [
|
|
|
72
84
|
* handler unreachable by construction instead of by deny-list. `code` may
|
|
73
85
|
* carry a `className`, and only one matching `language-…`, because that is how
|
|
74
86
|
* a fenced block's info string reaches {@link MarkdownTextProps.renderCodeBlock}.
|
|
87
|
+
* A table cell may carry `align`, and only one of the three values the
|
|
88
|
+
* delimiter row can write; the renderer turns it into `text-align`, which is
|
|
89
|
+
* the one inline style anything in this surface ever carries.
|
|
75
90
|
*/
|
|
76
91
|
const SCHEMA = {
|
|
77
92
|
tagNames: [...ALLOWED_TAGS],
|
|
@@ -84,6 +99,18 @@ const SCHEMA = {
|
|
|
84
99
|
],
|
|
85
100
|
code: [["className", /^language-[\w+#.-]+$/]],
|
|
86
101
|
ol: ["start"],
|
|
102
|
+
th: [[
|
|
103
|
+
"align",
|
|
104
|
+
"left",
|
|
105
|
+
"center",
|
|
106
|
+
"right"
|
|
107
|
+
]],
|
|
108
|
+
td: [[
|
|
109
|
+
"align",
|
|
110
|
+
"left",
|
|
111
|
+
"center",
|
|
112
|
+
"right"
|
|
113
|
+
]],
|
|
87
114
|
"*": []
|
|
88
115
|
},
|
|
89
116
|
protocols: {
|
|
@@ -304,6 +331,20 @@ function sealStream(source) {
|
|
|
304
331
|
for (let index = 0; index < tail.length; index += 1) if (!erase.has(index)) sealed += tail[index];
|
|
305
332
|
return head + sealed;
|
|
306
333
|
}
|
|
334
|
+
/**
|
|
335
|
+
* GitHub's table syntax, as a remark plugin, and nothing else from GFM.
|
|
336
|
+
*
|
|
337
|
+
* `remark-gfm` would bring task lists (a checkbox in a transcript), footnotes
|
|
338
|
+
* and autolink literals along with tables. This registers the table tokenizer
|
|
339
|
+
* and its tree handlers alone, which is what `remark-gfm` itself does for each
|
|
340
|
+
* of its five extensions, one at a time. The sanitizer still decides what a
|
|
341
|
+
* table may render as: the six elements and a cell's `align`.
|
|
342
|
+
*/
|
|
343
|
+
function remarkTables() {
|
|
344
|
+
const data = this.data();
|
|
345
|
+
(data.micromarkExtensions ??= []).push(gfmTable());
|
|
346
|
+
(data.fromMarkdownExtensions ??= []).push(gfmTableFromMarkdown());
|
|
347
|
+
}
|
|
307
348
|
/** The text and info string of a fenced block, read off its hast node. */
|
|
308
349
|
function readCodeBlock(node) {
|
|
309
350
|
const code = node?.children?.find((child) => child.tagName === "code");
|
|
@@ -347,9 +388,11 @@ function readCodeBlock(node) {
|
|
|
347
388
|
* - **Attributes are enumerated, not filtered.** An element that is not in the
|
|
348
389
|
* schema renders nothing; an element that is renders only the attributes
|
|
349
390
|
* named beside it — `href`/`title` on a link, `src`/`alt`/`title` on an
|
|
350
|
-
* image, `language-…` on a `code
|
|
391
|
+
* image, `language-…` on a `code`, and `align` on a table cell, held to
|
|
392
|
+
* `left`, `center` or `right`. No element in the subset accepts `id`,
|
|
351
393
|
* `style`, `srcset`, `formaction` or any `on*` handler, so none of them has
|
|
352
|
-
* an attribute to arrive on
|
|
394
|
+
* an attribute to arrive on; the one inline style anything carries is a
|
|
395
|
+
* cell's `text-align`, which the renderer writes from that `align`.
|
|
353
396
|
* - **URLs are allow-listed twice.** `http`, `https` and `mailto` on both
|
|
354
397
|
* `href` and `src`; a relative URL stays relative; everything else — a
|
|
355
398
|
* `javascript:` link, a `data:text/html` image, `vbscript:`, `blob:`,
|
|
@@ -361,10 +404,14 @@ function readCodeBlock(node) {
|
|
|
361
404
|
*
|
|
362
405
|
* **The subset.** Paragraphs, ATX and setext headings, bullet and ordered
|
|
363
406
|
* lists, block quotes, thematic breaks, fenced and indented code, inline code,
|
|
364
|
-
* emphasis, strong, strikethrough, links, images,
|
|
365
|
-
*
|
|
366
|
-
*
|
|
367
|
-
*
|
|
407
|
+
* emphasis, strong, strikethrough, links, images, hard breaks, and GitHub
|
|
408
|
+
* tables. Tables are the one part of GFM the parser reads, each column aligned
|
|
409
|
+
* as its delimiter row says and wrapped in a named region that scrolls
|
|
410
|
+
* sideways; task lists, footnotes and autolink literals stay out — an explicit
|
|
411
|
+
* `<https://…>` autolink still works. Fenced blocks are plain text unless
|
|
412
|
+
* {@link MarkdownTextProps.renderCodeBlock} is given, and
|
|
413
|
+
* {@link MarkdownTextProps.remarkPlugins} adds parser plugins without widening
|
|
414
|
+
* the subset.
|
|
368
415
|
*
|
|
369
416
|
* **Streaming.** Pass `streaming` while the turn is arriving and the token the
|
|
370
417
|
* model is halfway through typing is hidden rather than rendered as
|
|
@@ -373,10 +420,11 @@ function readCodeBlock(node) {
|
|
|
373
420
|
* words appear as they arrive; only their formatting waits for the closing
|
|
374
421
|
* token.
|
|
375
422
|
*
|
|
376
|
-
* `react-markdown
|
|
377
|
-
*
|
|
423
|
+
* `react-markdown`, `rehype-sanitize` and GFM's table pair
|
|
424
|
+
* (`micromark-extension-gfm-table`, `mdast-util-gfm-table`) are optional peers
|
|
425
|
+
* of this subpath alone, which is why this component is not re-exported from
|
|
378
426
|
* `@cueplusplus/ui/elements`: a console that renders a transcript without
|
|
379
|
-
* markdown should install
|
|
427
|
+
* markdown should install none of them.
|
|
380
428
|
*
|
|
381
429
|
* @example
|
|
382
430
|
* <Message agent="researcher">
|
|
@@ -388,8 +436,10 @@ function readCodeBlock(node) {
|
|
|
388
436
|
* <MarkdownText renderCodeBlock={({ language, code }) => <Highlight lang={language} code={code} />}>
|
|
389
437
|
* {answer}
|
|
390
438
|
* </MarkdownText>
|
|
439
|
+
* @example
|
|
440
|
+
* <MarkdownText remarkPlugins={[remarkMentions]}>{turn.text}</MarkdownText>
|
|
391
441
|
*/
|
|
392
|
-
const MarkdownText = React.forwardRef(function MarkdownText({ className, children, streaming = false, target = "_blank", headingLevel = 2, renderCodeBlock, ...elementProps }, ref) {
|
|
442
|
+
const MarkdownText = React.forwardRef(function MarkdownText({ className, children, streaming = false, target = "_blank", headingLevel = 2, renderCodeBlock, remarkPlugins, ...elementProps }, ref) {
|
|
393
443
|
const source = streaming ? sealStream(children) : children;
|
|
394
444
|
const components = React.useMemo(() => markdownComponents({
|
|
395
445
|
target,
|
|
@@ -408,6 +458,7 @@ const MarkdownText = React.forwardRef(function MarkdownText({ className, childre
|
|
|
408
458
|
...elementProps,
|
|
409
459
|
children: /* @__PURE__ */ jsx(Markdown, {
|
|
410
460
|
components,
|
|
461
|
+
remarkPlugins: [remarkTables, ...remarkPlugins ?? []],
|
|
411
462
|
rehypePlugins: [[rehypeSanitize, SCHEMA]],
|
|
412
463
|
skipHtml: true,
|
|
413
464
|
urlTransform: safeUrl,
|
|
@@ -522,7 +573,45 @@ function markdownComponents(options) {
|
|
|
522
573
|
children: renderCodeBlock(block)
|
|
523
574
|
})
|
|
524
575
|
});
|
|
525
|
-
}
|
|
576
|
+
},
|
|
577
|
+
table: ({ children }) => /* @__PURE__ */ jsx("div", {
|
|
578
|
+
role: "region",
|
|
579
|
+
"aria-label": "Table",
|
|
580
|
+
tabIndex: 0,
|
|
581
|
+
"data-slot": "markdown-table-scroll",
|
|
582
|
+
className: cn("rounded-control border border-border outline-none", codeScroll, FOCUS_RING),
|
|
583
|
+
children: /* @__PURE__ */ jsx("table", {
|
|
584
|
+
"data-slot": "markdown-table",
|
|
585
|
+
className: "w-full border-collapse text-(length:--cue-text-ui)",
|
|
586
|
+
children
|
|
587
|
+
})
|
|
588
|
+
}),
|
|
589
|
+
thead: ({ children }) => /* @__PURE__ */ jsx("thead", {
|
|
590
|
+
"data-slot": "markdown-table-head",
|
|
591
|
+
className: "bg-surface-2",
|
|
592
|
+
children
|
|
593
|
+
}),
|
|
594
|
+
tbody: ({ children }) => /* @__PURE__ */ jsx("tbody", {
|
|
595
|
+
"data-slot": "markdown-table-body",
|
|
596
|
+
children
|
|
597
|
+
}),
|
|
598
|
+
tr: ({ children }) => /* @__PURE__ */ jsx("tr", {
|
|
599
|
+
"data-slot": "markdown-table-row",
|
|
600
|
+
className: "border-b border-border last:border-b-0",
|
|
601
|
+
children
|
|
602
|
+
}),
|
|
603
|
+
th: ({ children, style }) => /* @__PURE__ */ jsx("th", {
|
|
604
|
+
"data-slot": "markdown-table-header",
|
|
605
|
+
style,
|
|
606
|
+
className: "px-(--cue-space-3) py-(--cue-space-2) text-start align-bottom font-medium text-fg",
|
|
607
|
+
children
|
|
608
|
+
}),
|
|
609
|
+
td: ({ children, style }) => /* @__PURE__ */ jsx("td", {
|
|
610
|
+
"data-slot": "markdown-table-cell",
|
|
611
|
+
style,
|
|
612
|
+
className: "px-(--cue-space-3) py-(--cue-space-2) align-top text-fg",
|
|
613
|
+
children
|
|
614
|
+
})
|
|
526
615
|
};
|
|
527
616
|
}
|
|
528
617
|
//#endregion
|
package/dist/forms/_chassis.d.ts
CHANGED
|
@@ -105,7 +105,10 @@ import { FormControlSize } from "./_chassis-size.js";
|
|
|
105
105
|
* **`field-ground`** is `styles.css`'s opt-in relative fill (spec
|
|
106
106
|
* 2026-10-08-field-ground R3): inside `[data-field-ground="relative"]` it
|
|
107
107
|
* re-declares this field's own `--cue-sunken` one step off the ground it stands
|
|
108
|
-
* on; anywhere else it compiles to nothing that applies.
|
|
108
|
+
* on; anywhere else it compiles to nothing that applies. The same hook carries
|
|
109
|
+
* the contrast edge (cue-ui-60): inside `[data-field-edge="contrast"]` this
|
|
110
|
+
* field's resting stroke reads `--cue-field-edge`, its theme's border moved to
|
|
111
|
+
* 3:1 on `surface-1`.
|
|
109
112
|
*
|
|
110
113
|
* **The resting shade** ({@link REST_SHADE_CLASS}, spec §3.10) is the one
|
|
111
114
|
* shadow this recipe draws: the stroke paints a border and a ground and no
|
package/dist/forms/_chassis.js
CHANGED
|
@@ -112,7 +112,10 @@ import { INPUT_FACE } from "../system/_style-input-face.js";
|
|
|
112
112
|
* **`field-ground`** is `styles.css`'s opt-in relative fill (spec
|
|
113
113
|
* 2026-10-08-field-ground R3): inside `[data-field-ground="relative"]` it
|
|
114
114
|
* re-declares this field's own `--cue-sunken` one step off the ground it stands
|
|
115
|
-
* on; anywhere else it compiles to nothing that applies.
|
|
115
|
+
* on; anywhere else it compiles to nothing that applies. The same hook carries
|
|
116
|
+
* the contrast edge (cue-ui-60): inside `[data-field-edge="contrast"]` this
|
|
117
|
+
* field's resting stroke reads `--cue-field-edge`, its theme's border moved to
|
|
118
|
+
* 3:1 on `surface-1`.
|
|
116
119
|
*
|
|
117
120
|
* **The resting shade** ({@link REST_SHADE_CLASS}, spec §3.10) is the one
|
|
118
121
|
* shadow this recipe draws: the stroke paints a border and a ground and no
|
package/dist/styles.css
CHANGED
|
@@ -179,9 +179,26 @@
|
|
|
179
179
|
* the chain the stroke already reads, `var(--cue-style-field-fill,
|
|
180
180
|
* var(--cue-sunken))`: a named fill still wins, a disabled field follows, and
|
|
181
181
|
* no field's class grows by more than this utility's name. A consumer's own
|
|
182
|
-
* field can wear `field-ground` too.
|
|
182
|
+
* field can wear `field-ground` too.
|
|
183
|
+
*
|
|
184
|
+
* The contrast edge (cue-ui-60), and opt-in too. A theme's `--cue-border` is a
|
|
185
|
+
* box edge, quiet on purpose, and against `surface-1` it measured 1.07:1 to
|
|
186
|
+
* 2.02:1 in every shipped theme: under WCAG 1.4.11's 3:1 for the line that
|
|
187
|
+
* marks out where to type. `--cue-field-edge` is that border moved toward the
|
|
188
|
+
* ink until it clears 3:1 (each theme writes it per block, as a literal), and
|
|
189
|
+
* inside an ancestor carrying `data-field-edge="contrast"` a field's resting
|
|
190
|
+
* stroke reads it. Declared on the field for the reason the fill is: resolved
|
|
191
|
+
* here, it is the theme the field stands in, island or not. It sets both ends
|
|
192
|
+
* of the stroke, so it wins over a paint pack's gradient edge — the opt-in is
|
|
193
|
+
* a reader's need, and a pack is a look. The hover, focus and invalid edges are
|
|
194
|
+
* their own colours and do not move. Same hook as the relative fill, so no
|
|
195
|
+
* field's class grows for it. */
|
|
183
196
|
@utility field-ground {
|
|
184
197
|
:where([data-field-ground="relative"]) & {
|
|
185
198
|
--cue-sunken: color-mix(in oklab, var(--cue-ground, var(--cue-bg)), var(--cue-style-field-toward, var(--cue-fg)) var(--cue-style-field-step, 6%));
|
|
186
199
|
}
|
|
200
|
+
:where([data-field-edge="contrast"]) & {
|
|
201
|
+
--cue-paint-stroke-from: var(--cue-field-edge, var(--cue-border));
|
|
202
|
+
--cue-paint-stroke-to: var(--cue-field-edge, var(--cue-border));
|
|
203
|
+
}
|
|
187
204
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { CONTRAST_REQUIREMENTS, ContrastCheck, ContrastInput, ContrastReport, ContrastRequirement, WCAG_AAA_TEXT, WCAG_AA_NON_TEXT, WCAG_AA_TEXT, contrastRatio, contrastReport as contrastReport$1, relativeLuminance } from "@cueplusplus/theme-base/contrast";
|
|
2
|
-
export { CONTRAST_REQUIREMENTS, type ContrastCheck, type ContrastInput, type ContrastReport, type ContrastRequirement, WCAG_AAA_TEXT, WCAG_AA_NON_TEXT, WCAG_AA_TEXT, contrastRatio, contrastReport$1 as contrastReport, relativeLuminance };
|
|
1
|
+
import { CONTRAST_REQUIREMENTS, ContrastCheck, ContrastInput, ContrastReport, ContrastRequirement, WCAG_AAA_TEXT, WCAG_AA_NON_TEXT, WCAG_AA_TEXT, contrastRatio, contrastReport as contrastReport$1, edgeToContrast, relativeLuminance } from "@cueplusplus/theme-base/contrast";
|
|
2
|
+
export { CONTRAST_REQUIREMENTS, type ContrastCheck, type ContrastInput, type ContrastReport, type ContrastRequirement, WCAG_AAA_TEXT, WCAG_AA_NON_TEXT, WCAG_AA_TEXT, contrastRatio, contrastReport$1 as contrastReport, edgeToContrast, relativeLuminance };
|
package/dist/theming/contrast.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { CONTRAST_REQUIREMENTS, WCAG_AAA_TEXT, WCAG_AA_NON_TEXT, WCAG_AA_TEXT, contrastRatio, contrastReport as contrastReport$1, relativeLuminance } from "@cueplusplus/theme-base/contrast";
|
|
2
|
-
export { CONTRAST_REQUIREMENTS, WCAG_AAA_TEXT, WCAG_AA_NON_TEXT, WCAG_AA_TEXT, contrastRatio, contrastReport$1 as contrastReport, relativeLuminance };
|
|
1
|
+
import { CONTRAST_REQUIREMENTS, WCAG_AAA_TEXT, WCAG_AA_NON_TEXT, WCAG_AA_TEXT, contrastRatio, contrastReport as contrastReport$1, edgeToContrast, relativeLuminance } from "@cueplusplus/theme-base/contrast";
|
|
2
|
+
export { CONTRAST_REQUIREMENTS, WCAG_AAA_TEXT, WCAG_AA_NON_TEXT, WCAG_AA_TEXT, contrastRatio, contrastReport$1 as contrastReport, edgeToContrast, relativeLuminance };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { formatColorString } from "../color/_convert.js";
|
|
2
|
-
import { WCAG_AA_NON_TEXT, WCAG_AA_TEXT, contrastRatio, contrastReport } from "./contrast.js";
|
|
2
|
+
import { WCAG_AA_NON_TEXT, WCAG_AA_TEXT, contrastRatio, contrastReport, edgeToContrast } from "./contrast.js";
|
|
3
3
|
import { RESOLVED_MONO, assertCssValue, assertThemeName, derivedTokens, serializeSystemModeAliases, serializeThemeCss, themeSelector, tokenProperty } from "./serialize.js";
|
|
4
4
|
import { COLOR_CONTRACT, resolve } from "@cueplusplus/theme-base";
|
|
5
5
|
import { clampChroma, converter, formatHex, inGamut, parse } from "culori";
|
|
@@ -462,7 +462,10 @@ function createTheme(anchors) {
|
|
|
462
462
|
if (anchors.radiusScale !== void 0 && !(anchors.radiusScale > 0)) throw new TypeError(`radiusScale must be a positive number, got ${anchors.radiusScale}`);
|
|
463
463
|
const dark = resolveDark(anchors, preset.dark);
|
|
464
464
|
const light = anchors.supportsLight ?? preset.light !== null ? resolveLight(dark, preset.light) : null;
|
|
465
|
-
const
|
|
465
|
+
const fieldEdge = (colors) => edgeToContrast(colors["border"], colors["surface-1"], colors["fg"]);
|
|
466
|
+
const derivedFor = (colors) => Object.entries(derivedTokens()).map(([property, value]) => property === tokenProperty("field-edge") ? [property, fieldEdge(colors)] : [property, value]);
|
|
467
|
+
const derived = derivedFor(dark.colors);
|
|
468
|
+
const derivedLight = light === null ? null : derivedFor(light.colors);
|
|
466
469
|
const fonts = resolveFonts$1(anchors, preset.fonts);
|
|
467
470
|
const radius = anchors.radiusScale === void 0 ? null : radiusBlocks(name, anchors.radiusScale);
|
|
468
471
|
const darkPaletteSections = [
|
|
@@ -482,7 +485,7 @@ function createTheme(anchors) {
|
|
|
482
485
|
}]];
|
|
483
486
|
const lightSections = light === null ? null : [{ declarations: colorDeclarations(light.colors) }, {
|
|
484
487
|
comment: DERIVED_COMMENT,
|
|
485
|
-
declarations: derived
|
|
488
|
+
declarations: derivedLight ?? derived
|
|
486
489
|
}];
|
|
487
490
|
const blocks = [
|
|
488
491
|
{
|
|
@@ -502,10 +505,16 @@ function createTheme(anchors) {
|
|
|
502
505
|
];
|
|
503
506
|
const inputs = [{
|
|
504
507
|
mode: "dark",
|
|
505
|
-
tokens:
|
|
508
|
+
tokens: {
|
|
509
|
+
...dark.colors,
|
|
510
|
+
"field-edge": fieldEdge(dark.colors)
|
|
511
|
+
}
|
|
506
512
|
}, ...light === null ? [] : [{
|
|
507
513
|
mode: "light",
|
|
508
|
-
tokens:
|
|
514
|
+
tokens: {
|
|
515
|
+
...light.colors,
|
|
516
|
+
"field-edge": fieldEdge(light.colors)
|
|
517
|
+
},
|
|
509
518
|
provisional: light.provisional
|
|
510
519
|
}]];
|
|
511
520
|
const explicitCss = serializeThemeCss(blocks, { banner: `@cueplusplus/ui — theme "${name}"` }).trimEnd();
|
|
@@ -522,7 +531,7 @@ function createTheme(anchors) {
|
|
|
522
531
|
sections: lightSections ?? darkPaletteSections
|
|
523
532
|
}])}\n`,
|
|
524
533
|
tokens: Object.fromEntries(darkSections.flatMap((section) => section.declarations)),
|
|
525
|
-
light: light === null ? null : Object.fromEntries([...colorDeclarations(light.colors), ...derived]),
|
|
534
|
+
light: light === null ? null : Object.fromEntries([...colorDeclarations(light.colors), ...derivedLight ?? derived]),
|
|
526
535
|
report: contrastReport(inputs)
|
|
527
536
|
};
|
|
528
537
|
}
|
package/dist/theming/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CONTRAST_REQUIREMENTS, ContrastCheck, ContrastInput, ContrastReport, ContrastRequirement, WCAG_AAA_TEXT, WCAG_AA_NON_TEXT, WCAG_AA_TEXT, contrastRatio, contrastReport, relativeLuminance } from "./contrast.js";
|
|
1
|
+
import { CONTRAST_REQUIREMENTS, ContrastCheck, ContrastInput, ContrastReport, ContrastRequirement, WCAG_AAA_TEXT, WCAG_AA_NON_TEXT, WCAG_AA_TEXT, contrastRatio, contrastReport, edgeToContrast, relativeLuminance } from "./contrast.js";
|
|
2
2
|
import { ACCENT_FG_MINIMUM, ACCENT_HOVER_LIGHTNESS_STEP, BORDER_LIGHTNESS_STEPS, CreatedTheme, FG_MUTED_LIGHTNESS_STEP, FG_SUBTLE_LIGHTNESS_STEP, INK_RAMP_CHROMA_SCALE, LIGHT_GROUND_COMPRESSION, SELECTION_ACCENT_PERCENT, SUNKEN_LIGHTNESS_STEP, SURFACE_LIGHTNESS_STEPS, THEME_COLOR_TOKENS, ThemeAnchors, ThemeColorToken, createTheme } from "./create-theme.js";
|
|
3
3
|
import { CUE_TOKEN_PREFIX, DERIVED_TOKEN_TEMPLATES, RESOLVED_MONO, SerializeThemeCssOptions, THEME_NAME_PATTERN, ThemeBlock, ThemeBlockSection, ThemeSelectorOptions, assertCssValue, assertThemeName, derivedTokens, serializeThemeBlock, serializeThemeCss, themeSelector, tokenProperty } from "./serialize.js";
|
|
4
|
-
export { ACCENT_FG_MINIMUM, ACCENT_HOVER_LIGHTNESS_STEP, BORDER_LIGHTNESS_STEPS, CONTRAST_REQUIREMENTS, CUE_TOKEN_PREFIX, type ContrastCheck, type ContrastInput, type ContrastReport, type ContrastRequirement, type CreatedTheme, DERIVED_TOKEN_TEMPLATES, FG_MUTED_LIGHTNESS_STEP, FG_SUBTLE_LIGHTNESS_STEP, INK_RAMP_CHROMA_SCALE, LIGHT_GROUND_COMPRESSION, RESOLVED_MONO, SELECTION_ACCENT_PERCENT, SUNKEN_LIGHTNESS_STEP, SURFACE_LIGHTNESS_STEPS, type SerializeThemeCssOptions, THEME_COLOR_TOKENS, THEME_NAME_PATTERN, type ThemeAnchors, type ThemeBlock, type ThemeBlockSection, type ThemeColorToken, type ThemeSelectorOptions, WCAG_AAA_TEXT, WCAG_AA_NON_TEXT, WCAG_AA_TEXT, assertCssValue, assertThemeName, contrastRatio, contrastReport, createTheme, derivedTokens, relativeLuminance, serializeThemeBlock, serializeThemeCss, themeSelector, tokenProperty };
|
|
4
|
+
export { ACCENT_FG_MINIMUM, ACCENT_HOVER_LIGHTNESS_STEP, BORDER_LIGHTNESS_STEPS, CONTRAST_REQUIREMENTS, CUE_TOKEN_PREFIX, type ContrastCheck, type ContrastInput, type ContrastReport, type ContrastRequirement, type CreatedTheme, DERIVED_TOKEN_TEMPLATES, FG_MUTED_LIGHTNESS_STEP, FG_SUBTLE_LIGHTNESS_STEP, INK_RAMP_CHROMA_SCALE, LIGHT_GROUND_COMPRESSION, RESOLVED_MONO, SELECTION_ACCENT_PERCENT, SUNKEN_LIGHTNESS_STEP, SURFACE_LIGHTNESS_STEPS, type SerializeThemeCssOptions, THEME_COLOR_TOKENS, THEME_NAME_PATTERN, type ThemeAnchors, type ThemeBlock, type ThemeBlockSection, type ThemeColorToken, type ThemeSelectorOptions, WCAG_AAA_TEXT, WCAG_AA_NON_TEXT, WCAG_AA_TEXT, assertCssValue, assertThemeName, contrastRatio, contrastReport, createTheme, derivedTokens, edgeToContrast, relativeLuminance, serializeThemeBlock, serializeThemeCss, themeSelector, tokenProperty };
|
package/dist/theming/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CONTRAST_REQUIREMENTS, WCAG_AAA_TEXT, WCAG_AA_NON_TEXT, WCAG_AA_TEXT, contrastRatio, contrastReport, relativeLuminance } from "./contrast.js";
|
|
1
|
+
import { CONTRAST_REQUIREMENTS, WCAG_AAA_TEXT, WCAG_AA_NON_TEXT, WCAG_AA_TEXT, contrastRatio, contrastReport, edgeToContrast, relativeLuminance } from "./contrast.js";
|
|
2
2
|
import { CUE_TOKEN_PREFIX, DERIVED_TOKEN_TEMPLATES, RESOLVED_MONO, THEME_NAME_PATTERN, assertCssValue, assertThemeName, derivedTokens, serializeThemeBlock, serializeThemeCss, themeSelector, tokenProperty } from "./serialize.js";
|
|
3
3
|
import { ACCENT_FG_MINIMUM, ACCENT_HOVER_LIGHTNESS_STEP, BORDER_LIGHTNESS_STEPS, FG_MUTED_LIGHTNESS_STEP, FG_SUBTLE_LIGHTNESS_STEP, INK_RAMP_CHROMA_SCALE, LIGHT_GROUND_COMPRESSION, SELECTION_ACCENT_PERCENT, SUNKEN_LIGHTNESS_STEP, SURFACE_LIGHTNESS_STEPS, THEME_COLOR_TOKENS, createTheme } from "./create-theme.js";
|
|
4
|
-
export { ACCENT_FG_MINIMUM, ACCENT_HOVER_LIGHTNESS_STEP, BORDER_LIGHTNESS_STEPS, CONTRAST_REQUIREMENTS, CUE_TOKEN_PREFIX, DERIVED_TOKEN_TEMPLATES, FG_MUTED_LIGHTNESS_STEP, FG_SUBTLE_LIGHTNESS_STEP, INK_RAMP_CHROMA_SCALE, LIGHT_GROUND_COMPRESSION, RESOLVED_MONO, SELECTION_ACCENT_PERCENT, SUNKEN_LIGHTNESS_STEP, SURFACE_LIGHTNESS_STEPS, THEME_COLOR_TOKENS, THEME_NAME_PATTERN, WCAG_AAA_TEXT, WCAG_AA_NON_TEXT, WCAG_AA_TEXT, assertCssValue, assertThemeName, contrastRatio, contrastReport, createTheme, derivedTokens, relativeLuminance, serializeThemeBlock, serializeThemeCss, themeSelector, tokenProperty };
|
|
4
|
+
export { ACCENT_FG_MINIMUM, ACCENT_HOVER_LIGHTNESS_STEP, BORDER_LIGHTNESS_STEPS, CONTRAST_REQUIREMENTS, CUE_TOKEN_PREFIX, DERIVED_TOKEN_TEMPLATES, FG_MUTED_LIGHTNESS_STEP, FG_SUBTLE_LIGHTNESS_STEP, INK_RAMP_CHROMA_SCALE, LIGHT_GROUND_COMPRESSION, RESOLVED_MONO, SELECTION_ACCENT_PERCENT, SUNKEN_LIGHTNESS_STEP, SURFACE_LIGHTNESS_STEPS, THEME_COLOR_TOKENS, THEME_NAME_PATTERN, WCAG_AAA_TEXT, WCAG_AA_NON_TEXT, WCAG_AA_TEXT, assertCssValue, assertThemeName, contrastRatio, contrastReport, createTheme, derivedTokens, edgeToContrast, relativeLuminance, serializeThemeBlock, serializeThemeCss, themeSelector, tokenProperty };
|
|
@@ -242,7 +242,8 @@ const DERIVED_TOKEN_TEMPLATES = [
|
|
|
242
242
|
["accent-fill", "var(--cue-accent)"],
|
|
243
243
|
["accent-fill-fg", "var(--cue-accent-fg)"],
|
|
244
244
|
["accent-fill-hover", "var(--cue-accent-hover)"],
|
|
245
|
-
["shade", "var(--cue-fg)"]
|
|
245
|
+
["shade", "var(--cue-fg)"],
|
|
246
|
+
["field-edge", "var(--cue-border)"]
|
|
246
247
|
];
|
|
247
248
|
/**
|
|
248
249
|
* {@link DERIVED_TOKEN_TEMPLATES} as a property → value map.
|
|
@@ -4,11 +4,13 @@
|
|
|
4
4
|
"group": "elements",
|
|
5
5
|
"importPath": "@cueplusplus/ui/elements/markdown",
|
|
6
6
|
"peerDependencies": [
|
|
7
|
+
"mdast-util-gfm-table",
|
|
8
|
+
"micromark-extension-gfm-table",
|
|
7
9
|
"react-markdown",
|
|
8
10
|
"rehype-sanitize"
|
|
9
11
|
],
|
|
10
12
|
"clientOnly": true,
|
|
11
|
-
"description": "Model output, rendered.\n\nThe one component in this library that turns a string into markup, and the\nonly one that has to be argued for rather than described. Everywhere else —\n`Message`, `AskBox`, every element in this group — text is text: a string\narrives from a model and is placed in the DOM as characters, because a\ncomponent that quietly parsed it would put every consumer of this library one\nprompt away from an injection. This one parses, so it states in full what it\nwill and will not do with what it is given.\n\n**Threat model.** The input is hostile. Assume the string was written by a\nmodel that was told to write it by a web page it fetched, a file it read, or\na tool result it did not verify — the practical shape of prompt injection is\nnot \"the model turned evil\", it is \"the model faithfully relayed an\nattacker's bytes\". Three things follow.\n\n- **No raw HTML, ever, and not by filtering it.** Markup in the source has no\n path to the DOM, and the first reason is an absence rather than a setting.\n `react-markdown` hands `remark-rehype` its own `allowDangerousHtml: true`\n and spreads it *over* whatever `remarkRehypeOptions` says, so the option\n cannot be turned off from here and `<img onerror=…>` does become a `raw`\n node. A `raw` node is a span of source text rather than an element, and the\n one thing that turns it into markup is `rehype-raw`. Nothing in this package\n depends on that plugin, and a test asserts it never will — so the string has\n no parser to reach. `rehype-sanitize` then drops the `raw` type outright,\n and react-markdown's own `skipHtml` removes whatever is left. Three gates,\n none of which is a deny-list of dangerous tags. A `<script>`, an\n `<iframe>`, an `<svg>` and an `onclick=` are all the same non-event: there\n is nothing in the tree for them to be.\n- **Attributes are enumerated, not filtered.** An element that is not in the\n schema renders nothing; an element that is renders only the attributes\n named beside it — `href`/`title` on a link, `src`/`alt`/`title` on an\n image, `language-…` on a `code`. No element in the subset accepts `id`,\n `style`, `srcset`, `formaction` or any `on*` handler, so none of them has\n an attribute to arrive on
|
|
13
|
+
"description": "Model output, rendered.\n\nThe one component in this library that turns a string into markup, and the\nonly one that has to be argued for rather than described. Everywhere else —\n`Message`, `AskBox`, every element in this group — text is text: a string\narrives from a model and is placed in the DOM as characters, because a\ncomponent that quietly parsed it would put every consumer of this library one\nprompt away from an injection. This one parses, so it states in full what it\nwill and will not do with what it is given.\n\n**Threat model.** The input is hostile. Assume the string was written by a\nmodel that was told to write it by a web page it fetched, a file it read, or\na tool result it did not verify — the practical shape of prompt injection is\nnot \"the model turned evil\", it is \"the model faithfully relayed an\nattacker's bytes\". Three things follow.\n\n- **No raw HTML, ever, and not by filtering it.** Markup in the source has no\n path to the DOM, and the first reason is an absence rather than a setting.\n `react-markdown` hands `remark-rehype` its own `allowDangerousHtml: true`\n and spreads it *over* whatever `remarkRehypeOptions` says, so the option\n cannot be turned off from here and `<img onerror=…>` does become a `raw`\n node. A `raw` node is a span of source text rather than an element, and the\n one thing that turns it into markup is `rehype-raw`. Nothing in this package\n depends on that plugin, and a test asserts it never will — so the string has\n no parser to reach. `rehype-sanitize` then drops the `raw` type outright,\n and react-markdown's own `skipHtml` removes whatever is left. Three gates,\n none of which is a deny-list of dangerous tags. A `<script>`, an\n `<iframe>`, an `<svg>` and an `onclick=` are all the same non-event: there\n is nothing in the tree for them to be.\n- **Attributes are enumerated, not filtered.** An element that is not in the\n schema renders nothing; an element that is renders only the attributes\n named beside it — `href`/`title` on a link, `src`/`alt`/`title` on an\n image, `language-…` on a `code`, and `align` on a table cell, held to\n `left`, `center` or `right`. No element in the subset accepts `id`,\n `style`, `srcset`, `formaction` or any `on*` handler, so none of them has\n an attribute to arrive on; the one inline style anything carries is a\n cell's `text-align`, which the renderer writes from that `align`.\n- **URLs are allow-listed twice.** `http`, `https` and `mailto` on both\n `href` and `src`; a relative URL stays relative; everything else — a\n `javascript:` link, a `data:text/html` image, `vbscript:`, `blob:`,\n `file:`, `//host` — loses the attribute and leaves an inert element\n behind. Every link also carries `rel=\"noopener noreferrer nofollow\"`,\n which is not a prop: reverse tabnabbing, referrer leakage of a private\n console URL, and SEO laundering through a transcript are all things a\n caller should not be able to turn off by accident.\n\n**The subset.** Paragraphs, ATX and setext headings, bullet and ordered\nlists, block quotes, thematic breaks, fenced and indented code, inline code,\nemphasis, strong, strikethrough, links, images, hard breaks, and GitHub\ntables. Tables are the one part of GFM the parser reads, each column aligned\nas its delimiter row says and wrapped in a named region that scrolls\nsideways; task lists, footnotes and autolink literals stay out — an explicit\n`<https://…>` autolink still works. Fenced blocks are plain text unless\n{@link MarkdownTextProps.renderCodeBlock} is given, and\n{@link MarkdownTextProps.remarkPlugins} adds parser plugins without widening\nthe subset.\n\n**Streaming.** Pass `streaming` while the turn is arriving and the token the\nmodel is halfway through typing is hidden rather than rendered as\npunctuation: no asterisks flickering through every bold run, no half-written\nURL, no fence that opens a code block one frame and closes it the next. The\nwords appear as they arrive; only their formatting waits for the closing\ntoken.\n\n`react-markdown`, `rehype-sanitize` and GFM's table pair\n(`micromark-extension-gfm-table`, `mdast-util-gfm-table`) are optional peers\nof this subpath alone, which is why this component is not re-exported from\n`@cueplusplus/ui/elements`: a console that renders a transcript without\nmarkdown should install none of them.",
|
|
12
14
|
"props": [
|
|
13
15
|
{
|
|
14
16
|
"name": "children",
|
|
@@ -71,6 +73,17 @@
|
|
|
71
73
|
"kind": "callback",
|
|
72
74
|
"fixture": "callback.log"
|
|
73
75
|
}
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"name": "remarkPlugins",
|
|
79
|
+
"type": "PluggableList | null",
|
|
80
|
+
"required": false,
|
|
81
|
+
"defaultValue": null,
|
|
82
|
+
"description": "More remark plugins, run after the component's own table syntax.\n\nA seam for the parser, not for the page: whatever a plugin adds to the\ntree still passes the same sanitizer, so a plugin can change what the\nmarkdown *means* — a mention syntax, a glossary link — but cannot widen\nwhat renders. An element outside the subset is dropped, and an attribute\nthe schema does not name never arrives.",
|
|
83
|
+
"control": {
|
|
84
|
+
"kind": "opaque",
|
|
85
|
+
"nullable": true
|
|
86
|
+
}
|
|
74
87
|
}
|
|
75
88
|
],
|
|
76
89
|
"typeReferences": [
|
|
@@ -95,6 +108,7 @@
|
|
|
95
108
|
"--cue-space-3",
|
|
96
109
|
"--cue-space-5",
|
|
97
110
|
"--cue-style-focus-width",
|
|
111
|
+
"--cue-surface-2",
|
|
98
112
|
"--cue-text-body",
|
|
99
113
|
"--cue-text-emphasis",
|
|
100
114
|
"--cue-text-label",
|
|
@@ -105,7 +119,7 @@
|
|
|
105
119
|
"examples": [
|
|
106
120
|
{
|
|
107
121
|
"title": "Usage",
|
|
108
|
-
"code": "<Message agent=\"researcher\">\n <MarkdownText>{turn.text}</MarkdownText>\n</Message>\n<MarkdownText streaming={turn.status === \"running\"}>{turn.text}</MarkdownText>\n<MarkdownText renderCodeBlock={({ language, code }) => <Highlight lang={language} code={code} />}>\n {answer}\n</MarkdownText>",
|
|
122
|
+
"code": "<Message agent=\"researcher\">\n <MarkdownText>{turn.text}</MarkdownText>\n</Message>\n<MarkdownText streaming={turn.status === \"running\"}>{turn.text}</MarkdownText>\n<MarkdownText renderCodeBlock={({ language, code }) => <Highlight lang={language} code={code} />}>\n {answer}\n</MarkdownText>\n<MarkdownText remarkPlugins={[remarkMentions]}>{turn.text}</MarkdownText>",
|
|
109
123
|
"language": "tsx"
|
|
110
124
|
}
|
|
111
125
|
],
|
package/manifest/manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"library": "@cueplusplus/ui",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.23.0",
|
|
5
5
|
"generatedAt": "1970-01-01T00:00:00.000Z",
|
|
6
6
|
"themes": [
|
|
7
7
|
"command",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
],
|
|
39
39
|
"tokens": {
|
|
40
40
|
"path": "./tokens.json",
|
|
41
|
-
"sha256": "
|
|
41
|
+
"sha256": "a1f258bdddd984a1fc1eae5e2508d512fc568c82b7e3628e7c604f073c98e9df"
|
|
42
42
|
},
|
|
43
43
|
"systemApis": [
|
|
44
44
|
{
|
|
@@ -2320,7 +2320,7 @@
|
|
|
2320
2320
|
"mdUrl": "/docs/components/markdown-text.md",
|
|
2321
2321
|
"jsonUrl": "/r/components/markdown-text.json",
|
|
2322
2322
|
"path": "./components/markdown-text.json",
|
|
2323
|
-
"sha256": "
|
|
2323
|
+
"sha256": "09114d7367b8b47786418e5362e657ac5ae8ec3b5f831da985332f6a96d2be42"
|
|
2324
2324
|
},
|
|
2325
2325
|
{
|
|
2326
2326
|
"name": "MathBlock",
|
package/manifest/tokens.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"library": "@cueplusplus/tokens",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.23.0",
|
|
5
5
|
"themes": [
|
|
6
6
|
{
|
|
7
7
|
"name": "command",
|
|
@@ -283,6 +283,7 @@
|
|
|
283
283
|
"--cue-fg",
|
|
284
284
|
"--cue-fg-muted",
|
|
285
285
|
"--cue-fg-subtle",
|
|
286
|
+
"--cue-field-edge",
|
|
286
287
|
"--cue-focus",
|
|
287
288
|
"--cue-font-mono",
|
|
288
289
|
"--cue-font-theme-mono",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cueplusplus/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "CUE++ design system components: Tailwind v4 styled wrappers over Base UI, driven by @cueplusplus/tokens.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -145,8 +145,8 @@
|
|
|
145
145
|
"clsx": "^2.1.1",
|
|
146
146
|
"culori": "^4.0.2",
|
|
147
147
|
"tailwind-merge": "^3.6.0",
|
|
148
|
-
"@cueplusplus/theme-base": "^1.
|
|
149
|
-
"@cueplusplus/tokens": "0.
|
|
148
|
+
"@cueplusplus/theme-base": "^1.4.0",
|
|
149
|
+
"@cueplusplus/tokens": "0.23.0"
|
|
150
150
|
},
|
|
151
151
|
"peerDependencies": {
|
|
152
152
|
"@assistant-ui/react": "^0.15.16",
|
|
@@ -160,6 +160,8 @@
|
|
|
160
160
|
"embla-carousel-react": "^8.6.0",
|
|
161
161
|
"heat-graph": "^0.0.15",
|
|
162
162
|
"lucide-react": "^1.31.0",
|
|
163
|
+
"mdast-util-gfm-table": "^2.0.0",
|
|
164
|
+
"micromark-extension-gfm-table": "^2.1.1",
|
|
163
165
|
"react": "^19.0.0",
|
|
164
166
|
"react-aria-components": "^1.20.0",
|
|
165
167
|
"react-day-picker": "^10.0.1",
|
|
@@ -201,6 +203,12 @@
|
|
|
201
203
|
"lucide-react": {
|
|
202
204
|
"optional": true
|
|
203
205
|
},
|
|
206
|
+
"mdast-util-gfm-table": {
|
|
207
|
+
"optional": true
|
|
208
|
+
},
|
|
209
|
+
"micromark-extension-gfm-table": {
|
|
210
|
+
"optional": true
|
|
211
|
+
},
|
|
204
212
|
"react-aria-components": {
|
|
205
213
|
"optional": true
|
|
206
214
|
},
|
|
@@ -249,6 +257,8 @@
|
|
|
249
257
|
"heat-graph": "^0.0.15",
|
|
250
258
|
"jsdom": "^30.0.1",
|
|
251
259
|
"lucide-react": "^1.31.0",
|
|
260
|
+
"mdast-util-gfm-table": "^2.0.0",
|
|
261
|
+
"micromark-extension-gfm-table": "^2.1.1",
|
|
252
262
|
"publint": "^0.3.23",
|
|
253
263
|
"react": "^19.2.0",
|
|
254
264
|
"react-aria-components": "^1.20.0",
|
|
@@ -262,7 +272,7 @@
|
|
|
262
272
|
"tsdown": "0.22.14",
|
|
263
273
|
"typescript": "^5.7.0",
|
|
264
274
|
"vitest": "^4.1.10",
|
|
265
|
-
"@cueplusplus/theme-tools": "1.
|
|
275
|
+
"@cueplusplus/theme-tools": "1.3.0",
|
|
266
276
|
"@repo/docgen": "0.0.0",
|
|
267
277
|
"@repo/eslint-config": "0.0.0",
|
|
268
278
|
"@repo/typescript-config": "0.0.0"
|