@cueplusplus/ui 0.22.0 → 0.24.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 +72 -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 +19 -6
- package/dist/forms/_chassis.js +19 -6
- package/dist/primitives/chip.d.ts +1 -1
- package/dist/styles.css +20 -1
- package/dist/system/_focus-ring-field.js +14 -2
- 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/autocomplete.json +1 -0
- package/manifest/components/color-field.json +1 -0
- package/manifest/components/color-picker.json +1 -0
- package/manifest/components/combobox.json +1 -0
- package/manifest/components/composer.json +1 -0
- package/manifest/components/date-field.json +1 -0
- package/manifest/components/date-picker.json +1 -0
- package/manifest/components/date-range-picker.json +1 -0
- package/manifest/components/env-var-input.json +1 -0
- package/manifest/components/input-group.json +1 -0
- package/manifest/components/input.json +1 -0
- package/manifest/components/markdown-text.json +16 -2
- package/manifest/components/multi-select.json +1 -0
- package/manifest/components/musical-time-input.json +1 -0
- package/manifest/components/number-field.json +1 -0
- package/manifest/components/otp-field.json +1 -0
- package/manifest/components/password-input.json +1 -0
- package/manifest/components/scrub-input.json +1 -0
- package/manifest/components/search-input.json +1 -0
- package/manifest/components/select.json +1 -0
- package/manifest/components/tags-input.json +1 -0
- package/manifest/components/textarea.json +1 -0
- package/manifest/components/time-field.json +1 -0
- package/manifest/manifest.json +25 -25
- package/manifest/tokens.json +3 -1
- package/package.json +14 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,77 @@
|
|
|
1
1
|
# @cueplusplus/ui
|
|
2
2
|
|
|
3
|
+
## 0.24.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 082ef7c: **A focused field's ring is faint now, and you set how faint.** A text field shows its focus ring
|
|
8
|
+
on a mouse click as well as on Tab, so a full-strength `--cue-focus` ring (pure white on the
|
|
9
|
+
default theme) lit up every clicked field like a box. Every recessed field (inputs, textareas,
|
|
10
|
+
selects, comboboxes, the date, colour and number fields, grouped fields) now draws
|
|
11
|
+
`--cue-focus` mixed to **15%**, and its border still lifts to `border-strong` on focus.
|
|
12
|
+
|
|
13
|
+
- **`--cue-style-field-focus-mix`** sets the share, from `0%` (no ring, only the border lift) to
|
|
14
|
+
`100%` (the full ring). Name it in a style set, or on any element:
|
|
15
|
+
`style={{ "--cue-style-field-focus-mix": "40%" }}`.
|
|
16
|
+
- **Inside `data-field-edge="contrast"`** the ring is back at 100%, beside the 3:1 edge.
|
|
17
|
+
- **An invalid field** keeps its danger border, and its ring turns red at the same share.
|
|
18
|
+
- Buttons, tabs, links and every other control keep their full ring, which only shows to a
|
|
19
|
+
keyboard.
|
|
20
|
+
|
|
21
|
+
### Patch Changes
|
|
22
|
+
|
|
23
|
+
- Updated dependencies [082ef7c]
|
|
24
|
+
- @cueplusplus/tokens@0.24.0
|
|
25
|
+
|
|
26
|
+
## 0.23.0
|
|
27
|
+
|
|
28
|
+
### Minor Changes
|
|
29
|
+
|
|
30
|
+
- dae9dec: **A field's edge can reach 3:1: `data-field-edge="contrast"`** (cue-ui-60). Every shipped theme's
|
|
31
|
+
`--cue-border` measured 1.07:1 to 2.02:1 against `surface-1`, under WCAG 1.4.11's 3:1 for the line
|
|
32
|
+
that marks out where to type. The border stays as it is, because it is a box edge and a quiet one on
|
|
33
|
+
purpose, and a field's edge becomes a role of its own.
|
|
34
|
+
|
|
35
|
+
- **`--cue-field-edge`** is each block's border moved toward its ink until it clears 3:1 on its
|
|
36
|
+
`surface-1`, written per block as a literal. Every theme package, `theme-base`'s own palette and
|
|
37
|
+
`createTheme()` write it, and the contrast report holds it to 3:1 as the required pair
|
|
38
|
+
`field-edge/surface-1`.
|
|
39
|
+
- **Opt in with `data-field-edge="contrast"`** on any element, `<html>` included. Every recessed
|
|
40
|
+
control inside then draws its resting edge in `--cue-field-edge`, disabled ones too, and over a
|
|
41
|
+
paint pack's stroke. Hover, focus and invalid edges keep their own colours. Outside the attribute,
|
|
42
|
+
nothing changes.
|
|
43
|
+
- **`edgeToContrast(edge, ground, toward, minimum = 3)`** is the arithmetic, exported from
|
|
44
|
+
`@cueplusplus/theme-base/contrast` and `@cueplusplus/ui/theming`.
|
|
45
|
+
- `@cueplusplus/tokens` publishes `field-edge` as a derived name, with `var(--cue-border)` as its
|
|
46
|
+
template. A stylesheet built before this release falls back to the plain border.
|
|
47
|
+
|
|
48
|
+
**For a theme package of your own:** `theme-tools` now writes one more line per block. Run
|
|
49
|
+
`generate` once after upgrading; until then `check` reports the stylesheet out of date.
|
|
50
|
+
|
|
51
|
+
- 39884b3: **`MarkdownText` renders GitHub tables** (cue-ui-58). An agent's table used to arrive as one
|
|
52
|
+
paragraph of pipes: the parser read CommonMark only, and the sanitizer allowed no table elements.
|
|
53
|
+
Tables are now the one part of GFM the parser reads. Each column is aligned as its delimiter row
|
|
54
|
+
says, header cells sit on `surface-2`, and the table scrolls sideways inside a named,
|
|
55
|
+
keyboard-reachable region rather than widening the turn. Task lists, footnotes and autolink literals
|
|
56
|
+
stay out, and raw HTML is still refused: a cell's markup is text, and alignment is the only style a
|
|
57
|
+
cell carries.
|
|
58
|
+
|
|
59
|
+
**`remarkPlugins`** adds parser plugins after the table syntax. It is a seam for the parser, not
|
|
60
|
+
for the page: whatever a plugin puts in the tree passes the same sanitizer, so it can change what
|
|
61
|
+
the markdown means but cannot widen what renders.
|
|
62
|
+
|
|
63
|
+
### Migrating
|
|
64
|
+
- **`@cueplusplus/ui/elements/markdown` has two more optional peers**, the table half of GFM:
|
|
65
|
+
`pnpm add micromark-extension-gfm-table mdast-util-gfm-table`. Like `react-markdown` and
|
|
66
|
+
`rehype-sanitize`, they are needed only where this subpath is imported. A build that imports it
|
|
67
|
+
without them fails to resolve the module, and names it.
|
|
68
|
+
|
|
69
|
+
### Patch Changes
|
|
70
|
+
|
|
71
|
+
- Updated dependencies [dae9dec]
|
|
72
|
+
- @cueplusplus/tokens@0.23.0
|
|
73
|
+
- @cueplusplus/theme-base@1.4.0
|
|
74
|
+
|
|
3
75
|
## 0.22.0
|
|
4
76
|
|
|
5
77
|
### 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
|
@@ -35,6 +35,15 @@ import { FormControlSize } from "./_chassis-size.js";
|
|
|
35
35
|
* as it does on Tab. Only the non-typing controls that ride this chassis — the
|
|
36
36
|
* `Select` trigger, the date triggers — actually distinguish the two.
|
|
37
37
|
*
|
|
38
|
+
* **And quiet means faint** (the owner's call, 2026-10-09). At full strength a
|
|
39
|
+
* white `--cue-focus` ring around every clicked field read as a glaring box, so
|
|
40
|
+
* the ring is `--cue-focus` mixed with transparent to
|
|
41
|
+
* `--cue-style-field-focus-mix`: 15% unless a style set, `:root` or any subtree
|
|
42
|
+
* names another share (`_focus-ring-field.ts`). The rim's lift to
|
|
43
|
+
* `border-strong` carries the rest of the signal. The 3:1 paragraphs below
|
|
44
|
+
* hold for the `--cue-focus` role itself, and so for the ring at 100%, which
|
|
45
|
+
* is what `data-field-edge="contrast"` restores for a reader who needs it.
|
|
46
|
+
*
|
|
38
47
|
* `--cue-focus`, not `--cue-accent`. Every theme ships both roles, and keeping
|
|
39
48
|
* them separable is what WCAG 1.4.11 needs: one role whose job is "read at 3:1
|
|
40
49
|
* against the surface behind the ring", independent of the role whose job is
|
|
@@ -77,9 +86,10 @@ import { FormControlSize } from "./_chassis-size.js";
|
|
|
77
86
|
* while focused. Pushing the ring off the rim would mean either floating it on
|
|
78
87
|
* a gap (WCAG 2.4.13 then wants 3px, and the halo is back) or insetting it
|
|
79
88
|
* further (which buries it deeper). So the ring takes the danger colour instead
|
|
80
|
-
* —
|
|
81
|
-
* and Radix both reached: the border stays danger, the ring
|
|
82
|
-
* and red communicates invalid whichever of the two is
|
|
89
|
+
* — `--cue-danger` at the same faint share as the focus ring — which is the
|
|
90
|
+
* ruling shadcn and Radix both reached: the border stays danger, the ring
|
|
91
|
+
* communicates focus, and red communicates invalid whichever of the two is
|
|
92
|
+
* painting. At the faint share the danger rim shows through the ring.
|
|
83
93
|
*
|
|
84
94
|
* The compound modifier outranks the plain one on specificity — an attribute
|
|
85
95
|
* selector and a pseudo-class against one pseudo-class — so its position in
|
|
@@ -105,7 +115,10 @@ import { FormControlSize } from "./_chassis-size.js";
|
|
|
105
115
|
* **`field-ground`** is `styles.css`'s opt-in relative fill (spec
|
|
106
116
|
* 2026-10-08-field-ground R3): inside `[data-field-ground="relative"]` it
|
|
107
117
|
* 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.
|
|
118
|
+
* on; anywhere else it compiles to nothing that applies. The same hook carries
|
|
119
|
+
* the contrast edge (cue-ui-60): inside `[data-field-edge="contrast"]` this
|
|
120
|
+
* field's resting stroke reads `--cue-field-edge`, its theme's border moved to
|
|
121
|
+
* 3:1 on `surface-1`.
|
|
109
122
|
*
|
|
110
123
|
* **The resting shade** ({@link REST_SHADE_CLASS}, spec §3.10) is the one
|
|
111
124
|
* shadow this recipe draws: the stroke paints a border and a ground and no
|
|
@@ -115,7 +128,7 @@ import { FormControlSize } from "./_chassis-size.js";
|
|
|
115
128
|
* own `shadow-` utility replaces it through `twMerge`, as it would have
|
|
116
129
|
* replaced nothing before.
|
|
117
130
|
*/
|
|
118
|
-
declare const controlSurface = "w-full field-ground rounded-[var(--cue-style-field-radius,calc(var(--cue-style-control-radius,var(--cue-radius-control))*var(--cue-radius-scale)))] border-transparent border-(length:--cue-paint-stroke-width) bg-[linear-gradient(var(--cue-style-field-fill,var(--cue-sunken)),var(--cue-style-field-fill,var(--cue-sunken))),linear-gradient(var(--cue-paint-stroke-angle),var(--cue-paint-stroke-from,var(--cue-border)),var(--cue-paint-stroke-to,var(--cue-border)))] bg-origin-border [background-clip:padding-box,border-box] shadow-[shadow:0_calc(2px*var(--cue-paint-shadow-scale))_calc(4px*var(--cue-paint-shadow-scale))_var(--cue-paint-rest-shade,transparent)] font-[family-name:var(--cue-style-input-face,var(--cue-font-mono))] font-[number:var(--cue-style-body-weight,inherit)] text-fg placeholder:text-fg-subtle outline-none hover:border-[color:var(--cue-style-field-hover-edge,transparent)] focus:border-[color:var(--cue-style-focus-edge,var(--cue-border-strong))] focus-visible:outline-solid focus-visible:outline-[length:var(--cue-style-focus-width,var(--cue-focus-ring))] focus-visible:outline-(--cue-focus) focus-visible:outline-offset-[var(--cue-style-field-focus-offset,-1px)] disabled:opacity-[var(--cue-style-disabled-opacity,0.4)] data-[disabled]:opacity-[var(--cue-style-disabled-opacity,0.4)] disabled:bg-[linear-gradient(var(--cue-style-disabled-fill,var(--cue-style-field-fill,var(--cue-sunken))),var(--cue-style-disabled-fill,var(--cue-style-field-fill,var(--cue-sunken)))),linear-gradient(var(--cue-paint-stroke-angle),var(--cue-paint-stroke-from,var(--cue-border)),var(--cue-paint-stroke-to,var(--cue-border)))] data-[disabled]:bg-[linear-gradient(var(--cue-style-disabled-fill,var(--cue-style-field-fill,var(--cue-sunken))),var(--cue-style-disabled-fill,var(--cue-style-field-fill,var(--cue-sunken)))),linear-gradient(var(--cue-paint-stroke-angle),var(--cue-paint-stroke-from,var(--cue-border)),var(--cue-paint-stroke-to,var(--cue-border)))] data-[invalid]:border-danger data-[invalid]:focus-visible:outline-danger";
|
|
131
|
+
declare const controlSurface = "w-full field-ground rounded-[var(--cue-style-field-radius,calc(var(--cue-style-control-radius,var(--cue-radius-control))*var(--cue-radius-scale)))] border-transparent border-(length:--cue-paint-stroke-width) bg-[linear-gradient(var(--cue-style-field-fill,var(--cue-sunken)),var(--cue-style-field-fill,var(--cue-sunken))),linear-gradient(var(--cue-paint-stroke-angle),var(--cue-paint-stroke-from,var(--cue-border)),var(--cue-paint-stroke-to,var(--cue-border)))] bg-origin-border [background-clip:padding-box,border-box] shadow-[shadow:0_calc(2px*var(--cue-paint-shadow-scale))_calc(4px*var(--cue-paint-shadow-scale))_var(--cue-paint-rest-shade,transparent)] font-[family-name:var(--cue-style-input-face,var(--cue-font-mono))] font-[number:var(--cue-style-body-weight,inherit)] text-fg placeholder:text-fg-subtle outline-none hover:border-[color:var(--cue-style-field-hover-edge,transparent)] focus:border-[color:var(--cue-style-focus-edge,var(--cue-border-strong))] focus-visible:outline-solid focus-visible:outline-[length:var(--cue-style-focus-width,var(--cue-focus-ring))] focus-visible:outline-[color:color-mix(in_oklab,var(--cue-focus)_var(--cue-style-field-focus-mix,15%),transparent)] focus-visible:outline-offset-[var(--cue-style-field-focus-offset,-1px)] disabled:opacity-[var(--cue-style-disabled-opacity,0.4)] data-[disabled]:opacity-[var(--cue-style-disabled-opacity,0.4)] disabled:bg-[linear-gradient(var(--cue-style-disabled-fill,var(--cue-style-field-fill,var(--cue-sunken))),var(--cue-style-disabled-fill,var(--cue-style-field-fill,var(--cue-sunken)))),linear-gradient(var(--cue-paint-stroke-angle),var(--cue-paint-stroke-from,var(--cue-border)),var(--cue-paint-stroke-to,var(--cue-border)))] data-[disabled]:bg-[linear-gradient(var(--cue-style-disabled-fill,var(--cue-style-field-fill,var(--cue-sunken))),var(--cue-style-disabled-fill,var(--cue-style-field-fill,var(--cue-sunken)))),linear-gradient(var(--cue-paint-stroke-angle),var(--cue-paint-stroke-from,var(--cue-border)),var(--cue-paint-stroke-to,var(--cue-border)))] data-[invalid]:border-danger data-[invalid]:focus-visible:outline-[color:color-mix(in_oklab,var(--cue-danger)_var(--cue-style-field-focus-mix,15%),transparent)]";
|
|
119
132
|
/**
|
|
120
133
|
* The full recessed-control recipe, one variant per rung of the control ladder.
|
|
121
134
|
*
|
|
@@ -185,6 +198,6 @@ declare const bareControlClasses = "min-w-0 flex-1 border-0 bg-transparent p-0 f
|
|
|
185
198
|
* pass's browser run caught it (`form-focus.spec.ts`, a grouped field). The
|
|
186
199
|
* wrapper never takes focus itself, so `:has(:focus)` is the same test.
|
|
187
200
|
*/
|
|
188
|
-
declare const controlGroupFocusClasses = "has-focus:border-border-strong has-focus-visible:outline-solid has-focus-visible:outline-[length:var(--cue-style-focus-width,var(--cue-focus-ring))] has-focus-visible:outline-(--cue-focus) has-focus-visible:outline-offset-[var(--cue-style-field-focus-offset,-1px)] data-[invalid]:has-focus-visible:outline-danger";
|
|
201
|
+
declare const controlGroupFocusClasses = "has-focus:border-border-strong has-focus-visible:outline-solid has-focus-visible:outline-[length:var(--cue-style-focus-width,var(--cue-focus-ring))] has-focus-visible:outline-[color:color-mix(in_oklab,var(--cue-focus)_var(--cue-style-field-focus-mix,15%),transparent)] has-focus-visible:outline-offset-[var(--cue-style-field-focus-offset,-1px)] data-[invalid]:has-focus-visible:outline-[color:color-mix(in_oklab,var(--cue-danger)_var(--cue-style-field-focus-mix,15%),transparent)]";
|
|
189
202
|
//#endregion
|
|
190
203
|
export { ControlVariantProps, type FormControlSize, bareControlClasses, controlClasses, controlGroupFocusClasses, controlSurface, controlVariants };
|
package/dist/forms/_chassis.js
CHANGED
|
@@ -42,6 +42,15 @@ import { INPUT_FACE } from "../system/_style-input-face.js";
|
|
|
42
42
|
* as it does on Tab. Only the non-typing controls that ride this chassis — the
|
|
43
43
|
* `Select` trigger, the date triggers — actually distinguish the two.
|
|
44
44
|
*
|
|
45
|
+
* **And quiet means faint** (the owner's call, 2026-10-09). At full strength a
|
|
46
|
+
* white `--cue-focus` ring around every clicked field read as a glaring box, so
|
|
47
|
+
* the ring is `--cue-focus` mixed with transparent to
|
|
48
|
+
* `--cue-style-field-focus-mix`: 15% unless a style set, `:root` or any subtree
|
|
49
|
+
* names another share (`_focus-ring-field.ts`). The rim's lift to
|
|
50
|
+
* `border-strong` carries the rest of the signal. The 3:1 paragraphs below
|
|
51
|
+
* hold for the `--cue-focus` role itself, and so for the ring at 100%, which
|
|
52
|
+
* is what `data-field-edge="contrast"` restores for a reader who needs it.
|
|
53
|
+
*
|
|
45
54
|
* `--cue-focus`, not `--cue-accent`. Every theme ships both roles, and keeping
|
|
46
55
|
* them separable is what WCAG 1.4.11 needs: one role whose job is "read at 3:1
|
|
47
56
|
* against the surface behind the ring", independent of the role whose job is
|
|
@@ -84,9 +93,10 @@ import { INPUT_FACE } from "../system/_style-input-face.js";
|
|
|
84
93
|
* while focused. Pushing the ring off the rim would mean either floating it on
|
|
85
94
|
* a gap (WCAG 2.4.13 then wants 3px, and the halo is back) or insetting it
|
|
86
95
|
* further (which buries it deeper). So the ring takes the danger colour instead
|
|
87
|
-
* —
|
|
88
|
-
* and Radix both reached: the border stays danger, the ring
|
|
89
|
-
* and red communicates invalid whichever of the two is
|
|
96
|
+
* — `--cue-danger` at the same faint share as the focus ring — which is the
|
|
97
|
+
* ruling shadcn and Radix both reached: the border stays danger, the ring
|
|
98
|
+
* communicates focus, and red communicates invalid whichever of the two is
|
|
99
|
+
* painting. At the faint share the danger rim shows through the ring.
|
|
90
100
|
*
|
|
91
101
|
* The compound modifier outranks the plain one on specificity — an attribute
|
|
92
102
|
* selector and a pseudo-class against one pseudo-class — so its position in
|
|
@@ -112,7 +122,10 @@ import { INPUT_FACE } from "../system/_style-input-face.js";
|
|
|
112
122
|
* **`field-ground`** is `styles.css`'s opt-in relative fill (spec
|
|
113
123
|
* 2026-10-08-field-ground R3): inside `[data-field-ground="relative"]` it
|
|
114
124
|
* 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.
|
|
125
|
+
* on; anywhere else it compiles to nothing that applies. The same hook carries
|
|
126
|
+
* the contrast edge (cue-ui-60): inside `[data-field-edge="contrast"]` this
|
|
127
|
+
* field's resting stroke reads `--cue-field-edge`, its theme's border moved to
|
|
128
|
+
* 3:1 on `surface-1`.
|
|
116
129
|
*
|
|
117
130
|
* **The resting shade** ({@link REST_SHADE_CLASS}, spec §3.10) is the one
|
|
118
131
|
* shadow this recipe draws: the stroke paints a border and a ground and no
|
|
@@ -122,7 +135,7 @@ import { INPUT_FACE } from "../system/_style-input-face.js";
|
|
|
122
135
|
* own `shadow-` utility replaces it through `twMerge`, as it would have
|
|
123
136
|
* replaced nothing before.
|
|
124
137
|
*/
|
|
125
|
-
const controlSurface = `w-full field-ground ${FIELD_RADIUS} ${STROKE_SUNKEN} ${REST_SHADE_CLASS} ${INPUT_FACE} ${BODY_WEIGHT} text-fg placeholder:text-fg-subtle outline-none ${FIELD_HOVER_EDGE} focus:border-[color:var(--cue-style-focus-edge,var(--cue-border-strong))] ${FOCUS_RING_FIELD} ${DISABLED_OPACITY} ${DATA_DISABLED_OPACITY} ${FIELD_DISABLED_FILL} ${INVALID_EDGE} data-[invalid]:focus-visible:outline-danger`;
|
|
138
|
+
const controlSurface = `w-full field-ground ${FIELD_RADIUS} ${STROKE_SUNKEN} ${REST_SHADE_CLASS} ${INPUT_FACE} ${BODY_WEIGHT} text-fg placeholder:text-fg-subtle outline-none ${FIELD_HOVER_EDGE} focus:border-[color:var(--cue-style-focus-edge,var(--cue-border-strong))] ${FOCUS_RING_FIELD} ${DISABLED_OPACITY} ${DATA_DISABLED_OPACITY} ${FIELD_DISABLED_FILL} ${INVALID_EDGE} data-[invalid]:focus-visible:outline-[color:color-mix(in_oklab,var(--cue-danger)_var(--cue-style-field-focus-mix,15%),transparent)]`;
|
|
126
139
|
/**
|
|
127
140
|
* The full recessed-control recipe, one variant per rung of the control ladder.
|
|
128
141
|
*
|
|
@@ -195,6 +208,6 @@ const bareControlClasses = `min-w-0 flex-1 border-0 bg-transparent p-0 ${INPUT_F
|
|
|
195
208
|
* pass's browser run caught it (`form-focus.spec.ts`, a grouped field). The
|
|
196
209
|
* wrapper never takes focus itself, so `:has(:focus)` is the same test.
|
|
197
210
|
*/
|
|
198
|
-
const controlGroupFocusClasses = `has-focus:border-border-strong ${FOCUS_RING_GROUP_FIELD} data-[invalid]:has-focus-visible:outline-danger`;
|
|
211
|
+
const controlGroupFocusClasses = `has-focus:border-border-strong ${FOCUS_RING_GROUP_FIELD} data-[invalid]:has-focus-visible:outline-[color:color-mix(in_oklab,var(--cue-danger)_var(--cue-style-field-focus-mix,15%),transparent)]`;
|
|
199
212
|
//#endregion
|
|
200
213
|
export { bareControlClasses, controlClasses, controlGroupFocusClasses, controlSurface, controlVariants };
|
|
@@ -2,7 +2,7 @@ import { ClassProp, VariantProps } from "../lib/cva.js";
|
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
//#region src/primitives/chip.d.ts
|
|
4
4
|
declare const chipVariants: (props?: ({
|
|
5
|
-
tone?: "danger" | "info" | "warn" | "
|
|
5
|
+
tone?: "danger" | "info" | "warn" | "accent" | "ok" | "busy" | "neutral" | null | undefined;
|
|
6
6
|
variant?: "outline" | "tinted" | "tag" | null | undefined;
|
|
7
7
|
interactive?: boolean | null | undefined;
|
|
8
8
|
} & ClassProp) | undefined) => string;
|
package/dist/styles.css
CHANGED
|
@@ -179,9 +179,28 @@
|
|
|
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
|
+
/* The full focus ring too: a field's is faint by default (`_focus-ring-field.ts`). */
|
|
204
|
+
--cue-style-field-focus-mix: 100%;
|
|
205
|
+
}
|
|
187
206
|
}
|
|
@@ -11,19 +11,31 @@
|
|
|
11
11
|
* would move with the buttons). `FOCUS_RING_INSET` keeps its fixed offset
|
|
12
12
|
* for its ten other consumers.
|
|
13
13
|
*
|
|
14
|
+
* **Faint, and configurable.** A text field matches `:focus-visible` on a
|
|
15
|
+
* mouse click as well as on Tab, so its ring is the one every click lights up.
|
|
16
|
+
* Drawn at full strength (a white `--cue-focus` on the default theme) it read
|
|
17
|
+
* as a glaring box, so a field paints `--cue-focus` mixed with transparent to
|
|
18
|
+
* `--cue-style-field-focus-mix`: 15% unless a style set, `:root` or any
|
|
19
|
+
* subtree names another share (0% hides it, 100% is the full ring). The
|
|
20
|
+
* border's lift to `border-strong` on `:focus` carries the rest of the signal.
|
|
21
|
+
* Inside `data-field-edge="contrast"` (`styles.css`) the share is 100%, so the
|
|
22
|
+
* reader who opted into the 3:1 edge gets the full ring back. Every other
|
|
23
|
+
* control's keyboard ring (`_focus-ring.ts`) is unchanged: it only ever shows
|
|
24
|
+
* to a keyboard.
|
|
25
|
+
*
|
|
14
26
|
* Its own module, not a constant in `_focus-ring.ts`: docgen attributes a
|
|
15
27
|
* token to a component by the file its class string lives in, and every
|
|
16
28
|
* focusable component imports that module.
|
|
17
29
|
*
|
|
18
30
|
* @internal Not part of the public API.
|
|
19
31
|
*/
|
|
20
|
-
const FOCUS_RING_FIELD = "focus-visible:outline-solid focus-visible:outline-[length:var(--cue-style-focus-width,var(--cue-focus-ring))] focus-visible:outline-(--cue-focus) focus-visible:outline-offset-[var(--cue-style-field-focus-offset,-1px)]";
|
|
32
|
+
const FOCUS_RING_FIELD = "focus-visible:outline-solid focus-visible:outline-[length:var(--cue-style-focus-width,var(--cue-focus-ring))] focus-visible:outline-[color:color-mix(in_oklab,var(--cue-focus)_var(--cue-style-field-focus-mix,15%),transparent)] focus-visible:outline-offset-[var(--cue-style-field-focus-offset,-1px)]";
|
|
21
33
|
/**
|
|
22
34
|
* `FOCUS_RING_GROUP_INSET` with {@link FOCUS_RING_FIELD}'s offset: the
|
|
23
35
|
* grouped field's ring reads `style-field-focus-offset` as the field it wraps
|
|
24
36
|
* does (plan V.2, T25), so a grouped field and a plain one keep one focus
|
|
25
37
|
* geometry under every Look.
|
|
26
38
|
*/
|
|
27
|
-
const FOCUS_RING_GROUP_FIELD = "has-focus-visible:outline-solid has-focus-visible:outline-[length:var(--cue-style-focus-width,var(--cue-focus-ring))] has-focus-visible:outline-(--cue-focus) has-focus-visible:outline-offset-[var(--cue-style-field-focus-offset,-1px)]";
|
|
39
|
+
const FOCUS_RING_GROUP_FIELD = "has-focus-visible:outline-solid has-focus-visible:outline-[length:var(--cue-style-focus-width,var(--cue-focus-ring))] has-focus-visible:outline-[color:color-mix(in_oklab,var(--cue-focus)_var(--cue-style-field-focus-mix,15%),transparent)] has-focus-visible:outline-offset-[var(--cue-style-field-focus-offset,-1px)]";
|
|
28
40
|
//#endregion
|
|
29
41
|
export { FOCUS_RING_FIELD, FOCUS_RING_GROUP_FIELD };
|
|
@@ -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
|
}
|