pmx-canvas 0.7.1 → 0.7.3

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 (76) hide show
  1. package/.github/extensions/pmx-canvas/extension.mjs +79 -61
  2. package/CHANGELOG.md +29 -1
  3. package/Readme.md +48 -9
  4. package/dist/canvas/global.css +144 -45
  5. package/dist/canvas/index.js +90 -90
  6. package/dist/json-render/index.js +55 -55
  7. package/dist/types/client/canvas/ChoiceMenu.d.ts +13 -0
  8. package/dist/types/client/canvas/ExternalWriters.d.ts +3 -3
  9. package/dist/types/client/icons.d.ts +15 -0
  10. package/dist/types/client/nodes/ExtAppFrame.d.ts +1 -1
  11. package/dist/types/client/nodes/FileNode.d.ts +0 -12
  12. package/dist/types/client/nodes/McpAppNode.d.ts +2 -1
  13. package/dist/types/client/nodes/viewer-runtime-state.d.ts +1 -0
  14. package/dist/types/client/state/canvas-store.d.ts +2 -0
  15. package/dist/types/client/state/intent-bridge.d.ts +1 -1
  16. package/dist/types/json-render/renderer/index.d.ts +2 -1
  17. package/dist/types/json-render/server.d.ts +2 -0
  18. package/dist/types/shared/ax-surface-protocol.d.ts +2 -0
  19. package/dist/types/shared/delimited-text.d.ts +14 -0
  20. package/docs/ax-host-adapter-contract.md +13 -2
  21. package/docs/cli.md +9 -2
  22. package/docs/http-api.md +12 -8
  23. package/docs/mcp.md +8 -1
  24. package/docs/sdk.md +2 -2
  25. package/package.json +1 -1
  26. package/skills/pmx-canvas/SKILL.md +41 -16
  27. package/skills/pmx-canvas/evals/evals.json +112 -2
  28. package/skills/pmx-canvas/references/full-reference.md +9 -4
  29. package/skills/pmx-canvas/references/github-copilot-app-adapter.md +17 -3
  30. package/skills/pmx-canvas/references/installing-pmx-canvas.md +76 -4
  31. package/skills/pmx-canvas-testing/SKILL.md +49 -0
  32. package/src/cli/commands/backup.ts +15 -4
  33. package/src/cli/commands/boards.ts +16 -3
  34. package/src/client/canvas/AttentionHistory.tsx +2 -1
  35. package/src/client/canvas/BoardSwitcher.tsx +3 -3
  36. package/src/client/canvas/CanvasNode.tsx +211 -197
  37. package/src/client/canvas/CanvasViewport.tsx +6 -0
  38. package/src/client/canvas/ChoiceMenu.tsx +122 -0
  39. package/src/client/canvas/CommandBar.tsx +27 -25
  40. package/src/client/canvas/ContextPinBar.tsx +3 -1
  41. package/src/client/canvas/ExpandedNodeOverlay.tsx +3 -3
  42. package/src/client/canvas/ExternalWriters.tsx +4 -3
  43. package/src/client/canvas/FeedbackDialog.tsx +12 -9
  44. package/src/client/canvas/IntentLayer.tsx +2 -2
  45. package/src/client/canvas/SelectionBar.tsx +2 -2
  46. package/src/client/canvas/SessionPanel.tsx +6 -4
  47. package/src/client/canvas/SessionReceipt.tsx +2 -1
  48. package/src/client/canvas/SnapshotPanel.tsx +3 -2
  49. package/src/client/canvas/ToolRail.tsx +48 -100
  50. package/src/client/canvas/TopBar.tsx +2 -2
  51. package/src/client/icons.tsx +118 -1
  52. package/src/client/nodes/BoardNode.tsx +2 -1
  53. package/src/client/nodes/ExtAppFrame.tsx +3 -1
  54. package/src/client/nodes/FileNode.tsx +4 -78
  55. package/src/client/nodes/ImageNode.tsx +2 -1
  56. package/src/client/nodes/MarkdownNode.tsx +2 -1
  57. package/src/client/nodes/McpAppNode.tsx +16 -0
  58. package/src/client/nodes/PromptNode.tsx +3 -1
  59. package/src/client/nodes/use-ax-surface-bridge.ts +4 -2
  60. package/src/client/nodes/viewer-runtime-state.ts +78 -0
  61. package/src/client/state/canvas-store.ts +3 -0
  62. package/src/client/state/intent-bridge.ts +14 -2
  63. package/src/client/theme/global.css +144 -45
  64. package/src/json-render/renderer/index.tsx +58 -38
  65. package/src/json-render/server.ts +8 -1
  66. package/src/mcp/server.ts +2 -2
  67. package/src/server/agent-presence.ts +17 -11
  68. package/src/server/board-export.ts +97 -32
  69. package/src/server/index.ts +9 -7
  70. package/src/server/operations/composites.ts +3 -2
  71. package/src/server/operations/ops/ax-presence.ts +7 -3
  72. package/src/server/operations/ops/boards.ts +12 -5
  73. package/src/server/operations/registry.ts +4 -13
  74. package/src/server/server.ts +13 -4
  75. package/src/shared/ax-surface-protocol.ts +3 -0
  76. package/src/shared/delimited-text.ts +75 -0
@@ -1,9 +1,10 @@
1
1
  import { useState } from 'preact/hooks';
2
- import { IconSteer } from '../icons';
2
+ import { IconSteer, IconClose } from '../icons';
3
3
  import { contextPinnedNodeIds, nodes, toggleContextPin } from '../state/canvas-store';
4
4
  import { steerableAgents } from '../state/presence-store';
5
5
  import { sendSteering } from '../state/session-store';
6
6
  import { modChord } from '../utils/platform';
7
+ import { ChoiceMenu } from './ChoiceMenu';
7
8
 
8
9
  /**
9
10
  * Command bar (rail-chrome-v2 phase 5): the human's steering surface while a
@@ -72,7 +73,7 @@ export function CommandBar() {
72
73
  title="Remove from context"
73
74
  onClick={() => toggleContextPin(id)}
74
75
  >
75
- ×
76
+ <IconClose size={14} />
76
77
  </button>
77
78
  </span>
78
79
  );
@@ -83,33 +84,34 @@ export function CommandBar() {
83
84
  <div class="command-bar-composer">
84
85
  <IconSteer size={15} class="command-bar-icon" />
85
86
  {showPicker && (
86
- <select
87
- class="command-bar-target"
88
- aria-label="Steer which agent"
89
- title="Connected agents — steering reaches all of them unless you address one"
87
+ <ChoiceMenu
88
+ className="command-bar-target"
89
+ label="Steer which agent"
90
90
  value={effectiveTarget ?? ''}
91
- onChange={(e) => setTarget((e.target as HTMLSelectElement).value || null)}
92
- >
93
- <option value="">All agents</option>
94
- {agents.map((agent) => {
95
- // Pump health at the point of choice: unclaimed steers already
96
- // sitting in this agent's queue (a growing number means its loop
97
- // is not polling), and proof-of-polling when its consumer key
98
- // claimed recently — attached alone doesn't mean anyone reads
99
- // the inbox.
100
- const queued = agent.pendingSteers && agent.pendingSteers > 0 ? ` · ${agent.pendingSteers} queued` : '';
101
- const polling = agent.polling ? ' · polling' : '';
102
- return (
103
- <option key={agent.value} value={agent.value} disabled={!agent.steerable}>
104
- {agent.attached
91
+ above
92
+ onChange={(value) => setTarget(value || null)}
93
+ options={[
94
+ { value: '', label: 'All agents' },
95
+ ...agents.map((agent) => {
96
+ // Pump health at the point of choice: unclaimed steers already
97
+ // sitting in this agent's queue (a growing number means its loop
98
+ // is not polling), and proof-of-polling when its consumer key
99
+ // claimed recently — attached alone doesn't mean anyone reads
100
+ // the inbox.
101
+ const queued = agent.pendingSteers && agent.pendingSteers > 0 ? ` · ${agent.pendingSteers} queued` : '';
102
+ const polling = agent.polling ? ' · polling' : '';
103
+ return {
104
+ value: agent.value,
105
+ disabled: !agent.steerable,
106
+ label: agent.attached
105
107
  ? `${agent.label}${polling}${queued}`
106
108
  : agent.steerable
107
109
  ? `${agent.label} · writer${polling}${queued}`
108
- : `${agent.label} · no inbox`}
109
- </option>
110
- );
111
- })}
112
- </select>
110
+ : `${agent.label} · no inbox`,
111
+ };
112
+ }),
113
+ ]}
114
+ />
113
115
  )}
114
116
  <input
115
117
  class="command-bar-input"
@@ -1,5 +1,6 @@
1
1
  import { clearContextPins, contextPinnedNodeIds } from '../state/canvas-store';
2
2
  import { attentionHistoryOpen } from '../state/attention-store';
3
+ import { IconClose } from '../icons';
3
4
 
4
5
  export function ContextPinBar() {
5
6
  const count = contextPinnedNodeIds.value.size;
@@ -15,8 +16,9 @@ export function ContextPinBar() {
15
16
  class="context-pin-bar-btn context-pin-bar-clear"
16
17
  onClick={clearContextPins}
17
18
  title="Clear all context pins"
19
+ aria-label="Clear all context pins"
18
20
  >
19
- {'\u00d7'}
21
+ <IconClose size={14} />
20
22
  </button>
21
23
  </div>
22
24
  );
@@ -25,7 +25,7 @@ import {
25
25
  pendingExpandedNodeCloseId,
26
26
  toggleContextPin,
27
27
  } from '../state/canvas-store';
28
- import { getNodeIcon } from '../icons';
28
+ import { getNodeIcon, IconClose, IconExternalLink } from '../icons';
29
29
  import { useFocusTrap } from './use-focus-trap';
30
30
  import { TYPE_LABELS } from '../types';
31
31
  import type { CanvasNodeState } from '../types';
@@ -316,7 +316,7 @@ export function ExpandedNodeOverlay() {
316
316
  onClick={() => void openNodeAsSite(node)}
317
317
  title="Open as a full-page site in the system browser"
318
318
  >
319
- Open in tab ↗
319
+ Open in tab <IconExternalLink size={14} />
320
320
  </button>
321
321
  )}
322
322
  {canPresent && (
@@ -331,7 +331,7 @@ export function ExpandedNodeOverlay() {
331
331
  )}
332
332
  </div>
333
333
  <button type="button" class="expanded-close" onClick={handleClose} title="Close (Esc)" aria-label="Close">
334
- ×
334
+ <IconClose />
335
335
  </button>
336
336
  </div>
337
337
 
@@ -1,4 +1,5 @@
1
1
  import { useEffect, useRef } from 'preact/hooks';
2
+ import { IconClose, IconExternalLink } from '../icons';
2
3
  import type { AgentPresence } from '../../shared/agent-presence.js';
3
4
  import { vetoGhostIntent } from '../state/intent-bridge';
4
5
  import { type ClientIntent, intents, removeIntent } from '../state/intent-store';
@@ -149,7 +150,7 @@ export function ActivityFeed() {
149
150
  activityFeedOpen.value = false;
150
151
  }}
151
152
  >
152
- ×
153
+ <IconClose />
153
154
  </button>
154
155
  </div>
155
156
  <div class="activity-filters">
@@ -226,7 +227,7 @@ export function ActivityFeed() {
226
227
  void startSession();
227
228
  }}
228
229
  >
229
- Start session ↗
230
+ Start session <IconExternalLink size={14} />
230
231
  </button>
231
232
  </div>
232
233
  </div>
@@ -267,7 +268,7 @@ export function WritersSheet() {
267
268
  <span class="writers-title">Connected writers</span>
268
269
  <span class="writers-board">{sessionId.value.slice(0, 12)}</span>
269
270
  <button type="button" class="activity-feed-close" aria-label="Close writers" onClick={close}>
270
- ×
271
+ <IconClose />
271
272
  </button>
272
273
  </div>
273
274
  <div class="writers-body">
@@ -1,5 +1,7 @@
1
1
  import { useRef, useState } from 'preact/hooks';
2
2
  import { useFocusTrap } from './use-focus-trap';
3
+ import { IconExternalLink } from '../icons';
4
+ import { ChoiceMenu } from './ChoiceMenu';
3
5
 
4
6
  /** Draft only: GitHub owns authentication and the final public submission. */
5
7
  export function FeedbackDialog({ onClose }: { onClose: () => void }) {
@@ -39,14 +41,15 @@ export function FeedbackDialog({ onClose }: { onClose: () => void }) {
39
41
  public. Only what you enter below is included; no board contents, workspace paths, screenshots, or logs are
40
42
  attached.
41
43
  </p>
42
- <label>
43
- Feedback type
44
- <select class="text-prompt-input" value={kind} onChange={(e) => setKind(e.currentTarget.value)}>
45
- <option>Bug report</option>
46
- <option>Feature request</option>
47
- <option>General feedback</option>
48
- </select>
49
- </label>
44
+ <div class="feedback-field">
45
+ <span>Feedback type</span>
46
+ <ChoiceMenu
47
+ label="Feedback type"
48
+ value={kind}
49
+ onChange={setKind}
50
+ options={['Bug report', 'Feature request', 'General feedback'].map((value) => ({ value, label: value }))}
51
+ />
52
+ </div>
50
53
  <label>
51
54
  Title
52
55
  <input
@@ -77,7 +80,7 @@ export function FeedbackDialog({ onClose }: { onClose: () => void }) {
77
80
  Cancel
78
81
  </button>
79
82
  <button type="submit" class="text-prompt-confirm" disabled={!title.trim() || !description.trim()}>
80
- Continue on GitHub ↗
83
+ Continue on GitHub <IconExternalLink size={14} />
81
84
  </button>
82
85
  </div>
83
86
  </form>
@@ -2,7 +2,7 @@ import { useEffect } from 'preact/hooks';
2
2
  import { nodes } from '../state/canvas-store';
3
3
  import { hoveredIntentId, intents, removeIntent, type ClientIntent } from '../state/intent-store';
4
4
  import { vetoGhostIntent } from '../state/intent-bridge';
5
- import { getNodeIcon } from '../icons';
5
+ import { getNodeIcon, IconClose } from '../icons';
6
6
  import { TYPE_LABELS } from '../types';
7
7
  import type { CanvasNodeState } from '../types';
8
8
 
@@ -102,7 +102,7 @@ function GhostInfo({ intent }: { intent: ClientIntent }) {
102
102
  void vetoGhostIntent(intent);
103
103
  }}
104
104
  >
105
- ✕
105
+ <IconClose size={14} />
106
106
  </button>
107
107
  )}
108
108
  </div>
@@ -1,5 +1,5 @@
1
1
  import { useCallback } from 'preact/hooks';
2
- import { IconArrange } from '../icons';
2
+ import { IconArrange, IconClose } from '../icons';
3
3
  import { attentionHistoryOpen } from '../state/attention-store';
4
4
  import {
5
5
  addContextPins,
@@ -204,7 +204,7 @@ export function SelectionBar() {
204
204
  </BarHint>
205
205
  <BarHint label="Clear selection" shortcut="Esc" side="up">
206
206
  <button type="button" class="selection-bar-icon" onClick={clearSelection} aria-label="Clear selection">
207
- ×
207
+ <IconClose size={14} />
208
208
  </button>
209
209
  </BarHint>
210
210
  </div>
@@ -1,4 +1,5 @@
1
1
  import { useEffect, useState } from 'preact/hooks';
2
+ import { IconChevronRight, IconUndo } from '../icons';
2
3
  import { useNow } from './use-now';
3
4
  import { agentIdentityHue, agentPhaseLabel } from '../../shared/agent-presence.js';
4
5
  import { formatCountdown, gateRemainingMs } from '../../shared/approval-gates.js';
@@ -301,7 +302,7 @@ function TimelineRow({ entry }: { entry: TimelineEntry }) {
301
302
  data-testid="timeline-undo"
302
303
  onClick={() => void undoAgentEdit(entry)}
303
304
  >
304
- ↩ undo this edit
305
+ <IconUndo size={14} /> undo this edit
305
306
  </button>
306
307
  )}
307
308
  {undone && <div class="session-timeline-undone">undone · steering sent</div>}
@@ -387,8 +388,9 @@ export function SessionPanel() {
387
388
  class="session-collapse"
388
389
  onClick={() => setManuallyOpened(false)}
389
390
  title="Collapse session panel"
391
+ aria-label="Collapse session panel"
390
392
  >
391
- ›
393
+ <IconChevronRight />
392
394
  </button>
393
395
  </header>
394
396
 
@@ -406,7 +408,7 @@ export function SessionPanel() {
406
408
  Agent edit on top: {undoable.body}
407
409
  </span>
408
410
  <button type="button" class="session-undo-btn" onClick={() => void undoAgentEdit(undoable)}>
409
- ↩ Undo
411
+ <IconUndo size={14} /> Undo
410
412
  </button>
411
413
  </div>
412
414
  );
@@ -455,7 +457,7 @@ export function SessionPanel() {
455
457
  {summary}
456
458
  </span>
457
459
  <span class={`session-section-chevron${showList ? ' is-open' : ''}`} aria-hidden="true">
458
- ›
460
+ <IconChevronRight size={14} />
459
461
  </span>
460
462
  </button>
461
463
  {showList &&
@@ -1,5 +1,6 @@
1
1
  import { useState } from 'preact/hooks';
2
2
  import { dismissSessionReceipt, sessionReceipt } from '../state/session-store';
3
+ import { IconClose } from '../icons';
3
4
 
4
5
  /**
5
6
  * Session receipt (rail-chrome-v2 phase 5, design item 2): a dismissible card
@@ -74,7 +75,7 @@ export function SessionReceipt({ onOpenSnapshots }: { onOpenSnapshots: () => voi
74
75
  onClick={dismissSessionReceipt}
75
76
  aria-label="Dismiss receipt"
76
77
  >
77
- ×
78
+ <IconClose />
78
79
  </button>
79
80
  </div>
80
81
  <div class="session-receipt-tiles">
@@ -1,4 +1,5 @@
1
1
  import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
2
+ import { IconClose } from '../icons';
2
3
  import {
3
4
  type CanvasSnapshotInfo,
4
5
  deleteSnapshot,
@@ -183,7 +184,7 @@ export function SnapshotPanel({
183
184
  aria-label="Delete snapshot"
184
185
  disabled={restoringId !== null}
185
186
  >
186
- ✕
187
+ <IconClose size={14} />
187
188
  </button>
188
189
  </>
189
190
  );
@@ -195,7 +196,7 @@ export function SnapshotPanel({
195
196
  <span class="snapshot-panel-sub">snapshots + sessions</span>
196
197
  <span class="snapshot-panel-spacer" />
197
198
  <button type="button" class="snapshot-panel-close" onClick={onClose} title="Close" aria-label="Close history">
198
- ×
199
+ <IconClose />
199
200
  </button>
200
201
  </div>
201
202
 
@@ -8,10 +8,7 @@ import {
8
8
  IconLogo,
9
9
  IconMinimap,
10
10
  IconMoon,
11
- IconNodeFile,
12
11
  IconNodeGroup,
13
- IconNodeHtml,
14
- IconNodeImage,
15
12
  IconNodeMarkdown,
16
13
  IconNodeWebpage,
17
14
  IconPen,
@@ -21,6 +18,7 @@ import {
21
18
  IconSun,
22
19
  IconTextAnnotation,
23
20
  IconTrace,
21
+ IconUpload,
24
22
  } from '../icons';
25
23
  import {
26
24
  autoArrange,
@@ -176,37 +174,33 @@ export function ToolRail({
176
174
  const traceNodeCount = Array.from(nodes.value.values()).filter((n) => n.type === 'trace').length;
177
175
  const edgeCount = edges.value.size;
178
176
  const [feedbackOpen, setFeedbackOpen] = useState(false);
179
- const [openMenu, setOpenMenu] = useState<null | 'theme' | 'annotate'>(null);
180
- const [menuAnchor, setMenuAnchor] = useState<{ top: number; bottom: number; right: number } | null>(null);
177
+ const [openMenu, setOpenMenu] = useState<null | 'theme'>(null);
178
+ const [menuAnchor, setMenuAnchor] = useState<{ bottom: number; right: number } | null>(null);
181
179
  const railRef = useRef<HTMLDivElement>(null);
182
- const documentInput = useRef<HTMLInputElement>(null);
180
+ const fileInput = useRef<HTMLInputElement>(null);
183
181
 
184
- const toggleMenu = (menu: 'theme' | 'annotate') => (e: MouseEvent) => {
182
+ const toggleMenu = (menu: 'theme') => (e: MouseEvent) => {
185
183
  if (openMenu === menu) {
186
184
  setOpenMenu(null);
187
185
  return;
188
186
  }
189
187
  const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
190
- setMenuAnchor({ top: rect.top, bottom: rect.bottom, right: rect.right });
188
+ setMenuAnchor({ bottom: rect.bottom, right: rect.right });
191
189
  setOpenMenu(menu);
192
190
  };
193
191
 
194
192
  // The rail scrolls (overflow-y:auto), so an absolutely-positioned popover
195
193
  // would be clipped by it. Fixed positioning from the trigger's rect escapes
196
- // the scroll container; the annotate menu top-aligns with its trigger, the
197
- // theme menu (bottom cluster) bottom-aligns so it grows upward.
198
- const sideMenuStyle = (alignBottom: boolean) =>
199
- menuAnchor
200
- ? alignBottom
201
- ? {
202
- position: 'fixed' as const,
203
- left: `${menuAnchor.right + 8}px`,
204
- bottom: `${window.innerHeight - menuAnchor.bottom}px`,
205
- top: 'auto',
206
- right: 'auto',
207
- }
208
- : { position: 'fixed' as const, left: `${menuAnchor.right + 8}px`, top: `${menuAnchor.top}px`, right: 'auto' }
209
- : undefined;
194
+ // the scroll container; the theme menu bottom-aligns so it grows upward.
195
+ const sideMenuStyle = menuAnchor
196
+ ? {
197
+ position: 'fixed' as const,
198
+ left: `${menuAnchor.right + 8}px`,
199
+ bottom: `${window.innerHeight - menuAnchor.bottom}px`,
200
+ top: 'auto',
201
+ right: 'auto',
202
+ }
203
+ : undefined;
210
204
 
211
205
  useEffect(() => {
212
206
  if (!openMenu) return;
@@ -330,22 +324,20 @@ export function ToolRail({
330
324
  >
331
325
  <IconNodeMarkdown size={15} />
332
326
  </RailButton>
333
- <RailButton label="Image" shortcut="I" onClick={() => promptedCreate('image')}>
334
- <IconNodeImage size={15} />
335
- </RailButton>
336
- <RailButton label="File" shortcut="Shift+F" onClick={() => promptedCreate('file')}>
337
- <IconNodeFile size={15} />
338
- </RailButton>
339
- <RailButton label="Attach document" onClick={() => documentInput.current?.click()}>
340
- <span aria-hidden="true">↥</span>
327
+ <RailButton
328
+ label="Attach files"
329
+ ariaLabel="Attach files"
330
+ detail="Images (PNG, JPEG, SVG…), Markdown and text/code files. PDF, Word, Excel, PowerPoint and OpenDocument use agent import with review."
331
+ onClick={() => fileInput.current?.click()}
332
+ >
333
+ <IconUpload size={15} />
341
334
  </RailButton>
342
335
  <input
343
- ref={documentInput}
336
+ ref={fileInput}
344
337
  type="file"
345
338
  hidden
346
339
  multiple
347
- accept=".pdf,.ppt,.pptx,.xls,.xlsx,.doc,.docx,.odt,.ods,.odp"
348
- aria-label="Attach documents"
340
+ aria-label="Attach files"
349
341
  onChange={(event) => {
350
342
  const area = canvasArea();
351
343
  const view = viewport.value;
@@ -360,17 +352,6 @@ export function ToolRail({
360
352
  <RailButton label="Webpage" shortcut="W" onClick={() => promptedCreate('webpage')}>
361
353
  <IconNodeWebpage size={15} />
362
354
  </RailButton>
363
- <RailButton
364
- label="HTML surface"
365
- shortcut="H"
366
- onClick={() =>
367
- void createNodeInView({ type: 'html', title: 'HTML surface' }).catch((error) =>
368
- logRailError('create html', error),
369
- )
370
- }
371
- >
372
- <IconNodeHtml />
373
- </RailButton>
374
355
  <RailButton
375
356
  label="Group"
376
357
  shortcut="G"
@@ -382,61 +363,28 @@ export function ToolRail({
382
363
  >
383
364
  <IconNodeGroup size={15} />
384
365
  </RailButton>
385
- <span class="toolbar-menu-anchor">
386
- <RailButton
387
- label="Annotate"
388
- shortcut="A"
389
- detail="Draw · Text note · Eraser"
390
- active={annotationTool !== null}
391
- menuOpen={openMenu === 'annotate'}
392
- onClick={toggleMenu('annotate')}
393
- >
394
- {annotationTool === 'eraser' ? (
395
- <IconEraser />
396
- ) : annotationTool === 'text' ? (
397
- <IconTextAnnotation />
398
- ) : (
399
- <IconPen />
400
- )}
401
- </RailButton>
402
- {openMenu === 'annotate' && (
403
- <div class="toolbar-menu" style={sideMenuStyle(false)} role="menu" aria-label="Annotate">
404
- <button
405
- type="button"
406
- class={`toolbar-menu-item${annotationTool === 'pen' ? ' active' : ''}`}
407
- onClick={() => {
408
- onSetAnnotationTool(annotationTool === 'pen' ? null : 'pen');
409
- setOpenMenu(null);
410
- }}
411
- >
412
- <IconPen />
413
- <span>{annotationTool === 'pen' ? 'Stop annotating' : 'Draw (A)'}</span>
414
- </button>
415
- <button
416
- type="button"
417
- class={`toolbar-menu-item${annotationTool === 'text' ? ' active' : ''}`}
418
- onClick={() => {
419
- onSetAnnotationTool(annotationTool === 'text' ? null : 'text');
420
- setOpenMenu(null);
421
- }}
422
- >
423
- <IconTextAnnotation />
424
- <span>{annotationTool === 'text' ? 'Stop text notes' : 'Text note'}</span>
425
- </button>
426
- <button
427
- type="button"
428
- class={`toolbar-menu-item${annotationTool === 'eraser' ? ' active' : ''}`}
429
- onClick={() => {
430
- onSetAnnotationTool(annotationTool === 'eraser' ? null : 'eraser');
431
- setOpenMenu(null);
432
- }}
433
- >
434
- <IconEraser />
435
- <span>{annotationTool === 'eraser' ? 'Stop erasing' : 'Eraser'}</span>
436
- </button>
437
- </div>
438
- )}
439
- </span>
366
+ <RailButton
367
+ label="Draw"
368
+ shortcut="A"
369
+ active={annotationTool === 'pen'}
370
+ onClick={() => onSetAnnotationTool(annotationTool === 'pen' ? null : 'pen')}
371
+ >
372
+ <IconPen />
373
+ </RailButton>
374
+ <RailButton
375
+ label="Text note"
376
+ active={annotationTool === 'text'}
377
+ onClick={() => onSetAnnotationTool(annotationTool === 'text' ? null : 'text')}
378
+ >
379
+ <IconTextAnnotation />
380
+ </RailButton>
381
+ <RailButton
382
+ label="Eraser"
383
+ active={annotationTool === 'eraser'}
384
+ onClick={() => onSetAnnotationTool(annotationTool === 'eraser' ? null : 'eraser')}
385
+ >
386
+ <IconEraser />
387
+ </RailButton>
440
388
 
441
389
  <div class="rail-spacer" />
442
390
 
@@ -484,7 +432,7 @@ export function ToolRail({
484
432
  {canvasThemeScheme(activeTheme) === 'dark' ? <IconSun /> : <IconMoon />}
485
433
  </RailButton>
486
434
  {openMenu === 'theme' && (
487
- <div class="toolbar-menu" style={sideMenuStyle(true)} role="menu" aria-label="Theme">
435
+ <div class="toolbar-menu" style={sideMenuStyle} role="menu" aria-label="Theme">
488
436
  {CANVAS_THEMES.map((name) => (
489
437
  <button
490
438
  key={name}
@@ -4,7 +4,7 @@ import { BoardSwitcher } from './BoardSwitcher';
4
4
  import type { ComponentChildren } from 'preact';
5
5
  import { presenting } from '../state/presentation';
6
6
  import { useEffect, useState } from 'preact/hooks';
7
- import { IconFitAll, IconZoomIn, IconZoomOut } from '../icons';
7
+ import { IconFitAll, IconZoomIn, IconZoomOut, IconClose } from '../icons';
8
8
  import {
9
9
  animateViewport,
10
10
  connectionStatus,
@@ -121,7 +121,7 @@ function AgentChip() {
121
121
  void endSession({ source: session.source, agentId: session.agentId });
122
122
  }}
123
123
  >
124
- ×
124
+ <IconClose size={14} />
125
125
  </button>
126
126
  </span>
127
127
  </BarHint>