@candor-ui/react 1.0.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/LICENSE +3 -2
  2. package/dist/blocks/copy-field.d.ts +6 -0
  3. package/dist/blocks/copy-field.js +9 -1
  4. package/dist/blocks/degraded-panel.d.ts +6 -0
  5. package/dist/blocks/degraded-panel.js +9 -1
  6. package/dist/blocks/locale.d.ts +86 -0
  7. package/dist/blocks/locale.js +146 -0
  8. package/dist/blocks/relative-time.d.ts +21 -3
  9. package/dist/blocks/relative-time.js +30 -11
  10. package/dist/blocks/status-dot.js +4 -2
  11. package/dist/blocks/step-list.js +4 -2
  12. package/dist/blocks/streaming-text.js +1 -1
  13. package/dist/blocks/surface.d.ts +2 -2
  14. package/dist/chat/agent-avatar.d.ts +14 -0
  15. package/dist/chat/agent-avatar.js +26 -2
  16. package/dist/chat/attachment-preview.d.ts +53 -0
  17. package/dist/chat/attachment-preview.js +82 -0
  18. package/dist/chat/card-registry.d.ts +7 -0
  19. package/dist/chat/card-registry.js +14 -4
  20. package/dist/chat/citation.d.ts +15 -0
  21. package/dist/chat/citation.js +34 -7
  22. package/dist/chat/composer.d.ts +30 -4
  23. package/dist/chat/composer.js +69 -20
  24. package/dist/chat/context-meter.d.ts +9 -0
  25. package/dist/chat/context-meter.js +21 -4
  26. package/dist/chat/entity-chip.d.ts +19 -0
  27. package/dist/chat/entity-chip.js +37 -3
  28. package/dist/chat/folded-turns.d.ts +33 -0
  29. package/dist/chat/folded-turns.js +29 -0
  30. package/dist/chat/message-actions.d.ts +9 -0
  31. package/dist/chat/message-actions.js +12 -1
  32. package/dist/chat/message-content.d.ts +12 -3
  33. package/dist/chat/message-content.js +24 -6
  34. package/dist/chat/message.d.ts +81 -4
  35. package/dist/chat/message.js +50 -9
  36. package/dist/chat/payload.d.ts +21 -4
  37. package/dist/chat/payload.js +38 -5
  38. package/dist/chat/prompt-suggestions.d.ts +6 -2
  39. package/dist/chat/prompt-suggestions.js +9 -2
  40. package/dist/chat/reasoning.d.ts +15 -0
  41. package/dist/chat/reasoning.js +12 -3
  42. package/dist/chat/thread-divider.d.ts +18 -0
  43. package/dist/chat/thread-divider.js +39 -0
  44. package/dist/chat/thread-event.d.ts +19 -0
  45. package/dist/chat/thread-event.js +15 -0
  46. package/dist/chat/thread.d.ts +12 -1
  47. package/dist/chat/thread.js +32 -4
  48. package/dist/chat/tool-call.d.ts +12 -0
  49. package/dist/chat/tool-call.js +16 -3
  50. package/dist/index.d.ts +7 -0
  51. package/dist/index.js +7 -0
  52. package/dist/lib/chat.d.ts +11 -1
  53. package/dist/lib/highlight.d.ts +15 -0
  54. package/dist/lib/highlight.js +224 -0
  55. package/dist/lib/status.d.ts +15 -0
  56. package/dist/lib/status.js +10 -1
  57. package/dist/lib/use-virtual-list.d.ts +39 -0
  58. package/dist/lib/use-virtual-list.js +124 -0
  59. package/dist/styles.css +40 -1
  60. package/dist/ui/button.d.ts +1 -1
  61. package/package.json +1 -1
@@ -0,0 +1,224 @@
1
+ /**
2
+ * A small syntax highlighter for code an agent shows: it splits text into typed tokens (keyword, string, comment…),
3
+ * and Payload renders each as a span. The output is text, never HTML, so nothing in the code can run or inject markup.
4
+ * It covers the languages agents show most (JS/TS, Python, SQL, JSON, shell, YAML, CSS, HTML/XML, Go, Rust, Java, C)
5
+ * in a few kilobytes; anything else comes back as one plain token.
6
+ */
7
+ const words = (s) => new Set(s.split(" "));
8
+ const js = words("abstract as async await break case catch class const continue debugger default delete do else enum export extends false finally for from function get if implements import in instanceof interface let new null of private protected public readonly return satisfies set static super switch this throw true try type typeof undefined var void while with yield");
9
+ const c = words("auto break case char const continue default do double else enum extern float for goto if int long register return short signed sizeof static struct switch typedef union unsigned void volatile while true false null NULL");
10
+ const grammars = {
11
+ js: { line: ["//"], block: [["/*", "*/"]], strings: ['"', "'", "`"], keywords: js },
12
+ python: {
13
+ line: ["#"],
14
+ strings: ['"""', "'''", '"', "'"],
15
+ keywords: words("and as assert async await break class continue def del elif else except False finally for from global if import in is lambda None nonlocal not or pass raise return True try while with yield match case self"),
16
+ },
17
+ sql: {
18
+ line: ["--"],
19
+ block: [["/*", "*/"]],
20
+ strings: ["'", '"'],
21
+ caseless: true,
22
+ keywords: words("select from where and or not in is null as join left right inner outer full cross on group by order having limit offset insert into values update set delete create table alter drop index view distinct union all case when then else end asc desc count sum avg min max interval like between exists primary key foreign references default with returning true false coalesce cast"),
23
+ },
24
+ json: { strings: ['"'], keywords: words("true false null"), properties: "json" },
25
+ bash: {
26
+ line: ["#"],
27
+ strings: ['"', "'"],
28
+ variables: true,
29
+ keywords: words("if then else elif fi for while until do done case esac in function return export local readonly set unset echo cd exit source alias sudo npm npx git curl"),
30
+ },
31
+ yaml: { line: ["#"], strings: ['"', "'"], keywords: words("true false null yes no on off"), properties: "yaml" },
32
+ css: { block: [["/*", "*/"]], strings: ['"', "'"], keywords: words("important inherit initial unset none auto") },
33
+ html: { block: [["<!--", "-->"]], strings: ['"', "'"], keywords: new Set(), markup: true },
34
+ go: {
35
+ line: ["//"],
36
+ block: [["/*", "*/"]],
37
+ strings: ['"', "`", "'"],
38
+ keywords: words("break case chan const continue default defer else fallthrough for func go goto if import interface map package range return select struct switch type var nil true false"),
39
+ },
40
+ rust: {
41
+ line: ["//"],
42
+ block: [["/*", "*/"]],
43
+ strings: ['"'],
44
+ keywords: words("as async await break const continue crate dyn else enum extern false fn for if impl in let loop match mod move mut pub ref return self Self static struct super trait true type unsafe use where while"),
45
+ },
46
+ java: {
47
+ line: ["//"],
48
+ block: [["/*", "*/"]],
49
+ strings: ['"', "'"],
50
+ keywords: words("abstract boolean break byte case catch char class const continue default do double else enum extends final finally float for if implements import instanceof int interface long new null package private protected public return short static super switch this throw throws true false try void while var"),
51
+ },
52
+ c: { line: ["//"], block: [["/*", "*/"]], strings: ['"', "'"], keywords: c },
53
+ };
54
+ const aliases = {
55
+ javascript: "js", jsx: "js", ts: "js", tsx: "js", typescript: "js", mjs: "js", cjs: "js",
56
+ py: "python",
57
+ sh: "bash", shell: "bash", zsh: "bash", console: "bash",
58
+ yml: "yaml",
59
+ xml: "html", svg: "html",
60
+ golang: "go", rs: "rust", kotlin: "java", cpp: "c", "c++": "c", h: "c", csharp: "java", cs: "java",
61
+ postgres: "sql", postgresql: "sql", mysql: "sql", sqlite: "sql",
62
+ };
63
+ /** Whether `language` has a grammar here. */
64
+ export function canHighlight(language) {
65
+ const l = language.toLowerCase();
66
+ return Boolean(grammars[aliases[l] ?? l]);
67
+ }
68
+ /** Long payloads stay plain: highlighting them would cost more than it helps. */
69
+ const MAX = 50_000;
70
+ /** The text split into typed tokens; their texts joined give back the input exactly. */
71
+ export function tokenize(text, language) {
72
+ const l = language.toLowerCase();
73
+ const g = grammars[aliases[l] ?? l];
74
+ if (!g || text.length > MAX)
75
+ return [{ type: "plain", text }];
76
+ const out = [];
77
+ const push = (type, t) => {
78
+ if (!t)
79
+ return;
80
+ const last = out[out.length - 1];
81
+ if (last && last.type === type)
82
+ last.text += t;
83
+ else
84
+ out.push({ type, text: t });
85
+ };
86
+ let i = 0;
87
+ let lineStart = true;
88
+ let inTag = false;
89
+ while (i < text.length) {
90
+ const rest = text.slice(i);
91
+ const ch = text[i];
92
+ // Comments.
93
+ const line = g.line?.find((m) => rest.startsWith(m) && !(g.variables && m === "#" && i > 0 && /\S/.test(text[i - 1])));
94
+ if (line && !inTag) {
95
+ const end = text.indexOf("\n", i);
96
+ const stop = end < 0 ? text.length : end;
97
+ push("comment", text.slice(i, stop));
98
+ i = stop;
99
+ continue;
100
+ }
101
+ const block = g.block?.find(([open]) => rest.startsWith(open));
102
+ if (block && !inTag) {
103
+ const end = text.indexOf(block[1], i + block[0].length);
104
+ const stop = end < 0 ? text.length : end + block[1].length;
105
+ push("comment", text.slice(i, stop));
106
+ i = stop;
107
+ continue;
108
+ }
109
+ // Markup: tag names, attributes, then attribute values as strings.
110
+ if (g.markup) {
111
+ const open = /^<\/?([A-Za-z][\w:.-]*)/.exec(rest);
112
+ if (!inTag && open) {
113
+ push("operator", rest.startsWith("</") ? "</" : "<");
114
+ push("tag", open[1]);
115
+ i += open[0].length;
116
+ inTag = true;
117
+ continue;
118
+ }
119
+ if (inTag) {
120
+ const close = /^\/?>/.exec(rest);
121
+ if (close) {
122
+ push("operator", close[0]);
123
+ i += close[0].length;
124
+ inTag = false;
125
+ continue;
126
+ }
127
+ const attr = /^[A-Za-z_:][\w:.-]*/.exec(rest);
128
+ if (attr) {
129
+ push("property", attr[0]);
130
+ i += attr[0].length;
131
+ continue;
132
+ }
133
+ }
134
+ else {
135
+ // Text between tags.
136
+ const next = text.indexOf("<", i + 1);
137
+ const stop = next < 0 ? text.length : next;
138
+ push("plain", text.slice(i, stop));
139
+ i = stop;
140
+ continue;
141
+ }
142
+ }
143
+ // Strings (with backslash escapes), and JSON's property names.
144
+ const quote = (!g.markup || inTag) && g.strings.find((q) => rest.startsWith(q));
145
+ if (quote) {
146
+ let j = i + quote.length;
147
+ while (j < text.length && !text.startsWith(quote, j)) {
148
+ if (text[j] === "\\")
149
+ j++;
150
+ else if (quote.length === 1 && quote !== "`" && text[j] === "\n")
151
+ break;
152
+ j++;
153
+ }
154
+ const stop = Math.min(text.length, j + (text.startsWith(quote, j) ? quote.length : 0));
155
+ const s = text.slice(i, stop);
156
+ push(g.properties === "json" && /^\s*:/.test(text.slice(stop)) ? "property" : "string", s);
157
+ i = stop;
158
+ lineStart = false;
159
+ continue;
160
+ }
161
+ // YAML keys at the start of a line (after indentation or "- ").
162
+ if (g.properties === "yaml" && lineStart) {
163
+ const key = /^([ \t]*(?:- )?)([\w.-][\w .-]*?)(\s*:)(?=\s|$)/.exec(rest);
164
+ if (key) {
165
+ push("plain", key[1]);
166
+ push("property", key[2]);
167
+ push("operator", key[3]);
168
+ i += key[0].length;
169
+ lineStart = false;
170
+ continue;
171
+ }
172
+ }
173
+ // Shell variables.
174
+ if (g.variables && ch === "$") {
175
+ const v = /^\$(\{[^}\n]*\}|[A-Za-z_]\w*|[0-9@#?*!$-])/.exec(rest);
176
+ if (v) {
177
+ push("variable", v[0]);
178
+ i += v[0].length;
179
+ lineStart = false;
180
+ continue;
181
+ }
182
+ }
183
+ // Numbers.
184
+ const num = /^(?:0x[\da-fA-F_]+|\d[\d_]*(?:\.\d+)?(?:[eE][+-]?\d+)?)/.exec(rest);
185
+ if (num && (i === 0 || !/[\w$]/.test(text[i - 1]))) {
186
+ push("number", num[0]);
187
+ i += num[0].length;
188
+ lineStart = false;
189
+ continue;
190
+ }
191
+ // Words: keywords, calls, types.
192
+ const word = /^[A-Za-z_$][\w$]*/.exec(rest);
193
+ if (word) {
194
+ const w = word[0];
195
+ const after = text.slice(i + w.length);
196
+ const type = (g.caseless ? g.keywords.has(w.toLowerCase()) : g.keywords.has(w))
197
+ ? "keyword"
198
+ : /^\s*\(/.test(after) && !g.properties
199
+ ? "function"
200
+ : /^[A-Z][a-z]/.test(w) && !g.caseless && !g.properties && grammars.css !== g
201
+ ? "type"
202
+ : "plain";
203
+ push(type, w);
204
+ i += w.length;
205
+ lineStart = false;
206
+ continue;
207
+ }
208
+ if (ch === "\n") {
209
+ push("plain", ch);
210
+ lineStart = true;
211
+ i++;
212
+ continue;
213
+ }
214
+ if (/\s/.test(ch)) {
215
+ push("plain", ch);
216
+ i++;
217
+ continue;
218
+ }
219
+ push(/[=+\-*/%<>!&|^~?:]/.test(ch) ? "operator" : "plain", ch);
220
+ lineStart = false;
221
+ i++;
222
+ }
223
+ return out;
224
+ }
@@ -5,7 +5,22 @@
5
5
  * map an SDK's parts onto them.
6
6
  */
7
7
  export type RunStatus = "idle" | "queued" | "running" | "waiting" | "blocked" | "succeeded" | "failed" | "cancelled" | "skipped";
8
+ /** Status words; override them under `status` in LocaleProvider's `strings`. */
9
+ export declare const statusStrings: {
10
+ idle: string;
11
+ queued: string;
12
+ running: string;
13
+ waiting: string;
14
+ blocked: string;
15
+ succeeded: string;
16
+ failed: string;
17
+ cancelled: string;
18
+ skipped: string;
19
+ };
20
+ /** English status labels. Kept for compatibility; components use `useStatusLabel()` so they follow the locale. */
8
21
  export declare const statusLabel: Record<RunStatus, string>;
22
+ /** A status's label in the current locale: `const label = useStatusLabel(); label("running")`. */
23
+ export declare function useStatusLabel(): (status: RunStatus) => string;
9
24
  /** The Badge variant that carries each status. */
10
25
  export declare const statusTone: {
11
26
  readonly idle: "outline";
@@ -1,4 +1,6 @@
1
- export const statusLabel = {
1
+ import { useStrings } from "../blocks/locale.js";
2
+ /** Status words; override them under `status` in LocaleProvider's `strings`. */
3
+ export const statusStrings = {
2
4
  idle: "Not started",
3
5
  queued: "Queued",
4
6
  running: "Running",
@@ -9,6 +11,13 @@ export const statusLabel = {
9
11
  cancelled: "Cancelled",
10
12
  skipped: "Skipped",
11
13
  };
14
+ /** English status labels. Kept for compatibility; components use `useStatusLabel()` so they follow the locale. */
15
+ export const statusLabel = statusStrings;
16
+ /** A status's label in the current locale: `const label = useStatusLabel(); label("running")`. */
17
+ export function useStatusLabel() {
18
+ const t = useStrings("status", statusStrings);
19
+ return t;
20
+ }
12
21
  /** The Badge variant that carries each status. */
13
22
  export const statusTone = {
14
23
  idle: "outline",
@@ -0,0 +1,39 @@
1
+ import * as React from "react";
2
+ /** What to render, in order: an item by its index, or a spacer standing in for the items between. */
3
+ export type VirtualRow = {
4
+ index: number;
5
+ } | {
6
+ spacer: number;
7
+ key: string;
8
+ };
9
+ export type VirtualList = {
10
+ /** The items to render and the spacers between them, in order. */
11
+ rows: VirtualRow[];
12
+ /** The ref for every rendered item, which also carries `data-virtual-key={key}`, so its real height is measured and kept. */
13
+ measure: (el: HTMLElement | null) => void | (() => void);
14
+ };
15
+ /**
16
+ * Render only the items of a long list that are in or near view, for Thread's and SessionList's `virtualize`.
17
+ * Items keep their measured heights by key, so a list that grows or reorders keeps its place; unmeasured items count
18
+ * as `estimate`. Spacers stand in for the rest. `pin` (an item's key) keeps that item rendered (the one holding
19
+ * focus), on its own if it's far from view, so focus is never lost to an item scrolling away. Until the scroller is
20
+ * measured (on the server, on the first render in the browser, and while it's hidden) the first or last `initial`
21
+ * items render, by `anchor`, so server and browser agree.
22
+ *
23
+ * Give each rendered item `ref={list.measure}` and `data-virtual-key={key}`.
24
+ */
25
+ export declare function useVirtualList({ enabled, count, keyOf, scroller, estimate, overscan, initial, anchor, gap, pin, }: {
26
+ enabled: boolean;
27
+ count: number;
28
+ keyOf: (index: number) => React.Key;
29
+ scroller: React.RefObject<HTMLElement | null>;
30
+ estimate?: number;
31
+ overscan?: number;
32
+ initial?: number;
33
+ /** Which end to show before the scroller is measured: "end" for a thread (newest last), "start" for a list. Defaults to "end". */
34
+ anchor?: "start" | "end";
35
+ /** The gap between items, in pixels, read once the scroller is measured (e.g. from the list's computed `row-gap`). */
36
+ gap?: () => number;
37
+ /** The key of an item to keep rendered, such as the one holding focus. */
38
+ pin?: string | null;
39
+ }): VirtualList;
@@ -0,0 +1,124 @@
1
+ import * as React from "react";
2
+ const keyAttr = "data-virtual-key";
3
+ /**
4
+ * Render only the items of a long list that are in or near view, for Thread's and SessionList's `virtualize`.
5
+ * Items keep their measured heights by key, so a list that grows or reorders keeps its place; unmeasured items count
6
+ * as `estimate`. Spacers stand in for the rest. `pin` (an item's key) keeps that item rendered (the one holding
7
+ * focus), on its own if it's far from view, so focus is never lost to an item scrolling away. Until the scroller is
8
+ * measured (on the server, on the first render in the browser, and while it's hidden) the first or last `initial`
9
+ * items render, by `anchor`, so server and browser agree.
10
+ *
11
+ * Give each rendered item `ref={list.measure}` and `data-virtual-key={key}`.
12
+ */
13
+ export function useVirtualList({ enabled, count, keyOf, scroller, estimate = 96, overscan = 800, initial = 20, anchor = "end", gap = () => 0, pin, }) {
14
+ const [heights, setHeights] = React.useState(() => new Map());
15
+ const [viewport, setViewport] = React.useState(null);
16
+ const gapOf = React.useRef(gap);
17
+ React.useLayoutEffect(() => {
18
+ gapOf.current = gap;
19
+ });
20
+ // One observer for the rendered items, made when the first one mounts; a changed height re-renders.
21
+ const observer = React.useRef(null);
22
+ React.useEffect(() => () => {
23
+ observer.current?.disconnect();
24
+ observer.current = null;
25
+ }, []);
26
+ const measure = React.useCallback((el) => {
27
+ if (!el)
28
+ return;
29
+ if (typeof ResizeObserver !== "undefined")
30
+ observer.current ??= new ResizeObserver((entries) => {
31
+ const found = entries.flatMap((e) => {
32
+ const key = e.target.getAttribute(keyAttr);
33
+ const h = e.target.offsetHeight;
34
+ return key !== null && h > 0 ? [[key, h]] : [];
35
+ });
36
+ if (found.length)
37
+ setHeights((prev) => (found.every(([k, h]) => prev.get(k) === h) ? prev : new Map([...prev, ...found])));
38
+ });
39
+ const ro = observer.current;
40
+ ro?.observe(el);
41
+ const key = el.getAttribute(keyAttr);
42
+ const h = el.offsetHeight;
43
+ if (key !== null && h > 0)
44
+ setHeights((prev) => (prev.get(key) === h ? prev : new Map(prev).set(key, h)));
45
+ // An item that unmounts stops being observed, so it can be collected.
46
+ return () => ro?.unobserve(el);
47
+ }, []);
48
+ React.useLayoutEffect(() => {
49
+ const el = scroller.current;
50
+ if (!enabled || !el)
51
+ return;
52
+ const pending = { frame: 0 };
53
+ const read = () => {
54
+ pending.frame = 0;
55
+ const g = gapOf.current();
56
+ setViewport((v) => (v && v.top === el.scrollTop && v.height === el.clientHeight && v.gap === g ? v : { top: el.scrollTop, height: el.clientHeight, gap: g }));
57
+ };
58
+ const later = () => {
59
+ if (!pending.frame)
60
+ pending.frame = requestAnimationFrame(read);
61
+ };
62
+ read();
63
+ el.addEventListener("scroll", later, { passive: true });
64
+ const ro = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(later);
65
+ ro?.observe(el);
66
+ return () => {
67
+ el.removeEventListener("scroll", later);
68
+ ro?.disconnect();
69
+ cancelAnimationFrame(pending.frame);
70
+ };
71
+ }, [enabled, scroller]);
72
+ if (!enabled)
73
+ return { rows: Array.from({ length: count }, (_, index) => ({ index })), measure };
74
+ const g = viewport?.gap ?? 0;
75
+ const size = (i) => heights.get(String(keyOf(i))) ?? estimate;
76
+ const tops = [];
77
+ let y = 0;
78
+ for (let i = 0; i < count; i++) {
79
+ tops.push(y);
80
+ y += size(i) + g;
81
+ }
82
+ const total = Math.max(0, y - g);
83
+ let start;
84
+ let end;
85
+ if (!viewport || viewport.height === 0) {
86
+ start = anchor === "end" ? Math.max(0, count - initial) : 0;
87
+ end = anchor === "end" ? count : Math.min(count, initial);
88
+ }
89
+ else {
90
+ const lo = viewport.top - overscan;
91
+ const hi = viewport.top + viewport.height + overscan;
92
+ start = 0;
93
+ while (start < count - 1 && tops[start] + size(start) < lo)
94
+ start++;
95
+ end = start;
96
+ while (end < count && tops[end] <= hi)
97
+ end++;
98
+ end = Math.max(end, Math.min(count, start + 1));
99
+ }
100
+ // The pinned item and its neighbours (so Tab and Shift+Tab reach the next and previous item) render with the window,
101
+ // or on their own between spacers when they're far from it.
102
+ let ranges = [[start, end]];
103
+ const p = pin == null ? -1 : Array.from({ length: count }, (_, i) => i).find((i) => String(keyOf(i)) === pin) ?? -1;
104
+ if (p >= 0) {
105
+ const island = [Math.max(0, p - 1), Math.min(count, p + 2)];
106
+ ranges = island[1] < start ? [island, [start, end]] : island[0] > end ? [[start, end], island] : [[Math.min(start, island[0]), Math.max(end, island[1])]];
107
+ }
108
+ // A spacer stands where its items were; the gaps the layout puts beside it account for theirs.
109
+ const rows = [];
110
+ let prevEnd = 0;
111
+ for (const [a, b] of ranges) {
112
+ if (a > 0) {
113
+ const height = prevEnd === 0 ? tops[a] - g : tops[a] - (tops[prevEnd - 1] + size(prevEnd - 1)) - 2 * g;
114
+ if (a > prevEnd)
115
+ rows.push({ spacer: Math.max(0, height), key: `spacer-${prevEnd}` });
116
+ }
117
+ for (let i = a; i < b; i++)
118
+ rows.push({ index: i });
119
+ prevEnd = b;
120
+ }
121
+ if (prevEnd < count)
122
+ rows.push({ spacer: Math.max(0, total - tops[prevEnd]), key: `spacer-${prevEnd}` });
123
+ return { rows, measure };
124
+ }
package/dist/styles.css CHANGED
@@ -15,7 +15,7 @@
15
15
 
16
16
  /*
17
17
  * Candor's default colours. GENERATED by scripts/theme.mjs from its data,
18
- * after checking every text/ground pairing for contrast (40 pairings in
18
+ * after checking every text/ground pairing for contrast (44 pairings in
19
19
  * each of 3 themes); edit there, not here.
20
20
  * Light is the default; dark and high contrast follow data-theme, and the
21
21
  * system setting when no data-theme is set.
@@ -237,6 +237,10 @@
237
237
  * To restyle, override the roles after this file: colours by their shadcn names
238
238
  * (--primary, --background…), everything else by its --cd-* name. A brand theme
239
239
  * is just such a file.
240
+ *
241
+ * Density: data-density="compact" on <html> (or any container) tightens type for
242
+ * dense apps: 14px conversation text and 13px body and label text, with a narrower
243
+ * thread and sidebar.
240
244
  */
241
245
  @custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
242
246
 
@@ -256,6 +260,8 @@
256
260
  --cd-text-3xl: 1.75rem;
257
261
  --cd-text-4xl: 2rem;
258
262
  --cd-text-5xl: 2.5rem;
263
+ /* Conversation text: messages and the composer. */
264
+ --cd-text-prose: var(--cd-text-md);
259
265
  --cd-tracking-body: -0.011em;
260
266
  --cd-weight-heading: 620;
261
267
 
@@ -278,6 +284,10 @@
278
284
  --cd-ease-inout: cubic-bezier(0.4, 0, 0.2, 1);
279
285
  --cd-ease-spring: linear(0, 0.4 15%, 0.7 30%, 1 50%, 0.95 65%, 1.02 80%, 1 100%);
280
286
 
287
+ /* The message column's width, and the app sidebar's. (Not --sidebar-width: shadcn's SidebarProvider sets that.) */
288
+ --cd-thread-width: 48rem;
289
+ --cd-sidebar-width: 16rem;
290
+
281
291
  --radius: var(--cd-radius-lg);
282
292
 
283
293
  --sidebar: var(--chrome);
@@ -298,6 +308,14 @@
298
308
  [data-theme="dark"] {
299
309
  --cd-shadow: oklch(0 0 0 / 0.35);
300
310
  }
311
+
312
+ /* The compact preset. Prose is pinned, since by default it follows --cd-text-md, which this shrinks. */
313
+ [data-density="compact"] {
314
+ --cd-text-prose: 0.875rem;
315
+ --cd-text-md: 0.8125rem;
316
+ --cd-thread-width: 44rem;
317
+ --cd-sidebar-width: 14rem;
318
+ }
301
319
  @media (prefers-color-scheme: dark) {
302
320
  :root:not([data-theme]) {
303
321
  --cd-shadow: oklch(0 0 0 / 0.35);
@@ -317,6 +335,8 @@
317
335
  --text-sm--line-height: 1.25rem;
318
336
  --text-base: var(--cd-text-md);
319
337
  --text-base--line-height: 1.375rem;
338
+ --text-prose: var(--cd-text-prose);
339
+ --text-prose--line-height: 1.375rem;
320
340
  --text-lg: var(--cd-text-lg);
321
341
  --text-lg--line-height: 1.5rem;
322
342
  --text-xl: var(--cd-text-xl);
@@ -426,6 +446,25 @@
426
446
  to { visibility: hidden; }
427
447
  }
428
448
 
449
+ /*
450
+ * Syntax colours for Payload's tokens: on under data-syntax="highlight" (on <html> for the whole app, or a Payload's
451
+ * own `highlight`), off again under data-syntax="off". Only text roles that pass contrast on the sunken well are used.
452
+ */
453
+ [data-syntax="highlight"] [data-token="keyword"] { color: var(--accent-text); }
454
+ [data-syntax="highlight"] [data-token="function"] { color: var(--accent-text); font-weight: 500; }
455
+ [data-syntax="highlight"] [data-token="string"] { color: var(--success-text); }
456
+ [data-syntax="highlight"] [data-token="comment"] { color: var(--muted-foreground); font-style: italic; }
457
+ [data-syntax="highlight"] :is([data-token="number"], [data-token="variable"]) { color: var(--attention-text); }
458
+ [data-syntax="highlight"] :is([data-token="property"], [data-token="tag"], [data-token="type"]) { color: var(--warning-text); }
459
+ [data-syntax="off"] [data-token] { color: inherit; font-style: inherit; font-weight: inherit; }
460
+ /* A Payload's own `highlight` wins over an "off" around it. */
461
+ [data-slot="payload"][data-syntax="highlight"] [data-token="keyword"] { color: var(--accent-text); }
462
+ [data-slot="payload"][data-syntax="highlight"] [data-token="function"] { color: var(--accent-text); font-weight: 500; }
463
+ [data-slot="payload"][data-syntax="highlight"] [data-token="string"] { color: var(--success-text); }
464
+ [data-slot="payload"][data-syntax="highlight"] [data-token="comment"] { color: var(--muted-foreground); font-style: italic; }
465
+ [data-slot="payload"][data-syntax="highlight"] :is([data-token="number"], [data-token="variable"]) { color: var(--attention-text); }
466
+ [data-slot="payload"][data-syntax="highlight"] :is([data-token="property"], [data-token="tag"], [data-token="type"]) { color: var(--warning-text); }
467
+
429
468
  @media (prefers-reduced-motion: reduce) {
430
469
  *, *::before, *::after {
431
470
  animation-duration: 0.001ms !important;
@@ -2,7 +2,7 @@ import * as React from "react";
2
2
  import { type VariantProps } from "class-variance-authority";
3
3
  import { type RenderProp } from "../lib/render-prop.js";
4
4
  declare const buttonVariants: (props?: ({
5
- variant?: "outline" | "secondary" | "destructive" | "link" | "default" | "ghost" | null | undefined;
5
+ variant?: "link" | "outline" | "secondary" | "destructive" | "default" | "ghost" | null | undefined;
6
6
  size?: "default" | "xs" | "sm" | "lg" | "icon" | "icon-xs" | "icon-sm" | "icon-lg" | null | undefined;
7
7
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
8
8
  declare function Button(allProps: React.ComponentProps<"button"> & VariantProps<typeof buttonVariants> & {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@candor-ui/react",
3
- "version": "1.0.1",
3
+ "version": "1.3.0",
4
4
  "description": "Candor UI's free tier as an npm package: React components for agents that do real work, on shadcn/ui and Tailwind 4.",
5
5
  "license": "MIT",
6
6
  "author": "Methodic LLC",