agent-canvas 0.1.44 → 0.2.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 (115) hide show
  1. package/README.md +14 -0
  2. package/bin/lib/commands/daemon.ts +3 -6
  3. package/bin/lib/commands/push.ts +1 -1
  4. package/bin/lib/daemon-lifecycle.ts +65 -72
  5. package/bin/lib/helpers.test.ts +1 -0
  6. package/bin/lib/helpers.ts +30 -74
  7. package/daemon/build.ts +37 -16
  8. package/daemon/client/App.tsx +27 -61
  9. package/daemon/client/CompareView.tsx +4 -4
  10. package/daemon/client/FileBrowser.tsx +4 -4
  11. package/daemon/client/FileViewer.tsx +4 -4
  12. package/daemon/client/OverviewView.tsx +2 -2
  13. package/daemon/client/RevisionSelect.tsx +1 -1
  14. package/daemon/client/canvas-runtime.ts +1 -0
  15. package/daemon/client/clientApi.ts +18 -0
  16. package/daemon/client/components/index.ts +3 -16
  17. package/daemon/client/styles.css +4 -457
  18. package/daemon/dist/annotations-runtime.js +1 -0
  19. package/daemon/dist/client.css +1 -1
  20. package/daemon/dist/client.js +76 -76
  21. package/daemon/dist/components.js +59 -59
  22. package/daemon/dist/index.html +1 -0
  23. package/daemon/dist/preact-compat.js +1 -1
  24. package/daemon/dist/runtime.js +1 -1
  25. package/daemon/src/components.ts +13 -0
  26. package/daemon/src/handlers/api.ts +13 -9
  27. package/daemon/src/handlers/files.ts +2 -3
  28. package/daemon/src/handlers/static.ts +1 -1
  29. package/daemon/src/handlers/uploads.ts +2 -3
  30. package/daemon/src/paths.ts +10 -24
  31. package/daemon/src/remote-feedback.test.ts +1 -1
  32. package/daemon/src/remote-feedback.ts +1 -1
  33. package/daemon/src/secrets-api.test.ts +2 -2
  34. package/daemon/src/server.ts +11 -10
  35. package/daemon/src/share-e2e.test.ts +1 -1
  36. package/daemon/src/share.test.ts +1 -1
  37. package/daemon/src/share.ts +1 -1
  38. package/daemon/tsconfig.json +1 -3
  39. package/package.json +11 -4
  40. package/.github/dark.png +0 -0
  41. package/.github/light.png +0 -0
  42. package/.github/workflows/ci.yml +0 -48
  43. package/.github/workflows/deploy-worker-prod.yml +0 -19
  44. package/.github/workflows/deploy-worker-stage.yml +0 -21
  45. package/.github/workflows/deploy-worker.yml +0 -72
  46. package/.github/workflows/release.yml +0 -36
  47. package/CLAUDE.md +0 -86
  48. package/bun.lock +0 -377
  49. package/daemon/client/AnnotationEditor.tsx +0 -315
  50. package/daemon/client/AnnotationProvider.tsx +0 -195
  51. package/daemon/client/AnnotationSidebar.tsx +0 -627
  52. package/daemon/client/FileIcon.tsx +0 -76
  53. package/daemon/client/PlanRenderer.tsx +0 -614
  54. package/daemon/client/Popover.tsx +0 -350
  55. package/daemon/client/RenderErrorContext.ts +0 -11
  56. package/daemon/client/ResponsePreview.tsx +0 -186
  57. package/daemon/client/SessionContext.tsx +0 -3
  58. package/daemon/client/annotationContext.ts +0 -114
  59. package/daemon/client/annotationDom.ts +0 -104
  60. package/daemon/client/annotationDraft.test.ts +0 -188
  61. package/daemon/client/annotationDraft.ts +0 -130
  62. package/daemon/client/compat/preact-all.ts +0 -4
  63. package/daemon/client/components/Callout.tsx +0 -26
  64. package/daemon/client/components/Checklist.tsx +0 -37
  65. package/daemon/client/components/Choice.tsx +0 -176
  66. package/daemon/client/components/CodeBlock.tsx +0 -35
  67. package/daemon/client/components/Diff.tsx +0 -65
  68. package/daemon/client/components/FilePreview.tsx +0 -88
  69. package/daemon/client/components/Image.tsx +0 -45
  70. package/daemon/client/components/Markdown.tsx +0 -44
  71. package/daemon/client/components/Mermaid.tsx +0 -565
  72. package/daemon/client/components/Note.tsx +0 -14
  73. package/daemon/client/components/Priority.tsx +0 -19
  74. package/daemon/client/components/ResponseNote.tsx +0 -19
  75. package/daemon/client/components/Section.tsx +0 -33
  76. package/daemon/client/components/Table.tsx +0 -35
  77. package/daemon/client/components/Task.tsx +0 -76
  78. package/daemon/client/components/UserInput.tsx +0 -174
  79. package/daemon/client/components/renderMarkdownChildren.tsx +0 -97
  80. package/daemon/client/generateMarkdown.ts +0 -273
  81. package/daemon/client/highlightRange.ts +0 -224
  82. package/daemon/client/popoverPosition.ts +0 -31
  83. package/daemon/client/runtime.ts +0 -123
  84. package/daemon/client/theme.css +0 -66
  85. package/daemon/client/useTextAnnotation.tsx +0 -188
  86. package/daemon/client/utils.ts +0 -13
  87. package/daemon/src/compiler.test.ts +0 -57
  88. package/daemon/src/compiler.ts +0 -360
  89. package/daemon/src/handlers/utils.ts +0 -14
  90. package/daemon/src/router.ts +0 -16
  91. package/daemon/src/session-secrets.test.ts +0 -83
  92. package/daemon/src/session.ts +0 -612
  93. package/daemon/src/watcher.ts +0 -71
  94. package/daemon/src/websocket.test.ts +0 -99
  95. package/daemon/src/websocket.ts +0 -211
  96. package/tsconfig.json +0 -14
  97. package/workers/canvas-share/README.md +0 -159
  98. package/workers/canvas-share/oblaka.ts +0 -56
  99. package/workers/canvas-share/package.json +0 -25
  100. package/workers/canvas-share/public/assets/.gitkeep +0 -0
  101. package/workers/canvas-share/public/assets/client.css +0 -2
  102. package/workers/canvas-share/public/assets/client.js +0 -91
  103. package/workers/canvas-share/public/assets/components.js +0 -65
  104. package/workers/canvas-share/public/assets/jsx-runtime.js +0 -3
  105. package/workers/canvas-share/public/assets/preact-compat.js +0 -1
  106. package/workers/canvas-share/public/assets/runtime.js +0 -1
  107. package/workers/canvas-share/public/index.html +0 -385
  108. package/workers/canvas-share/public/screenshot-dark.png +0 -0
  109. package/workers/canvas-share/public/screenshot-light.png +0 -0
  110. package/workers/canvas-share/src/index.test.ts +0 -209
  111. package/workers/canvas-share/src/index.ts +0 -518
  112. package/workers/canvas-share/src/types.ts +0 -119
  113. package/workers/canvas-share/src/util.ts +0 -156
  114. package/workers/canvas-share/src/validation.ts +0 -160
  115. package/workers/canvas-share/tsconfig.json +0 -16
@@ -1,104 +0,0 @@
1
- import type { Annotation } from "./AnnotationProvider";
2
- import type { ActiveView } from "./App";
3
-
4
- /** All blocks that can be annotated via block-level comments */
5
- const ANNOTATABLE_SELECTOR = "[data-md='item'], [data-md='section'], [data-md='table'] tbody tr, [data-md='callout'], [data-md='note'], [data-md='checklist-item'], [data-md='image']";
6
-
7
- /** Extract snippet identifier for a block element (mirrors PlanRenderer's getBlockSnippet) */
8
- function getBlockSnippet(block: HTMLElement): string | null {
9
- const md = block.getAttribute("data-md");
10
- if (md === "item") {
11
- const label = block.getAttribute("data-md-label");
12
- return label ? `[Item] ${label}` : null;
13
- }
14
- if (md === "section") {
15
- const title = block.getAttribute("data-md-title");
16
- return title ? `[Section] ${title}` : null;
17
- }
18
- if (md === "callout") {
19
- const type = block.getAttribute("data-md-type") || "info";
20
- const text = block.textContent?.trim().slice(0, 60) || "Callout";
21
- return `[Callout:${type}] ${text}`;
22
- }
23
- if (md === "note") {
24
- const text = block.textContent?.trim().slice(0, 60) || "Note";
25
- return `[Note] ${text}`;
26
- }
27
- if (block.tagName === "TR") {
28
- const cells = Array.from(block.querySelectorAll("td")).map((td) => td.textContent?.trim()).filter(Boolean);
29
- return cells.length ? `[Row] ${cells.join(" | ")}` : null;
30
- }
31
- if (md === "checklist-item") {
32
- const label = block.getAttribute("data-md-label");
33
- return label ? `[Checklist] ${label}` : null;
34
- }
35
- if (md === "choice-option" || md === "multichoice-option") {
36
- const label = block.getAttribute("data-md-label");
37
- return label ? `[Option] ${label}` : null;
38
- }
39
- if (md === "userinput") {
40
- const label = block.getAttribute("data-md-label");
41
- return label ? `[Input] ${label}` : null;
42
- }
43
- if (md === "rangeinput") {
44
- const label = block.getAttribute("data-md-label");
45
- return label ? `[Range] ${label}` : null;
46
- }
47
- if (md === "image") {
48
- const src = block.getAttribute("data-md-src");
49
- return src ? `[Image] ${src}` : null;
50
- }
51
- return null;
52
- }
53
-
54
- /**
55
- * Find the DOM element for an annotation — either an inline mark (data-annotation-id)
56
- * or a block element matched by snippet.
57
- */
58
- export function findAnnotationElement(ann: Annotation): HTMLElement | null {
59
- // Try inline mark first
60
- const mark = document.querySelector(`[data-annotation-id="${ann.id}"]`) as HTMLElement | null;
61
- if (mark) return mark;
62
-
63
- // For block annotations (snippet starts with "["), find by matching snippet against blocks
64
- if (ann.snippet.startsWith("[")) {
65
- for (const el of document.querySelectorAll(ANNOTATABLE_SELECTOR)) {
66
- if (getBlockSnippet(el as HTMLElement) === ann.snippet) {
67
- return el as HTMLElement;
68
- }
69
- }
70
- }
71
-
72
- return null;
73
- }
74
-
75
- /** Flash/pulse the element briefly — blinks bg via CSS animation */
76
- function flashElement(el: HTMLElement) {
77
- el.classList.remove("ann-flash");
78
- void el.offsetWidth; // force reflow to restart
79
- el.classList.add("ann-flash");
80
- el.addEventListener("animationend", () => { el.classList.remove("ann-flash"); }, { once: true });
81
- }
82
-
83
- /**
84
- * Scroll the canvas/file view to an annotation element and flash it.
85
- * Handles file annotations by switching view first.
86
- */
87
- export function scrollToAnnotation(ann: Annotation, setActiveView: (view: ActiveView) => void) {
88
- if (ann.filePath) {
89
- setActiveView({ type: "file", path: ann.filePath });
90
- setTimeout(() => {
91
- const el = findAnnotationElement(ann);
92
- if (el) {
93
- el.scrollIntoView({ behavior: "smooth", block: "center" });
94
- flashElement(el);
95
- }
96
- }, 150);
97
- } else {
98
- const el = findAnnotationElement(ann);
99
- if (el) {
100
- el.scrollIntoView({ behavior: "smooth", block: "center" });
101
- flashElement(el);
102
- }
103
- }
104
- }
@@ -1,188 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { annotationDraftKey, carryUnsubmittedDraft, clearPersistedDraft } from "./annotationDraft";
3
-
4
- class MemoryStorage {
5
- private readonly values = new Map<string, string>();
6
-
7
- getItem(key: string): string | null {
8
- return this.values.get(key) ?? null;
9
- }
10
-
11
- setItem(key: string, value: string): void {
12
- this.values.set(key, value);
13
- }
14
-
15
- removeItem(key: string): void {
16
- this.values.delete(key);
17
- }
18
- }
19
-
20
- const sessionId = "session";
21
- const key = (revision: number) => annotationDraftKey(sessionId, revision);
22
- const nextKey = (revision: number) => annotationDraftKey(sessionId, revision, "next");
23
-
24
- describe("carryUnsubmittedDraft", () => {
25
- test("clears a submitted draft and its carry marker", () => {
26
- const storage = new MemoryStorage();
27
- storage.setItem(key(2), JSON.stringify({ generalNote: "Submitted" }));
28
- storage.setItem(`${key(2)}:draft-handled`, "1");
29
-
30
- clearPersistedDraft(storage, sessionId, 2);
31
-
32
- expect(storage.getItem(key(2))).toBeNull();
33
- expect(storage.getItem(`${key(2)}:draft-handled`)).toBeNull();
34
- });
35
-
36
- test("carries the latest unsent draft into a new revision", () => {
37
- const storage = new MemoryStorage();
38
- storage.setItem(key(2), JSON.stringify({
39
- annotations: [{ id: "annotation-1" }],
40
- generalNote: "Review in progress",
41
- responses: [["choice", { value: "keep" }]],
42
- feedbackEntries: [["old", { markdown: "stale generated content" }]],
43
- }));
44
- storage.setItem(key(3), JSON.stringify({
45
- annotations: [],
46
- generalNote: "",
47
- responses: [["new-choice", { value: null }]],
48
- feedbackEntries: [["new", { markdown: "generated content" }]],
49
- }));
50
-
51
- const sourceRevision = carryUnsubmittedDraft(storage, sessionId, 3, [
52
- { revision: 1, hasFeedback: true },
53
- { revision: 2, hasFeedback: false },
54
- { revision: 3, hasFeedback: false },
55
- ]);
56
-
57
- expect(sourceRevision).toBe(2);
58
- expect(JSON.parse(storage.getItem(key(3)) ?? "")).toEqual({
59
- annotations: [{ id: "annotation-1" }],
60
- generalNote: "Review in progress",
61
- responses: [["choice", { value: "keep" }]],
62
- feedbackEntries: [],
63
- });
64
- });
65
-
66
- test("does not cross a submitted revision", () => {
67
- const storage = new MemoryStorage();
68
- storage.setItem(key(1), JSON.stringify({
69
- annotations: [{ id: "stale-annotation" }],
70
- generalNote: "",
71
- responses: [],
72
- }));
73
-
74
- expect(carryUnsubmittedDraft(storage, sessionId, 3, [
75
- { revision: 1, hasFeedback: false },
76
- { revision: 2, hasFeedback: true },
77
- { revision: 3, hasFeedback: false },
78
- ])).toBeNull();
79
- expect(storage.getItem(key(3))).toBeNull();
80
- });
81
-
82
- test("carries a post-feedback draft into the next revision", () => {
83
- const storage = new MemoryStorage();
84
- storage.setItem(key(2), JSON.stringify({
85
- annotations: [{ id: "submitted-annotation" }],
86
- generalNote: "Submitted feedback",
87
- responses: [],
88
- }));
89
- storage.setItem(nextKey(2), JSON.stringify({
90
- annotations: [{ id: "next-annotation" }],
91
- generalNote: "",
92
- responses: [],
93
- feedbackEntries: [["generated", { markdown: "stale generated content" }]],
94
- }));
95
-
96
- expect(carryUnsubmittedDraft(storage, sessionId, 3, [
97
- { revision: 2, hasFeedback: true },
98
- { revision: 3, hasFeedback: false },
99
- ])).toBe(2);
100
- expect(JSON.parse(storage.getItem(key(3)) ?? "")).toEqual({
101
- annotations: [{ id: "next-annotation" }],
102
- generalNote: "",
103
- responses: [],
104
- feedbackEntries: [],
105
- });
106
- expect(storage.getItem(nextKey(2))).toBeNull();
107
- });
108
-
109
- test("clearing submitted feedback preserves its next-revision draft", () => {
110
- const storage = new MemoryStorage();
111
- storage.setItem(key(2), JSON.stringify({ annotations: [{ id: "submitted" }] }));
112
- storage.setItem(nextKey(2), JSON.stringify({ annotations: [{ id: "next" }] }));
113
-
114
- clearPersistedDraft(storage, sessionId, 2);
115
-
116
- expect(storage.getItem(key(2))).toBeNull();
117
- expect(storage.getItem(nextKey(2))).not.toBeNull();
118
- });
119
-
120
- test("does not overwrite meaningful target data", () => {
121
- const storage = new MemoryStorage();
122
- storage.setItem(key(2), JSON.stringify({
123
- annotations: [{ id: "old-annotation" }],
124
- generalNote: "",
125
- responses: [],
126
- }));
127
- const current = JSON.stringify({
128
- annotations: [{ id: "current-annotation" }],
129
- generalNote: "",
130
- responses: [],
131
- });
132
- storage.setItem(key(3), current);
133
-
134
- expect(carryUnsubmittedDraft(storage, sessionId, 3, [
135
- { revision: 2, hasFeedback: false },
136
- { revision: 3, hasFeedback: false },
137
- ])).toBeNull();
138
- expect(storage.getItem(key(3))).toBe(current);
139
- });
140
-
141
- test("skips empty intermediate revision state", () => {
142
- const storage = new MemoryStorage();
143
- storage.setItem(key(2), JSON.stringify({
144
- annotations: [],
145
- generalNote: "Still drafting",
146
- responses: [],
147
- }));
148
- storage.setItem(key(3), JSON.stringify({
149
- annotations: [],
150
- generalNote: "",
151
- responses: [["choice", { value: [] }]],
152
- feedbackEntries: [["generated", { markdown: "not user input" }]],
153
- }));
154
-
155
- expect(carryUnsubmittedDraft(storage, sessionId, 4, [
156
- { revision: 1, hasFeedback: true },
157
- { revision: 2, hasFeedback: false },
158
- { revision: 3, hasFeedback: false },
159
- { revision: 4, hasFeedback: false },
160
- ])).toBe(2);
161
- });
162
-
163
- test("does not resurrect a carried draft after it is cleared", () => {
164
- const storage = new MemoryStorage();
165
- storage.setItem(key(2), JSON.stringify({
166
- annotations: [{ id: "annotation-1" }],
167
- generalNote: "",
168
- responses: [],
169
- }));
170
- const revisions = [
171
- { revision: 1, hasFeedback: true },
172
- { revision: 2, hasFeedback: false },
173
- { revision: 3, hasFeedback: false },
174
- ];
175
-
176
- expect(carryUnsubmittedDraft(storage, sessionId, 3, revisions)).toBe(2);
177
- const cleared = JSON.stringify({ annotations: [], generalNote: "", responses: [] });
178
- storage.setItem(key(3), cleared);
179
-
180
- expect(carryUnsubmittedDraft(storage, sessionId, 3, revisions)).toBeNull();
181
- expect(storage.getItem(key(3))).toBe(cleared);
182
- expect(carryUnsubmittedDraft(storage, sessionId, 4, [
183
- ...revisions,
184
- { revision: 4, hasFeedback: false },
185
- ])).toBeNull();
186
- expect(storage.getItem(key(4))).toBeNull();
187
- });
188
- });
@@ -1,130 +0,0 @@
1
- interface RevisionState {
2
- revision: number;
3
- hasFeedback: boolean;
4
- }
5
-
6
- interface AnnotationStorage {
7
- getItem(key: string): string | null;
8
- setItem(key: string, value: string): void;
9
- removeItem(key: string): void;
10
- }
11
-
12
- interface RemovableAnnotationStorage {
13
- removeItem(key: string): void;
14
- }
15
-
16
- export type AnnotationDraftPhase = "current" | "next";
17
-
18
- export function annotationDraftKey(
19
- sessionId: string,
20
- revision: number,
21
- phase: AnnotationDraftPhase = "current",
22
- ): string {
23
- const base = `canvas:${sessionId}:rev:${revision}`;
24
- return phase === "next" ? `${base}:next-draft` : base;
25
- }
26
-
27
- function handledKey(sessionId: string, revision: number): string {
28
- return `${annotationDraftKey(sessionId, revision)}:draft-handled`;
29
- }
30
-
31
- export function clearPersistedDraft(
32
- storage: RemovableAnnotationStorage,
33
- sessionId: string,
34
- revision: number,
35
- ): void {
36
- try {
37
- storage.removeItem(annotationDraftKey(sessionId, revision));
38
- storage.removeItem(handledKey(sessionId, revision));
39
- } catch {}
40
- }
41
-
42
- function parseState(raw: string | null): object | null {
43
- if (!raw) return null;
44
- try {
45
- const value: unknown = JSON.parse(raw);
46
- return typeof value === "object" && value !== null && !Array.isArray(value)
47
- ? value
48
- : null;
49
- } catch {
50
- return null;
51
- }
52
- }
53
-
54
- function hasResponseValue(entry: unknown): boolean {
55
- if (!Array.isArray(entry) || entry.length < 2) return false;
56
- const response: unknown = entry[1];
57
- if (typeof response !== "object" || response === null || Array.isArray(response)) return false;
58
-
59
- const note = Reflect.get(response, "note");
60
- if (typeof note === "string" && note.trim().length > 0) return true;
61
-
62
- const value = Reflect.get(response, "value");
63
- if (typeof value === "string") return value.trim().length > 0;
64
- if (Array.isArray(value)) return value.length > 0;
65
- return value !== null && value !== undefined;
66
- }
67
-
68
- function hasDraftContent(state: object | null): boolean {
69
- if (!state) return false;
70
- const annotations = Reflect.get(state, "annotations");
71
- if (Array.isArray(annotations) && annotations.length > 0) return true;
72
-
73
- const generalNote = Reflect.get(state, "generalNote");
74
- if (typeof generalNote === "string" && generalNote.trim().length > 0) return true;
75
-
76
- const responses = Reflect.get(state, "responses");
77
- return Array.isArray(responses) && responses.some(hasResponseValue);
78
- }
79
-
80
- /** Carry an unsent draft forward without crossing submitted revisions. */
81
- export function carryUnsubmittedDraft(
82
- storage: AnnotationStorage,
83
- sessionId: string,
84
- targetRevision: number,
85
- revisions: RevisionState[],
86
- ): number | null {
87
- try {
88
- const targetKey = annotationDraftKey(sessionId, targetRevision);
89
- if (hasDraftContent(parseState(storage.getItem(targetKey)))) {
90
- storage.setItem(handledKey(sessionId, targetRevision), "1");
91
- return null;
92
- }
93
- if (storage.getItem(handledKey(sessionId, targetRevision)) !== null) return null;
94
-
95
- const previous = revisions
96
- .filter((revision) => revision.revision < targetRevision)
97
- .sort((left, right) => right.revision - left.revision);
98
-
99
- for (const revision of previous) {
100
- if (revision.hasFeedback) {
101
- const sourceKey = annotationDraftKey(sessionId, revision.revision, "next");
102
- const source = parseState(storage.getItem(sourceKey));
103
- if (!hasDraftContent(source)) return null;
104
-
105
- storage.setItem(targetKey, JSON.stringify({
106
- ...source,
107
- feedbackEntries: [],
108
- }));
109
- storage.setItem(handledKey(sessionId, targetRevision), "1");
110
- storage.removeItem(sourceKey);
111
- return revision.revision;
112
- }
113
-
114
- const source = parseState(storage.getItem(annotationDraftKey(sessionId, revision.revision)));
115
- if (!hasDraftContent(source)) {
116
- if (storage.getItem(handledKey(sessionId, revision.revision)) !== null) return null;
117
- continue;
118
- }
119
-
120
- storage.setItem(targetKey, JSON.stringify({
121
- ...source,
122
- feedbackEntries: [],
123
- }));
124
- storage.setItem(handledKey(sessionId, targetRevision), "1");
125
- return revision.revision;
126
- }
127
- } catch {}
128
-
129
- return null;
130
- }
@@ -1,4 +0,0 @@
1
- export * from "preact/compat";
2
- export { default } from "preact/compat";
3
- export { jsx, jsxs } from "preact/jsx-runtime";
4
- export { createRoot, hydrateRoot } from "preact/compat/client";
@@ -1,26 +0,0 @@
1
- import React from "react";
2
- import { renderMarkdownChildren } from "./renderMarkdownChildren";
3
-
4
- interface CalloutProps {
5
- type?: "info" | "warning" | "danger" | "tip";
6
- children?: React.ReactNode;
7
- }
8
-
9
- const config = {
10
- info: { bg: "bg-accent-blue-muted", icon: "i", iconColor: "text-accent-blue" },
11
- warning: { bg: "bg-accent-amber-muted", icon: "!", iconColor: "text-accent-amber" },
12
- danger: { bg: "bg-accent-red-muted", icon: "\u2715", iconColor: "text-accent-red" },
13
- tip: { bg: "bg-accent-green-muted", icon: "\u2713", iconColor: "text-accent-green" },
14
- };
15
-
16
- export function Callout({ type = "info", children }: CalloutProps) {
17
- const cfg = config[type];
18
- return (
19
- <div className={`${cfg.bg} relative rounded-lg px-5 py-4 mt-3 flex gap-3 items-start`} data-md="callout" data-md-type={type}>
20
- <span className={`${cfg.iconColor} opacity-70 text-base leading-relaxed flex-shrink-0 mt-px`}>
21
- {cfg.icon}
22
- </span>
23
- <div className="text-body text-text-secondary leading-relaxed">{renderMarkdownChildren(children)}</div>
24
- </div>
25
- );
26
- }
@@ -1,37 +0,0 @@
1
- import React from "react";
2
-
3
- interface ChecklistItem {
4
- label: string;
5
- checked: boolean;
6
- }
7
-
8
- interface ChecklistProps {
9
- items: ChecklistItem[];
10
- }
11
-
12
- export function Checklist({ items }: ChecklistProps) {
13
- return (
14
- <ul className="space-y-1 mt-3" data-md="checklist">
15
- {items.map((item, i) => (
16
- <li key={i} data-md="checklist-item" data-md-label={item.label} className="relative flex items-start gap-2.5 py-1">
17
- <span className={`w-4 h-4 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5 transition-all ${
18
- item.checked
19
- ? "bg-accent-green"
20
- : "border-[1.5px] border-border-hover"
21
- }`}>
22
- {item.checked && (
23
- <svg width="10" height="10" viewBox="0 0 10 10" fill="none">
24
- <path d="M2.5 5L4.5 7L7.5 3" style={{ stroke: "var(--color-text-inverse)" }} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
25
- </svg>
26
- )}
27
- </span>
28
- <span className={`text-body font-body ${
29
- item.checked ? "text-text-tertiary line-through decoration-border-hover" : "text-text-secondary"
30
- }`}>
31
- {item.label}
32
- </span>
33
- </li>
34
- ))}
35
- </ul>
36
- );
37
- }
@@ -1,176 +0,0 @@
1
- import React, { useEffect, useMemo, useRef, useState } from "react";
2
- import { useAnnotations } from "#canvas/runtime";
3
- import { ResponseNote } from "./ResponseNote";
4
-
5
- export type ChoiceOption = string | { value: string; label?: string };
6
-
7
- type NormalizedOption = { value: string; label: string };
8
-
9
- function normalizeOptions(options: ChoiceOption[]): NormalizedOption[] {
10
- return options.map((opt) =>
11
- typeof opt === "string"
12
- ? { value: opt, label: opt }
13
- : { value: opt.value, label: opt.label ?? opt.value },
14
- );
15
- }
16
-
17
- /** Radio — pick one option */
18
- interface ChoiceProps {
19
- id: string;
20
- label: string;
21
- options: ChoiceOption[];
22
- required?: boolean;
23
- }
24
-
25
- export function Choice({ id, label, options, required }: ChoiceProps) {
26
- const { responses, setResponse } = useAnnotations();
27
- const normalized = useMemo(() => normalizeOptions(options), [options]);
28
- const current = responses.get(id);
29
- const selected = current?.value as string | undefined;
30
- const note = current?.note || "";
31
- const [showNote, setShowNote] = useState(false);
32
- const wrapperRef = useRef<HTMLDivElement>(null);
33
-
34
- if (!responses.has(id)) {
35
- setResponse(id, { id, type: "radio", label, value: null, options: normalized.map((o) => o.value), required });
36
- }
37
-
38
- const handleSelect = (value: string) => {
39
- setResponse(id, { ...current!, value });
40
- };
41
-
42
- // Keyboard selection via custom event from PlanRenderer
43
- useEffect(() => {
44
- const el = wrapperRef.current;
45
- if (!el) return;
46
- const handler = (e: Event) => {
47
- const target = (e.target as HTMLElement).closest("[data-md-label]");
48
- const labelAttr = target?.getAttribute("data-md-label");
49
- const match = normalized.find((o) => o.label === labelAttr);
50
- if (match) handleSelect(match.value);
51
- };
52
- el.addEventListener("kb-select", handler);
53
- return () => el.removeEventListener("kb-select", handler);
54
- }, [normalized, handleSelect]);
55
-
56
- const showError = current?.required && !selected;
57
-
58
- return (
59
- <div ref={wrapperRef} data-md="choice" data-md-label={label} className="-mx-4 px-4 py-3 my-1 rounded-lg transition-colors duration-150 hover:bg-bg-input">
60
- <div className="flex items-baseline gap-2 mb-2">
61
- <span className="text-[13px] font-body font-medium text-text-primary">{label}</span>
62
- {required && <span className="text-[10px] text-accent-red font-body">*</span>}
63
- </div>
64
- <div className="space-y-1">
65
- {normalized.map(({ value, label: optLabel }) => (
66
- <label
67
- key={value}
68
- data-md="choice-option"
69
- data-md-label={optLabel}
70
- onClick={() => handleSelect(value)}
71
- className={`flex items-center gap-2.5 px-3 py-2 rounded-lg cursor-pointer transition-all duration-150 ${
72
- selected === value
73
- ? "bg-highlight-selected"
74
- : "hover:bg-bg-elevated"
75
- }`}
76
- >
77
- <span className={`w-4 h-4 rounded-full border-[1.5px] flex items-center justify-center flex-shrink-0 transition-all ${
78
- selected === value
79
- ? "border-accent-amber bg-accent-amber"
80
- : "border-border-strong"
81
- }`}>
82
- {selected === value && <span className="w-1.5 h-1.5 rounded-full bg-text-inverse" />}
83
- </span>
84
- <span className="text-[13px] font-body text-text-secondary">{optLabel}</span>
85
- </label>
86
- ))}
87
- </div>
88
- <ResponseNote show={showNote} note={note} onToggle={() => setShowNote(!showNote)} onChange={(n) => setResponse(id, { ...current!, note: n })} />
89
- {showError && <p className="text-[11px] text-accent-red font-body mt-1">Please select an option.</p>}
90
- </div>
91
- );
92
- }
93
-
94
- /** Checkbox — pick multiple options */
95
- interface MultiChoiceProps {
96
- id: string;
97
- label: string;
98
- options: ChoiceOption[];
99
- required?: boolean;
100
- }
101
-
102
- export function MultiChoice({ id, label, options, required }: MultiChoiceProps) {
103
- const { responses, setResponse } = useAnnotations();
104
- const normalized = useMemo(() => normalizeOptions(options), [options]);
105
- const current = responses.get(id);
106
- const selected: string[] = (current?.value as string[]) || [];
107
- const note = current?.note || "";
108
- const [showNote, setShowNote] = useState(false);
109
- const wrapperRef = useRef<HTMLDivElement>(null);
110
-
111
- if (!responses.has(id)) {
112
- setResponse(id, { id, type: "checkbox", label, value: [], options: normalized.map((o) => o.value), required });
113
- }
114
-
115
- const toggle = (value: string) => {
116
- const next = selected.includes(value) ? selected.filter((o) => o !== value) : [...selected, value];
117
- setResponse(id, { ...current!, value: next });
118
- };
119
-
120
- // Keyboard selection via custom event from PlanRenderer
121
- useEffect(() => {
122
- const el = wrapperRef.current;
123
- if (!el) return;
124
- const handler = (e: Event) => {
125
- const target = (e.target as HTMLElement).closest("[data-md-label]");
126
- const labelAttr = target?.getAttribute("data-md-label");
127
- const match = normalized.find((o) => o.label === labelAttr);
128
- if (match) toggle(match.value);
129
- };
130
- el.addEventListener("kb-select", handler);
131
- return () => el.removeEventListener("kb-select", handler);
132
- }, [normalized, toggle]);
133
-
134
- const showError = current?.required && selected.length === 0;
135
-
136
- return (
137
- <div ref={wrapperRef} data-md="multichoice" data-md-label={label} className="-mx-4 px-4 py-3 my-1 rounded-lg transition-colors duration-150 hover:bg-bg-input">
138
- <div className="flex items-baseline gap-2 mb-2">
139
- <span className="text-[13px] font-body font-medium text-text-primary">{label}</span>
140
- {required && <span className="text-[10px] text-accent-red font-body">*</span>}
141
- </div>
142
- <div className="space-y-1">
143
- {normalized.map(({ value, label: optLabel }) => {
144
- const checked = selected.includes(value);
145
- return (
146
- <label
147
- key={value}
148
- data-md="multichoice-option"
149
- data-md-label={optLabel}
150
- onClick={() => toggle(value)}
151
- className={`flex items-center gap-2.5 px-3 py-2 rounded-lg cursor-pointer transition-all duration-150 ${
152
- checked ? "bg-highlight-selected" : "hover:bg-bg-elevated"
153
- }`}
154
- >
155
- <span className={`w-4 h-4 rounded flex items-center justify-center flex-shrink-0 transition-all ${
156
- checked
157
- ? "bg-accent-amber"
158
- : "border-[1.5px] border-border-strong"
159
- }`}>
160
- {checked && (
161
- <svg width="10" height="10" viewBox="0 0 10 10" fill="none">
162
- <path d="M2.5 5L4.5 7L7.5 3" style={{ stroke: "var(--color-text-inverse)" }} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
163
- </svg>
164
- )}
165
- </span>
166
- <span className="text-[13px] font-body text-text-secondary">{optLabel}</span>
167
- </label>
168
- );
169
- })}
170
- </div>
171
- <ResponseNote show={showNote} note={note} onToggle={() => setShowNote(!showNote)} onChange={(n) => setResponse(id, { ...current!, note: n })} />
172
- {showError && <p className="text-[11px] text-accent-red font-body mt-1">Please select at least one option.</p>}
173
- </div>
174
- );
175
- }
176
-